@noob-stupid/dsh-plugin-console 0.3.31 → 0.3.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +113 -0
- package/lib/client.js +186 -37
- package/lib/index.js +256 -53
- package/package.json +3 -2
package/SECURITY.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# 安全政策 Security Policy
|
|
2
|
+
|
|
3
|
+
本文件说明 `@noob-stupid/dsh-plugin-console`(仓库 [Noob-stupid/dsh-plugin-hub](https://github.com/Noob-stupid/dsh-plugin-hub))的安全政策:这个插件**能做什么**(能力清单)、已内置哪些护栏、自动化静态扫描容易误报的地方,以及如何报告漏洞。
|
|
4
|
+
|
|
5
|
+
英文能力摘要见文末 [Capability summary (English)](#capability-summary-english)。
|
|
6
|
+
|
|
7
|
+
## 支持的版本
|
|
8
|
+
|
|
9
|
+
| 版本 | 支持状态 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| 0.3.x(当前 latest) | ✅ 支持 |
|
|
12
|
+
| 0.3.x 更早版本 | ⚠️ 仅严重漏洞修复 |
|
|
13
|
+
|
|
14
|
+
## 报告漏洞
|
|
15
|
+
|
|
16
|
+
**请勿在公开 Issue 中披露安全漏洞**。请通过以下方式私下报告:
|
|
17
|
+
|
|
18
|
+
1. 通过 GitHub 的 [Security Advisory](https://github.com/Noob-stupid/dsh-plugin-hub/security/advisories/new)(推荐,私密)
|
|
19
|
+
2. 或给仓库 owner 发 GitHub 私信
|
|
20
|
+
|
|
21
|
+
请在报告中包含:
|
|
22
|
+
|
|
23
|
+
- 影响面(哪条路由/哪个文件、可造成什么危害)
|
|
24
|
+
- 复现步骤(最小化,脱敏密钥)
|
|
25
|
+
- 受影响版本(插件版本 + DSH 版本 + 操作系统)
|
|
26
|
+
|
|
27
|
+
## 处理承诺
|
|
28
|
+
|
|
29
|
+
- 收到报告后 3 天内确认收到;
|
|
30
|
+
- 修复后发布补丁版本前,不会公开细节;
|
|
31
|
+
- 修复发布后,会在 CHANGELOG 中致谢报告者(如需匿名请说明)。
|
|
32
|
+
|
|
33
|
+
## 能力清单:这个插件能做什么
|
|
34
|
+
|
|
35
|
+
插件控制台是**插件的管理器**——安装/更新/启停/审查其他 DSH 插件,因此需要文件写入、网络访问与进程控制能力。以下能力**全部由你在控制台里主动触发**,或属于你显式配置过的后台行为(组件自启动、索引源拉取)。**没有遥测、没有硬编码的回传地址**;除你调用的功能(搜索、市场索引、安装、检查更新)之外,不向外发送任何数据。
|
|
36
|
+
|
|
37
|
+
### 读(文件系统)
|
|
38
|
+
|
|
39
|
+
- DSH profile 目录:`cordis.yml` / `cordis.patch.yml`、`node_modules` 元数据、已装插件的 `package.json` / `README.md` / 源码(用于兼容性源码扫描)
|
|
40
|
+
- `<dshHome>` 状态:软件源配置、组件记录、任务与缓存状态、GitHub 登录**状态**(令牌本身永不下发到浏览器)
|
|
41
|
+
- DSH 安装目录:定位 `bin.js`,用于「重启服务」
|
|
42
|
+
|
|
43
|
+
### 写(白名单)
|
|
44
|
+
|
|
45
|
+
| 目标 | 用途 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| profile 的 `cordis.patch.yml` | 启用/停用/自动停用/新增插件行 |
|
|
48
|
+
| `<dshHome>/plugin-console/*` | 软件源、组件记录、任务与缓存状态 |
|
|
49
|
+
| profile 的 `node_modules` | 安装或卸载你指定的插件 |
|
|
50
|
+
| `<dshHome>/skills` | 安装或删除你指定的技能 |
|
|
51
|
+
| 临时目录(`%TEMP%`、仓库落地目录) | git 克隆、下载、AI 调研用检出,任务结束即清理 |
|
|
52
|
+
|
|
53
|
+
所有写入都经过 `isAllowedWritePath` 校验(DSH profile、`~/.dsh`、`~/.openviking`、ASCII 数据根);「仓库落地」的删除额外要求目标位于你配置的仓库目录内。
|
|
54
|
+
|
|
55
|
+
### 网络
|
|
56
|
+
|
|
57
|
+
- GitHub API / raw:搜索、元数据、release 查询,带镜像回退(ghproxy / ghfast / jsDelivr)
|
|
58
|
+
- **npm 软件源:列表完全由你配置**。默认 `registry.npmmirror.com`(主)+ `registry.npmjs.org`;内网 Verdaccio / Nexus 的明文 `http` **只在环回与 RFC1918 私网地址**上被接受
|
|
59
|
+
- 市场静态索引(一个 JSON 文件),地址可配置——这是「完全离线 / 内网市场」的实现方式
|
|
60
|
+
- Git 克隆源,可配置(`{owner}/{repo}` 模板,含 `file://` 本地裸仓库)
|
|
61
|
+
|
|
62
|
+
### 进程
|
|
63
|
+
|
|
64
|
+
- `pnpm`(优先)/ `npm` 安装:固定 argv 数组、`shell: false`
|
|
65
|
+
- `git clone` / `git ls-remote`:非交互环境(`GIT_TERMINAL_PROMPT=0`、`GCM_INTERACTIVE=never`、`GIT_ASKPASS=echo`、`SSH_ASKPASS=echo`),克隆绝不会卡在凭据弹窗上
|
|
66
|
+
- `taskkill.exe /PID <记录的 PID> /F`:只用于停止**你注册过**的组件;「重启服务」走计划任务 + `Stop-Process`
|
|
67
|
+
- 组件自启动:DSH 启动时,把你标记为「自启动」的组件用记录的启动命令拉起(以你的用户权限运行)
|
|
68
|
+
|
|
69
|
+
插件**不执行第三方仓库的安装脚本**;套装/聚合安装只按仓库声明的安装语义走 npm / git。
|
|
70
|
+
|
|
71
|
+
### HTTP 面
|
|
72
|
+
|
|
73
|
+
所有路由都在 `/plugin-console` 下,且**仅限本机**:socket 地址不是环回直接 403,`Host` 头必须与服务端口一致,所有非 GET 路由还必须通过同源校验(`Origin` / `Sec-Fetch-Site`)。因为判定用的是 socket 地址,即使 DSH 绑定 `0.0.0.0`,这些路由在局域网内依然不可达。
|
|
74
|
+
|
|
75
|
+
## 已内置的安全护栏
|
|
76
|
+
|
|
77
|
+
| 领域 | 护栏 |
|
|
78
|
+
|---|---|
|
|
79
|
+
| 写路由 | 环回 + `Host` 校验 + 同源校验,仅接受 JSON body |
|
|
80
|
+
| 路径 | 写入路径白名单;仓库落地/删除限定在配置目录内 |
|
|
81
|
+
| 密钥 | GitHub / Gitee 令牌不出本机(只回传 `loggedIn` / `login` 状态);自定义源请求头与 Gitee clientSecret 在响应里打码,并单独存于 secrets 文件 |
|
|
82
|
+
| 克隆 | git 非交互环境(不会弹凭据框、不会挂住) |
|
|
83
|
+
| 供应链 | **没有 `preinstall` / `postinstall` 脚本**;唯一的生命周期脚本是 `prepublishOnly`(发布前 BOM 自检) |
|
|
84
|
+
| 框架升级 | 升级前备份、失败自动回滚;引用了已删除框架 API 的插件会被自动停用,而不是放任整个服务崩溃 |
|
|
85
|
+
| 安装安全 | 注册插件行之前校验 bundle 引用模块,引用无法解析的插件会被拒绝,避免启动崩溃 |
|
|
86
|
+
|
|
87
|
+
## 给自动化静态扫描器的说明(已知误报)
|
|
88
|
+
|
|
89
|
+
如果你用 AST / 启发式扫描器审查本插件,以下模式是有意为之,可以按误报处理:
|
|
90
|
+
|
|
91
|
+
- **`process.kill(pid, 0)`**:signal `0` 的**存活探测**。不发送任何信号、无法终止进程,是 Node.js 判断「记录的组件 PID 是否还活着」的标准写法。
|
|
92
|
+
- **`rmSync(...)`**:23 处调用全部清理本插件自己在安装/克隆/升级流程中创建的目录(临时检出、下载缓存、被替换的旧目录、升级备份);另有 2 处是**用户主动触发**的删除(删技能、删已落地仓库),带名称正则 / 路径前缀校验。
|
|
93
|
+
- **`spawn` / `execFile`**:固定 argv 数组 + `shell: false`,不存在拼接出来的 shell 命令串。
|
|
94
|
+
- **`process.env` 读取**:用于定位 `DSH_HOME` / `LOCALAPPDATA`,不向外传输任何环境变量值。
|
|
95
|
+
- **外联地址**:GitHub、npm 软件源、市场索引——要么是公开默认值,要么是你在控制台里配置的地址。
|
|
96
|
+
|
|
97
|
+
## 用户加固建议
|
|
98
|
+
|
|
99
|
+
- 软件源优先用 `https://`;`http` 只对环回/私网开放,在内网使用时请自行知情。
|
|
100
|
+
- 除非前面有配对门(`dsh-remote-web-ui`)保护 `/api`,否则不要把服务绑定切到局域网。
|
|
101
|
+
- 安装前先看:控制台会展示仓库地址、README 摘要与源码级兼容性扫描结果;可与 [dsh.so 的 artifact 页](https://www.dsh.so/zh/artifact/dsh-plugin-hub/)交叉验证。
|
|
102
|
+
- 只把信任的组件标记「自启动」——它们会在每次 DSH 启动时以你的权限运行。
|
|
103
|
+
|
|
104
|
+
## 已知关注点
|
|
105
|
+
|
|
106
|
+
- 插件控制台只允许本机(127.0.0.1)访问,并对写路由做 Origin 校验(见 `lib/index.js`);
|
|
107
|
+
- AI 赋能执行器有命令/路径白名单与破坏性命令拦截;若发现绕过方式,请优先私信报告。
|
|
108
|
+
|
|
109
|
+
## Capability summary (English)
|
|
110
|
+
|
|
111
|
+
`@noob-stupid/dsh-plugin-console` is a plugin **manager** for DeepSeek Harness. It reads the DSH profile and `<dshHome>` state; it writes only to the profile's `cordis.patch.yml`, `<dshHome>/plugin-console/*`, profile `node_modules` (installs you ask for), `<dshHome>/skills`, and its own temp dirs (all gated by a write-path whitelist). It reaches the network only for GitHub, **user-configured** npm registries, the configurable market index and configured git sources — there is **no telemetry and no hardcoded upload endpoint**. It spawns `pnpm`/`npm`/`git` with fixed argv and `shell: false`, stops only component PIDs you registered (`taskkill /PID`), and restarts DSH through a scheduled-task helper. All HTTP routes are under `/plugin-console`, **loopback-only**, with `Host` and same-origin checks on writes. There are **no `preinstall`/`postinstall` scripts**.
|
|
112
|
+
|
|
113
|
+
Known static-scanner false positives: `process.kill(pid, 0)` is a signal-0 liveness probe; `rmSync` call sites clean up temp/backup directories the plugin created (plus two user-initiated, path-checked deletions); `process.env` reads only locate `DSH_HOME`/`LOCALAPPDATA`.
|
package/lib/client.js
CHANGED
|
@@ -6,7 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
//#region styles
|
|
9
|
-
const css = ".pc_section{width:100%;max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}.pc_section h3{margin:0;font-size:13px;font-weight:600;line-height:20px}.pc_message{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;margin:0}.pc_messageRow{display:flex;align-items:center;gap:10px}.pc_spinner{width:14px;height:14px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}@keyframes pcspin{to{transform:rotate(360deg)}}.pc_message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.pc_message[data-warn=true]{color:var(--dsw-alias-state-warning-primary)}.pc_list{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none;display:grid}.pc_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_rowTop{align-items:center;gap:8px;display:flex}.pc_name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.pc_tag{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px;flex:none}.pc_tag[data-enabled=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-user=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_tag[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_status[data-pending=true]{color:var(--dsw-alias-state-warning-primary)}.pc_toggle[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-skill=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-system=true]{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.pc_tag[data-disabled=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tag[data-suite=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-error=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tabGroup{display:inline-flex;gap:4px;align-items:center}.pc_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;gap:6px;display:flex;align-items:center}.pc_phase[data-phase=failed]{color:var(--dsw-alias-state-error-primary)}.pc_phase[data-phase=active]{color:var(--dsw-alias-state-success-primary)}.pc_toggle{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_toggle:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_toggle:disabled{opacity:.5;cursor:default}.pc_search{gap:8px;display:flex}.pc_search input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;flex:1;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 12px;font-size:13px}.pc_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.pc_search button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:0 14px;font-size:13px}.pc_search button:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_market{flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;display:flex}.pc_item{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_itemTop{align-items:center;gap:8px;display:flex}.pc_itemTop a{color:var(--dsw-alias-state-business-primary);font-size:13px;font-weight:600;text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pc_stars{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}.pc_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_item button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;align-self:flex-start;font-size:12px}.pc_item button:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_item button:disabled{opacity:.5;cursor:default}.pc_detail{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}.pc_marketHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.pc_descWrap{flex-direction:column;display:flex}.pc_installedHead{display:flex;align-items:center;gap:10px}.pc_backTopWrap{position:fixed;top:56px;right:20px;z-index:100;display:flex;flex-direction:column;gap:8px}.pc_backTop{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(22,27,34,.72);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);cursor:pointer;backdrop-filter:blur(4px);opacity:.7}.pc_backTop:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_descTopbar{display:flex;justify-content:flex-end;margin:2px 0}.pc_ghpill{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 10px;font-size:11px;line-height:18px}.pc_ghpill[data-on=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_extraBtn{opacity:.75;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 10px;font-size:12px;line-height:16px;flex:none;transition:opacity .15s ease,border-color 1.5s ease,color 1.5s ease}.pc_extraBtn:hover{opacity:1}.pc_extraBtn[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_extraBtn[data-flash=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_view{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:2px 10px;font-size:12px;line-height:18px;flex:none}.pc_floatPanel{position:fixed;left:20px;top:50%;transform:translateY(-50%);width:340px;max-height:75vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:50;flex-direction:column;gap:8px;display:flex}.pc_refresh{position:sticky;bottom:14px;align-self:flex-end;margin-top:8px;z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_refresh:hover{opacity:.9}.pc_date{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;flex:none}.pc_searchInner{position:relative;flex:1;display:flex;align-items:center;min-width:0}.pc_searchInner input{padding-right:32px;width:100%}.pc_starBtn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;opacity:.75;transition:opacity .15s ease;flex:none}.pc_starBtn:hover{opacity:1}.pc_starBtn[data-active=true]{opacity:1;color:var(--dsw-alias-state-business-primary)}.pc_restartBtn{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_restartBtn:hover{opacity:.9}.pc_relaunchFloat{position:fixed;left:20px;top:50%;transform:translateY(-50%);z-index:70;opacity:.85;border:1px solid var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_relaunchFloat:hover{opacity:1}.pc_aiToggle{position:fixed;right:20px;top:20px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiToggle:hover{opacity:1}.pc_aiToggle[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_sourcesFloat{position:fixed;right:20px;top:104px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_sourcesFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat{position:fixed;right:20px;top:204px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_modeFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_trashBtn{border:0;background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:16px;padding:2px 6px;flex:none;display:flex;align-items:center;border-radius:6px;transition:color .15s ease}.pc_trashBtn:hover{color:var(--dsw-alias-state-error-primary)}.pc_consentRemember{display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer}.pc_modalBackdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:center;justify-content:center}.pc_modalCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:16px 18px;width:min(760px,calc(100vw - 64px));max-height:calc(100vh - 96px);overflow-y:auto;overscroll-behavior:contain;flex-direction:column;gap:10px;display:flex;box-shadow:0 12px 32px rgba(0,0,0,.25)}.pc_modalCard::-webkit-scrollbar{width:8px}.pc_modalCard::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:4px}.pc_srcUrl{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%}.pc_accWrap{display:flex;flex-direction:column;gap:8px}.pc_acc{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}.pc_accHead{width:100%;text-align:left;background:transparent;border:0;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;font-weight:600;line-height:20px;padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:6px}.pc_accHead:hover{background:var(--dsw-alias-bg-layer-3)}.pc_accHead[data-open=true]{border-bottom:1px solid var(--dsw-alias-border-l2)}.pc_accBody{padding:10px 12px;display:flex;flex-direction:column;gap:8px}.pc_ghpill{background:0 0;cursor:pointer}.pc_ghwrap{position:relative;display:inline-flex}.pc_sourceMenu{position:absolute;right:0;top:calc(100% + 6px);z-index:90;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:4px;flex-direction:column;display:flex;min-width:120px;box-shadow:0 8px 24px rgba(0,0,0,.18)}.pc_sourceOpt{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 10px;text-align:left;font-size:12px;line-height:16px}.pc_sourceOpt[data-active=true]{color:var(--dsw-alias-state-business-primary);font-weight:600}.pc_sourceOpt:hover{background:var(--dsw-alias-bg-layer-3)}.pc_aiEmpower{position:fixed;right:20px;top:48px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiEmpower:hover{opacity:.9}.pc_aiEmpower[data-busy=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_aiEmpower[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_compCard{position:fixed;left:8px;top:20px;width:320px;max-height:70vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:45;flex-direction:column;gap:8px;display:flex}.pc_compCard .pc_rowTop{flex-wrap:wrap}.pc_compCard .pc_toggle{padding:3px 8px;font-size:11px}.pc_compCard[data-hidden=true]{display:none}.pc_compDrop{background:rgba(24,28,34,.92);backdrop-filter:blur(6px);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,.35);flex-direction:column;display:flex;gap:2px}.pc_compDropRow{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 8px;text-align:left;font-size:12px;line-height:16px;flex-direction:column;display:flex;gap:2px}.pc_compDropRow:hover{background:var(--dsw-alias-bg-layer-2)}.pc_compDropUrl{color:var(--dsw-alias-label-tertiary);font-size:11px;word-break:break-all}.pc_compToggle{position:fixed;left:336px;top:20px;z-index:46;opacity:.55;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:rgba(22,27,34,.72);font:inherit;cursor:pointer;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1;backdrop-filter:blur(4px);transition:opacity .15s ease}.pc_compToggle:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain{position:fixed;right:70px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:border-color .15s ease}.pc_toolbarMain:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain[data-active=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolbarGroup{position:fixed;right:70px;top:56px;z-index:70;flex-direction:column;gap:8px;display:flex;transform-origin:top right;animation:pcDrawerIn .3s cubic-bezier(.34,1.56,.64,1)}@media (max-width:1160px){.pc_toolbarMain{top:104px}.pc_toolbarGroup{top:140px}.pc_aiToggle{top:104px}.pc_aiEmpower{top:132px}.pc_sourcesFloat{top:188px}.pc_modeFloat{top:288px}}@keyframes pcDrawerIn{from{opacity:0;transform:translateX(28px) scale(.94)}to{opacity:1;transform:translateX(0) scale(1)}}.pc_toolItem{opacity:.85;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_toolItem:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_switch{width:34px;height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;flex:none;transition:background .15s ease,border-color .15s ease}.pc_switch:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_switch[data-on=true]{background:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.pc_switchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;transition:left .15s ease}.pc_switch[data-on=true] .pc_switchKnob{left:18px}.pc_aiSpinner{width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}.pc_aiSpinBtn{position:fixed;right:152px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px}.pc_aiSpinBtn:hover{border-color:var(--dsw-alias-state-business-primary)}";
|
|
9
|
+
const css = ".pc_section{width:100%;max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}.pc_section h3{margin:0;font-size:13px;font-weight:600;line-height:20px}.pc_message{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;margin:0}.pc_messageRow{display:flex;align-items:center;gap:10px}.pc_spinner{width:14px;height:14px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}@keyframes pcspin{to{transform:rotate(360deg)}}.pc_message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.pc_message[data-warn=true]{color:var(--dsw-alias-state-warning-primary)}.pc_list{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none;display:grid}.pc_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_rowTop{align-items:center;gap:8px;display:flex}.pc_name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.pc_tag{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px;flex:none}.pc_tag[data-enabled=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-user=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_tag[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_status[data-pending=true]{color:var(--dsw-alias-state-warning-primary)}.pc_toggle[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-skill=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-system=true]{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.pc_tag[data-disabled=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tag[data-suite=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-error=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tabGroup{display:inline-flex;gap:4px;align-items:center}.pc_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;gap:6px;display:flex;align-items:center}.pc_phase[data-phase=failed]{color:var(--dsw-alias-state-error-primary)}.pc_phase[data-phase=active]{color:var(--dsw-alias-state-success-primary)}.pc_toggle{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_toggle:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_toggle:disabled{opacity:.5;cursor:default}.pc_search{gap:8px;display:flex}.pc_search input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;flex:1;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 12px;font-size:13px}.pc_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.pc_search button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:0 14px;font-size:13px}.pc_search button:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_market{flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;display:flex}.pc_item{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_itemTop{align-items:center;gap:8px;display:flex}.pc_itemTop a{color:var(--dsw-alias-state-business-primary);font-size:13px;font-weight:600;text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pc_stars{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}.pc_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_item button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;align-self:flex-start;font-size:12px}.pc_item button:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_item button:disabled{opacity:.5;cursor:default}.pc_detail{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}.pc_marketHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.pc_descWrap{flex-direction:column;display:flex}.pc_installedHead{display:flex;align-items:center;gap:10px}.pc_backTopWrap{position:fixed;top:56px;right:20px;z-index:100;display:flex;flex-direction:column;gap:8px}.pc_backTop{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(22,27,34,.72);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);cursor:pointer;backdrop-filter:blur(4px);opacity:.7}.pc_backTop:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_descTopbar{display:flex;justify-content:flex-end;margin:2px 0}.pc_ghpill{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 10px;font-size:11px;line-height:18px}.pc_ghpill[data-on=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_extraBtn{opacity:.75;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 10px;font-size:12px;line-height:16px;flex:none;transition:opacity .15s ease,border-color 1.5s ease,color 1.5s ease}.pc_extraBtn:hover{opacity:1}.pc_extraBtn[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_extraBtn[data-flash=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_view{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:2px 10px;font-size:12px;line-height:18px;flex:none}.pc_floatPanel{position:fixed;left:20px;top:50%;transform:translateY(-50%);width:340px;max-height:75vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:50;flex-direction:column;gap:8px;display:flex}.pc_refresh{position:sticky;bottom:14px;align-self:flex-end;margin-top:8px;z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_refresh:hover{opacity:.9}.pc_date{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;flex:none}.pc_searchInner{position:relative;flex:1;display:flex;align-items:center;min-width:0}.pc_searchInner input{padding-right:32px;width:100%}.pc_starBtn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;opacity:.75;transition:opacity .15s ease;flex:none}.pc_starBtn:hover{opacity:1}.pc_starBtn[data-active=true]{opacity:1;color:var(--dsw-alias-state-business-primary)}.pc_restartBtn{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_restartBtn:hover{opacity:.9}.pc_relaunchFloat{position:fixed;left:20px;top:50%;transform:translateY(-50%);z-index:70;opacity:.85;border:1px solid var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_relaunchFloat:hover{opacity:1}.pc_aiToggle{position:fixed;right:20px;top:20px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiToggle:hover{opacity:1}.pc_aiToggle[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_sourcesFloat{position:fixed;right:20px;top:104px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_sourcesFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat{position:fixed;right:20px;top:204px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_modeFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_trashBtn{border:0;background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:16px;padding:2px 6px;flex:none;display:flex;align-items:center;border-radius:6px;transition:color .15s ease}.pc_trashBtn:hover{color:var(--dsw-alias-state-error-primary)}.pc_consentRemember{display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer}.pc_modalBackdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:center;justify-content:center}.pc_modalCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:16px 18px;width:min(760px,calc(100vw - 64px));max-height:calc(100vh - 96px);overflow-y:auto;overscroll-behavior:contain;flex-direction:column;gap:10px;display:flex;box-shadow:0 12px 32px rgba(0,0,0,.25)}.pc_modalCard::-webkit-scrollbar{width:8px}.pc_modalCard::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:4px}.pc_srcUrl{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%}.pc_accWrap{display:flex;flex-direction:column;gap:8px}.pc_acc{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}.pc_accHead{width:100%;text-align:left;background:transparent;border:0;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;font-weight:600;line-height:20px;padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:6px}.pc_accHead:hover{background:var(--dsw-alias-bg-layer-3)}.pc_accHead[data-open=true]{border-bottom:1px solid var(--dsw-alias-border-l2)}.pc_accBody{padding:10px 12px;display:flex;flex-direction:column;gap:8px}.pc_ghpill{background:0 0;cursor:pointer}.pc_ghwrap{position:relative;display:inline-flex}.pc_sourceMenu{position:absolute;right:0;top:calc(100% + 6px);z-index:90;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:4px;flex-direction:column;display:flex;min-width:120px;box-shadow:0 8px 24px rgba(0,0,0,.18)}.pc_sourceOpt{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 10px;text-align:left;font-size:12px;line-height:16px}.pc_sourceOpt[data-active=true]{color:var(--dsw-alias-state-business-primary);font-weight:600}.pc_sourceOpt:hover{background:var(--dsw-alias-bg-layer-3)}.pc_aiEmpower{position:fixed;right:20px;top:48px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiEmpower:hover{opacity:.9}.pc_aiEmpower[data-busy=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_aiEmpower[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_compCard{position:fixed;left:8px;top:20px;width:320px;max-height:70vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:45;flex-direction:column;gap:8px;display:flex}.pc_compCard .pc_rowTop{flex-wrap:wrap}.pc_compCard .pc_toggle{padding:3px 8px;font-size:11px}.pc_compCard[data-hidden=true]{display:none}.pc_compDrop{background:rgba(24,28,34,.92);backdrop-filter:blur(6px);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,.35);flex-direction:column;display:flex;gap:2px}.pc_compDropRow{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 8px;text-align:left;font-size:12px;line-height:16px;flex-direction:column;display:flex;gap:2px}.pc_compDropRow:hover{background:var(--dsw-alias-bg-layer-2)}.pc_compDropUrl{color:var(--dsw-alias-label-tertiary);font-size:11px;word-break:break-all}.pc_compToggle{position:fixed;left:336px;top:20px;z-index:46;opacity:.55;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:rgba(22,27,34,.72);font:inherit;cursor:pointer;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1;backdrop-filter:blur(4px);transition:opacity .15s ease}.pc_compToggle:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain{position:fixed;right:70px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:border-color .15s ease}.pc_toolbarMain:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain[data-active=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolbarGroup{position:fixed;right:70px;top:56px;z-index:70;flex-direction:column;gap:8px;display:flex;transform-origin:top right;animation:pcDrawerIn .3s cubic-bezier(.34,1.56,.64,1)}@media (max-width:1160px){.pc_toolbarMain{top:104px}.pc_toolbarGroup{top:140px}.pc_aiToggle{top:104px}.pc_aiEmpower{top:132px}.pc_sourcesFloat{top:188px}.pc_modeFloat{top:288px}}@keyframes pcDrawerIn{from{opacity:0;transform:translateX(28px) scale(.94)}to{opacity:1;transform:translateX(0) scale(1)}}.pc_toolItem{opacity:.85;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_toolItem:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_switch{width:34px;height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;flex:none;transition:background .15s ease,border-color .15s ease}.pc_switch:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_switch[data-on=true]{background:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.pc_switchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;transition:left .15s ease}.pc_switch[data-on=true] .pc_switchKnob{left:18px}.pc_aiSpinner{width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}.pc_aiSpinBtn{position:fixed;right:152px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px}.pc_aiSpinBtn:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_tag[data-bad=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}";
|
|
10
10
|
const tagId = "@noob-stupid/dsh-plugin-console/PluginConsoleTab.css";
|
|
11
11
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
12
12
|
const tag = document.createElement("style");
|
|
@@ -275,6 +275,13 @@ window.__ModuleLoader__.load({
|
|
|
275
275
|
sourcesDesc: "分工:① 软件源 = 下载 npm 包(安装用);② 索引源 = 市场首页清单 JSON;③ Git 源 = 克隆仓库代码(无 npm 包时);④ 搜索源 = 搜索时查哪个平台——搜到之后仍按 ①/③ 下载",
|
|
276
276
|
sourcePrimary: "主源",
|
|
277
277
|
setPrimary: "设为主源",
|
|
278
|
+
registryScan: "扫描软件源",
|
|
279
|
+
registryScanHint: "逐个探测每个软件源:是否可达、响应延迟、以及该源上 dsh-plugin-console 的最新版本(用于判断主源是否最优)",
|
|
280
|
+
registryScanning: "正在扫描…",
|
|
281
|
+
registryScanSummary: "扫描完成:{ok}/{total} 个可达",
|
|
282
|
+
registryScanBest: "最新版本 {version} 来自:{name}",
|
|
283
|
+
registryScanNoVersion: "该源上没有这个包",
|
|
284
|
+
registryScanFail: "不可达",
|
|
278
285
|
setPrimaryHint: "设为主源:优先使用它,失败时自动回退到其他源",
|
|
279
286
|
editSourceHint: "编辑:修改这个源的名称与地址",
|
|
280
287
|
removeSourceHint: "删除:从列表中移除这个源(不影响已安装的插件)",
|
|
@@ -611,6 +618,13 @@ window.__ModuleLoader__.load({
|
|
|
611
618
|
sourcesDesc: "Roles: ① registry = download npm packages (install); ② index sources = market list JSON; ③ git sources = clone repo code (when no npm package); ④ search sources = where to search — results are still downloaded via ①/③",
|
|
612
619
|
sourcePrimary: "Primary",
|
|
613
620
|
setPrimary: "Set primary",
|
|
621
|
+
registryScan: "Scan sources",
|
|
622
|
+
registryScanHint: "Probe each npm registry: reachability, latency, and the latest dsh-plugin-console version it serves (to pick the best primary)",
|
|
623
|
+
registryScanning: "Scanning…",
|
|
624
|
+
registryScanSummary: "Scan complete: {ok}/{total} reachable",
|
|
625
|
+
registryScanBest: "Newest {version} comes from: {name}",
|
|
626
|
+
registryScanNoVersion: "Package not on this registry",
|
|
627
|
+
registryScanFail: "unreachable",
|
|
614
628
|
setPrimaryHint: "Set primary: tried first, falls back to the others on failure",
|
|
615
629
|
editSourceHint: "Edit: change this source's name and URL",
|
|
616
630
|
removeSourceHint: "Remove: delete this source from the list (installed plugins unaffected)",
|
|
@@ -1008,23 +1022,31 @@ window.__ModuleLoader__.load({
|
|
|
1008
1022
|
return out;
|
|
1009
1023
|
}
|
|
1010
1024
|
/** 批量识别搜索结果:官方通道(根包 dsh.bundle)/ 聚合仓库(private+workspaces,子包才是插件)/ 普通项目。 */
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1025
|
+
/** 补标「官方 / 聚合」标记。每条要发网络请求(raw 多通道竞速),
|
|
1026
|
+
* 因此限制并发(默认 6 条一批),避免打满浏览器连接数拖慢整页。 */
|
|
1027
|
+
async function enrichOfficialBundle(items, concurrency = 6) {
|
|
1028
|
+
const out = [];
|
|
1029
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
1030
|
+
const batch = items.slice(i, i + concurrency);
|
|
1031
|
+
const done = await Promise.all(batch.map(async (item) => {
|
|
1032
|
+
let official = null;
|
|
1033
|
+
let aggregate = false;
|
|
1034
|
+
const pkgText = await fetchRawText(item.fullName, item.defaultBranch, "package.json", item.source ?? "github");
|
|
1035
|
+
if (pkgText !== null) {
|
|
1036
|
+
try {
|
|
1037
|
+
const pkg = JSON.parse(pkgText);
|
|
1038
|
+
if (typeof pkg.dsh?.bundle?.patch === "string") {
|
|
1039
|
+
official = true;
|
|
1040
|
+
} else if (pkg.private === true && (Array.isArray(pkg.workspaces) || /(^|-)dsh[-/]/u.test(String(pkg.name ?? "")))) {
|
|
1041
|
+
aggregate = true;
|
|
1042
|
+
}
|
|
1043
|
+
} catch {}
|
|
1044
|
+
}
|
|
1045
|
+
return { ...item, official, aggregate };
|
|
1046
|
+
}));
|
|
1047
|
+
out.push(...done);
|
|
1048
|
+
}
|
|
1049
|
+
return out;
|
|
1028
1050
|
}
|
|
1029
1051
|
/** 源地址校验(与宿主端一致):https 任意;http 仅限本机/私网(内网源常用 http)。 */
|
|
1030
1052
|
function isAllowedSourceUrl(url) {
|
|
@@ -1071,6 +1093,87 @@ window.__ModuleLoader__.load({
|
|
|
1071
1093
|
const [indexData, setIndexData] = react.useState(null);
|
|
1072
1094
|
// 索引浏览当前展示条数(分页展示,避免一次渲染 500 条)
|
|
1073
1095
|
const [indexShown, setIndexShown] = react.useState(50);
|
|
1096
|
+
// 已补标(官方/聚合标记)到的索引位置:随「加载更多」增量推进,避免一次性发上千请求
|
|
1097
|
+
const [enrichedUpTo, setEnrichedUpTo] = react.useState(0);
|
|
1098
|
+
/** 合并一条补标结果(不动已有标记,避免 null 覆盖)。 */
|
|
1099
|
+
function mergeEnrichMark(e, b) {
|
|
1100
|
+
return {
|
|
1101
|
+
...e,
|
|
1102
|
+
official: e.official !== null ? e.official : (b?.official ?? null),
|
|
1103
|
+
aggregate: e.aggregate === true || b?.aggregate === true,
|
|
1104
|
+
aggregateInstallable: b?.aggregateInstallable === true,
|
|
1105
|
+
hasSkill: e.hasSkill === true || b?.hasSkill === true,
|
|
1106
|
+
hasSuite: e.hasSuite === true || b?.hasSuite === true,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
/** 服务端 /enrich 兜底:并发限流 12 + 24h 缓存,补齐客户端直连失败/遗漏的标记
|
|
1110
|
+
* (每次最多 30 条,按需分批;结果按位置合并回索引与当前展示切片)。 */
|
|
1111
|
+
function enrichRangeServer(from, slice) {
|
|
1112
|
+
const BATCH = 30;
|
|
1113
|
+
for (let i = 0; i < slice.length; i += BATCH) {
|
|
1114
|
+
const batch = slice.slice(i, i + BATCH);
|
|
1115
|
+
const start = from + i;
|
|
1116
|
+
call("/plugin-console/enrich", { items: batch }).then(
|
|
1117
|
+
(r) => {
|
|
1118
|
+
if (r === null || !Array.isArray(r.items)) return;
|
|
1119
|
+
const got = r.items;
|
|
1120
|
+
setIndexData((prev) => {
|
|
1121
|
+
if (prev === null || prev.loaded !== true) return prev;
|
|
1122
|
+
const items = [...prev.items];
|
|
1123
|
+
got.forEach((e, k) => {
|
|
1124
|
+
const idx = start + k;
|
|
1125
|
+
if (idx < items.length) items[idx] = mergeEnrichMark(e, items[idx]);
|
|
1126
|
+
});
|
|
1127
|
+
return { ...prev, items };
|
|
1128
|
+
});
|
|
1129
|
+
setMarket((prev) => {
|
|
1130
|
+
if (prev === null || prev.status !== "ready" || prev.fromIndex !== true) return prev;
|
|
1131
|
+
return {
|
|
1132
|
+
...prev,
|
|
1133
|
+
data: prev.data.map((it) => {
|
|
1134
|
+
const hit = got.find((e) => e.fullName === it.fullName);
|
|
1135
|
+
return hit ? mergeEnrichMark(hit, it) : it;
|
|
1136
|
+
}),
|
|
1137
|
+
};
|
|
1138
|
+
});
|
|
1139
|
+
},
|
|
1140
|
+
() => {},
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/** 增量补标索引区间 [from, to):首屏先补前 50 条,之后随「加载更多」继续往下补。 */
|
|
1145
|
+
function enrichRange(from, to, list) {
|
|
1146
|
+
const slice = (list ?? []).slice(from, to);
|
|
1147
|
+
if (slice.length === 0) return;
|
|
1148
|
+
enrichOfficialBundle(slice).then((enriched) => {
|
|
1149
|
+
if (!Array.isArray(enriched)) return;
|
|
1150
|
+
setIndexData((prev) => {
|
|
1151
|
+
if (prev === null || prev.loaded !== true) return prev;
|
|
1152
|
+
const items = [...prev.items];
|
|
1153
|
+
enriched.forEach((e, i) => {
|
|
1154
|
+
const idx = from + i;
|
|
1155
|
+
if (idx < items.length) items[idx] = mergeEnrichMark(e, items[idx]);
|
|
1156
|
+
});
|
|
1157
|
+
return { ...prev, items };
|
|
1158
|
+
});
|
|
1159
|
+
// 同步刷新当前展示的切片(引用变化才会重渲染)
|
|
1160
|
+
setMarket((prev) => {
|
|
1161
|
+
if (prev === null || prev.status !== "ready" || prev.fromIndex !== true) return prev;
|
|
1162
|
+
return {
|
|
1163
|
+
...prev,
|
|
1164
|
+
data: prev.data.map((it) => {
|
|
1165
|
+
const hit = enriched.find((e) => e.fullName === it.fullName);
|
|
1166
|
+
return hit ? mergeEnrichMark(hit, it) : it;
|
|
1167
|
+
}),
|
|
1168
|
+
};
|
|
1169
|
+
});
|
|
1170
|
+
setEnrichedUpTo((prev) => Math.max(prev, to));
|
|
1171
|
+
});
|
|
1172
|
+
// ② 服务端 /enrich 兜底:延后 1.5 秒执行(不抢首屏带宽),走 24h 缓存 + 并发限流通道
|
|
1173
|
+
if (typeof window.setTimeout === "function") {
|
|
1174
|
+
window.setTimeout(() => enrichRangeServer(from, slice), 1500);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1074
1177
|
// 市场总模式:'plugins' = 插件市场 / 'skills' = 技能市场(localStorage 持久化)
|
|
1075
1178
|
const [mode, setMode] = react.useState(() => {
|
|
1076
1179
|
try { return localStorage.getItem("pc-market-mode") === "skills" ? "skills" : "plugins"; } catch { return "plugins"; }
|
|
@@ -1435,6 +1538,8 @@ window.__ModuleLoader__.load({
|
|
|
1435
1538
|
const [sourceName, setSourceName] = react.useState("");
|
|
1436
1539
|
const [sourceUrl, setSourceUrl] = react.useState("");
|
|
1437
1540
|
const [sourcesBusy, setSourcesBusy] = react.useState(false);
|
|
1541
|
+
// 软件源扫描结果:null=未扫描 / {status:'scanning'} / {status:'ready', results, scannedAt}
|
|
1542
|
+
const [regScan, setRegScan] = react.useState(null);
|
|
1438
1543
|
// registry 行内编辑
|
|
1439
1544
|
const [editReg, setEditReg] = react.useState(null);
|
|
1440
1545
|
const [indexSourceName, setIndexSourceName] = react.useState("");
|
|
@@ -1509,24 +1614,14 @@ window.__ModuleLoader__.load({
|
|
|
1509
1614
|
cachedAt: typeof data.cachedAt === "number" ? data.cachedAt : null,
|
|
1510
1615
|
sourceName: typeof data.sourceName === "string" ? data.sourceName : null,
|
|
1511
1616
|
});
|
|
1512
|
-
//
|
|
1513
|
-
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
...e,
|
|
1521
|
-
official: e.official !== null ? e.official : (b.official ?? null),
|
|
1522
|
-
aggregate: e.aggregate === true || b.aggregate === true,
|
|
1523
|
-
aggregateInstallable: b.aggregateInstallable === true,
|
|
1524
|
-
hasSkill: e.hasSkill === true || b.hasSkill === true,
|
|
1525
|
-
hasSuite: e.hasSuite === true || b.hasSuite === true,
|
|
1526
|
-
};
|
|
1527
|
-
});
|
|
1528
|
-
setIndexData((prev) => (prev !== null && prev.loaded === true ? { ...prev, items: merged } : prev));
|
|
1529
|
-
});
|
|
1617
|
+
// ★「只看官方」需要官方标记:先补首屏 50 条,其余随「加载更多」增量补
|
|
1618
|
+
//(每条要发网络请求,全量 500 条会打满浏览器连接数,导致打开插件页明显变慢)
|
|
1619
|
+
const runEnrich = () => enrichRange(0, 50, data.items);
|
|
1620
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
1621
|
+
window.requestIdleCallback(() => runEnrich(), { timeout: 2000 });
|
|
1622
|
+
} else {
|
|
1623
|
+
window.setTimeout(runEnrich, 400);
|
|
1624
|
+
}
|
|
1530
1625
|
}
|
|
1531
1626
|
},
|
|
1532
1627
|
(error) => setMessage(t("marketIndexFailed") + ":" + (error?.message ?? t("unknown"))),
|
|
@@ -1871,6 +1966,8 @@ window.__ModuleLoader__.load({
|
|
|
1871
1966
|
const more = sourceData.slice(0, indexShown + next * 50);
|
|
1872
1967
|
setMarket({ status: "ready", data: more, direct: false, source: "github", fromIndex: true });
|
|
1873
1968
|
setIndexShown(indexShown + next * 50);
|
|
1969
|
+
// 新增的这段继续补「官方/聚合」标记(与首屏同样的增量策略)
|
|
1970
|
+
if (mode !== "skills") enrichRange(enrichedUpTo, indexShown + next * 50, indexData.items);
|
|
1874
1971
|
setLoadingMore(false);
|
|
1875
1972
|
return;
|
|
1876
1973
|
}
|
|
@@ -2159,6 +2256,29 @@ window.__ModuleLoader__.load({
|
|
|
2159
2256
|
}
|
|
2160
2257
|
sourcesAction({ action: "add", name: sourceName.trim(), url: sourceUrl.trim() }, () => { setSourceName(""); setSourceUrl(""); });
|
|
2161
2258
|
};
|
|
2259
|
+
/** 软件源扫描:服务端并发探测每个源(可达性/延迟/最新版本),结果显示在源列表行内。 */
|
|
2260
|
+
const scanRegistries = () => {
|
|
2261
|
+
setRegScan({ status: "scanning" });
|
|
2262
|
+
call("/plugin-console/registry-scan", {}).then(
|
|
2263
|
+
(data) => setRegScan({ status: "ready", results: Array.isArray(data.results) ? data.results : [], scannedAt: typeof data.scannedAt === "number" ? data.scannedAt : null }),
|
|
2264
|
+
(error) => { setRegScan(null); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
|
|
2265
|
+
);
|
|
2266
|
+
};
|
|
2267
|
+
// 扫描结果派生视图:每条源的命中结果 + 可达计数 + 最新版本来自哪个源
|
|
2268
|
+
const scanRows = regScan !== null && regScan.status === "ready" ? regScan.results : [];
|
|
2269
|
+
const scanOf = (id) => scanRows.find((row) => row.id === id) ?? null;
|
|
2270
|
+
const scanOkCount = scanRows.filter((row) => row.ok === true).length;
|
|
2271
|
+
const scanBest = (() => {
|
|
2272
|
+
const withVersion = scanRows.filter((row) => row.ok === true && typeof row.latest === "string");
|
|
2273
|
+
if (withVersion.length === 0) return null;
|
|
2274
|
+
const parse = (v) => String(v).split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
2275
|
+
const newer = (a, b) => {
|
|
2276
|
+
const [pa, pb] = [parse(a), parse(b)];
|
|
2277
|
+
for (let i = 0; i < 3; i += 1) if (pa[i] !== pb[i]) return pa[i] > pb[i];
|
|
2278
|
+
return false;
|
|
2279
|
+
};
|
|
2280
|
+
return withVersion.reduce((best, row) => (best === null || newer(row.latest, best.latest) ? row : best), null);
|
|
2281
|
+
})();
|
|
2162
2282
|
const saveEditReg = () => {
|
|
2163
2283
|
if (editReg === null) return;
|
|
2164
2284
|
sourcesAction({ action: "edit", id: editReg.id, name: editReg.name, url: editReg.url }, () => setEditReg(null));
|
|
@@ -3675,7 +3795,22 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
3675
3795
|
: el("div", { key: src.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
|
|
3676
3796
|
el("div", { className: styles.rowTop, style: { flex: 1, minWidth: 0 } },
|
|
3677
3797
|
el("strong", { className: styles.name, style: { flex: "none", maxWidth: "35%" } }, src.name + (src.primary ? "(" + t("sourcePrimary") + ")" : "")),
|
|
3678
|
-
el("code", { className: styles.srcUrl }, src.url)
|
|
3798
|
+
el("code", { className: styles.srcUrl }, src.url),
|
|
3799
|
+
(() => {
|
|
3800
|
+
const hit = scanOf(src.id);
|
|
3801
|
+
if (hit === null) return null;
|
|
3802
|
+
const title = hit.ok === true
|
|
3803
|
+
? hit.url + " · " + hit.ms + "ms" + (hit.versions !== null ? " · " + hit.versions + " 个版本" : "") + (hit.latest === null ? " · " + t("registryScanNoVersion") : "")
|
|
3804
|
+
: hit.url + " · " + (hit.error ?? t("registryScanFail")) + " · " + hit.ms + "ms";
|
|
3805
|
+
return el("span", {
|
|
3806
|
+
className: styles.tag,
|
|
3807
|
+
"data-enabled": hit.ok === true ? "true" : undefined,
|
|
3808
|
+
"data-bad": hit.ok === true ? undefined : "true",
|
|
3809
|
+
title,
|
|
3810
|
+
}, hit.ok === true
|
|
3811
|
+
? "✓ " + hit.ms + "ms" + (hit.latest !== null ? " · v" + hit.latest : " · " + t("registryScanNoVersion"))
|
|
3812
|
+
: "✗ " + (hit.error ?? t("registryScanFail")));
|
|
3813
|
+
})()),
|
|
3679
3814
|
el("div", { className: styles.rowTop },
|
|
3680
3815
|
src.primary ? null : el("button", { type: "button", className: styles.toggle, title: t("setPrimaryHint"), disabled: sourcesBusy, onClick: () => sourcesAction({ action: "set-primary", id: src.id }) }, t("setPrimary")),
|
|
3681
3816
|
el("button", { type: "button", className: styles.toggle, title: t("editSourceHint"), disabled: sourcesBusy, onClick: () => setEditReg({ id: src.id, name: src.name, url: src.url }) }, t("editSource"))))),
|
|
@@ -3683,6 +3818,20 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
3683
3818
|
el("input", { type: "text", placeholder: t("sourceName"), value: sourceName, onChange: (event) => setSourceName(event.currentTarget.value), style: { flex: 1, minWidth: 0 } }),
|
|
3684
3819
|
el("input", { type: "text", placeholder: t("sourceUrl"), value: sourceUrl, onChange: (event) => setSourceUrl(event.currentTarget.value), style: { flex: 2, minWidth: 0 } }),
|
|
3685
3820
|
el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: addSource }, t("addSource"))),
|
|
3821
|
+
el("div", { className: styles.rowTop },
|
|
3822
|
+
el("button", {
|
|
3823
|
+
type: "button",
|
|
3824
|
+
className: styles.toggle,
|
|
3825
|
+
title: t("registryScanHint"),
|
|
3826
|
+
disabled: regScan !== null && regScan.status === "scanning",
|
|
3827
|
+
onClick: scanRegistries,
|
|
3828
|
+
}, regScan !== null && regScan.status === "scanning" ? t("registryScanning") : t("registryScan")),
|
|
3829
|
+
regScan !== null && regScan.status === "scanning" ? el("span", { className: styles.spinner }) : null,
|
|
3830
|
+
regScan !== null && regScan.status === "ready"
|
|
3831
|
+
? el("span", { className: styles.message },
|
|
3832
|
+
t("registryScanSummary").replace("{ok}", String(scanOkCount)).replace("{total}", String(scanRows.length))
|
|
3833
|
+
+ (scanBest !== null ? " · " + t("registryScanBest").replace("{version}", scanBest.latest).replace("{name}", scanBest.name) : ""))
|
|
3834
|
+
: null),
|
|
3686
3835
|
]),
|
|
3687
3836
|
accSection("index", t("indexSourcesTitle"), [
|
|
3688
3837
|
el("p", { className: styles.message }, t("indexSourcesDesc")),
|
package/lib/index.js
CHANGED
|
@@ -1800,23 +1800,29 @@ async function enrichItemOne(item) {
|
|
|
1800
1800
|
const pkg = JSON.parse(pkgResult.value)
|
|
1801
1801
|
if (typeof pkg.dsh?.bundle?.patch === 'string') {
|
|
1802
1802
|
official = true
|
|
1803
|
-
} else
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const
|
|
1813
|
-
.
|
|
1814
|
-
.
|
|
1815
|
-
|
|
1816
|
-
|
|
1803
|
+
} else {
|
|
1804
|
+
// 成功读到 package.json 且没有 dsh.bundle.patch → 确定「非官方」。
|
|
1805
|
+
// 必须落成 false(而不是留 null),否则缓存条件 official !== null 永不成立,
|
|
1806
|
+
// 每个非官方插件每次打开都重新请求 → /enrich 缓存命中也要十几秒。
|
|
1807
|
+
official = false
|
|
1808
|
+
if (pkg.private === true && (Array.isArray(pkg.workspaces) || /(^|-)dsh[-/]/u.test(String(pkg.name ?? '')))) {
|
|
1809
|
+
aggregate = true
|
|
1810
|
+
try {
|
|
1811
|
+
const tree = await curlJson(`https://api.github.com/repos/${item.fullName}/git/trees/${encodeURIComponent(branch)}?recursive=1`, 6000)
|
|
1812
|
+
const pkgPaths = (tree.tree ?? [])
|
|
1813
|
+
.filter((n) => n.type === 'blob' && /^packages\/[^/]+\/package\.json$/u.test(n.path))
|
|
1814
|
+
.map((n) => n.path)
|
|
1815
|
+
.slice(0, 12)
|
|
1816
|
+
if (pkgPaths.length > 0) {
|
|
1817
|
+
const subs = await Promise.all(pkgPaths.map((p) => curlText(`https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/${p}`, 4000)
|
|
1818
|
+
.then((t) => { try { return JSON.parse(t) } catch { return null } })
|
|
1819
|
+
.catch(() => null)))
|
|
1820
|
+
if (subs.some((sp) => sp && typeof sp.dsh?.bundle?.patch === 'string')) {
|
|
1821
|
+
aggregateInstallable = true
|
|
1822
|
+
}
|
|
1817
1823
|
}
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1824
|
+
} catch {}
|
|
1825
|
+
}
|
|
1820
1826
|
}
|
|
1821
1827
|
}
|
|
1822
1828
|
if (skillResult.status === 'fulfilled' && skillResult.value?.hasSkill === true) {
|
|
@@ -1845,9 +1851,13 @@ async function enrichItems(items) {
|
|
|
1845
1851
|
const key = `${item.fullName}@${item.defaultBranch ?? 'main'}`
|
|
1846
1852
|
const cached = cache[key]
|
|
1847
1853
|
try {
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1854
|
+
// official 已判定(true/false)→ 24h 缓存;判定失败(null)→ 1h 短缓存,减少无谓重试
|
|
1855
|
+
if (cached !== undefined && typeof cached?.at === 'number' && cached.data) {
|
|
1856
|
+
const ttl = cached.data.official === null ? 60 * 60 * 1000 : ENRICH_CACHE_TTL
|
|
1857
|
+
if (Date.now() - cached.at < ttl) {
|
|
1858
|
+
out[i] = { ...item, ...cached.data }
|
|
1859
|
+
continue
|
|
1860
|
+
}
|
|
1851
1861
|
}
|
|
1852
1862
|
let result = await enrichItemOne(item)
|
|
1853
1863
|
// 本次失败 → 回退缓存(哪怕已过期),避免「看天吃饭」
|
|
@@ -1859,7 +1869,16 @@ async function enrichItems(items) {
|
|
|
1859
1869
|
}
|
|
1860
1870
|
out[i] = result
|
|
1861
1871
|
} catch {
|
|
1862
|
-
|
|
1872
|
+
// 抛错的条目也必须写缓存(null 结果 + 1h 短 TTL),否则每次打开都重试同一批,
|
|
1873
|
+
// /enrich 即使「命中缓存」也要等十几秒。
|
|
1874
|
+
if (cached?.data) {
|
|
1875
|
+
out[i] = { ...item, ...cached.data }
|
|
1876
|
+
} else {
|
|
1877
|
+
const miss = { official: null, aggregate: false, aggregateInstallable: false, hasSkill: false, hasSuite: false }
|
|
1878
|
+
cache[key] = { at: Date.now(), data: miss }
|
|
1879
|
+
cacheDirty = true
|
|
1880
|
+
out[i] = { ...item, ...miss }
|
|
1881
|
+
}
|
|
1863
1882
|
}
|
|
1864
1883
|
}
|
|
1865
1884
|
}
|
|
@@ -3778,6 +3797,99 @@ function checkpointFrameworkTree(fwRoot, destRoot) {
|
|
|
3778
3797
|
return { dest, mirrored }
|
|
3779
3798
|
}
|
|
3780
3799
|
|
|
3800
|
+
/**
|
|
3801
|
+
* 预设/组合文件的配置迁移门禁(2026-09-10 事故):
|
|
3802
|
+
* 框架升级会改「插件配置 schema」——0.1.5 的 @deepseek-ai/dsh-persona 把 text 改名为 prefix(必填)。
|
|
3803
|
+
* 适配门只扫「已装插件包源码」,**预设不在其中**(~/.dsh/.agent-presets/<name>/agent.cordis.yml),
|
|
3804
|
+
* 于是升级后预设挂载失败 → 服务/会话起不来(这次就是这么挂的)。
|
|
3805
|
+
* 升级前按目标版本扫描并自动迁移(写 .bak 备份),返回迁移清单供步骤展示。
|
|
3806
|
+
*/
|
|
3807
|
+
const PRESET_CONFIG_MIGRATIONS = [
|
|
3808
|
+
{ pkg: '@deepseek-ai/dsh-persona', from: 'text', to: 'prefix', since: '0.1.5' },
|
|
3809
|
+
]
|
|
3810
|
+
|
|
3811
|
+
/** 待扫描的 agent 配置文件:所有预设 + profile 的 host 组合。 */
|
|
3812
|
+
function collectAgentConfigFiles(profileDir) {
|
|
3813
|
+
const files = []
|
|
3814
|
+
const presetRoot = join(dshHome(), '.agent-presets')
|
|
3815
|
+
try {
|
|
3816
|
+
for (const entry of readdirSync(presetRoot, { withFileTypes: true })) {
|
|
3817
|
+
if (!entry.isDirectory()) continue
|
|
3818
|
+
for (const name of ['agent.cordis.yml', 'agent.cordis.yaml']) {
|
|
3819
|
+
const f = join(presetRoot, entry.name, name)
|
|
3820
|
+
if (existsSync(f)) files.push(f)
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
} catch {}
|
|
3824
|
+
for (const name of ['cordis.yml', 'cordis.yaml', 'cordis.patch.yml']) {
|
|
3825
|
+
const f = join(profileDir, name)
|
|
3826
|
+
if (existsSync(f)) files.push(f)
|
|
3827
|
+
}
|
|
3828
|
+
return files
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
/** 迁移一个文件里 persona 行 config 的旧键(仅当尚无新键时改写,先写 .bak)。 */
|
|
3832
|
+
function migratePresetPersona(file, mig) {
|
|
3833
|
+
let text = null
|
|
3834
|
+
try { text = readFileSync(file, 'utf8') } catch { return null }
|
|
3835
|
+
const lines = text.split(/\r?\n/u)
|
|
3836
|
+
let rowAt = -1
|
|
3837
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3838
|
+
if (new RegExp(`name:\\s*['"]?${escapeRegExp(mig.pkg)}['"]?\\s*$`, 'u').test(lines[i])) { rowAt = i; break }
|
|
3839
|
+
}
|
|
3840
|
+
if (rowAt === -1) return null
|
|
3841
|
+
let cfgAt = -1
|
|
3842
|
+
for (let i = rowAt + 1; i < lines.length; i += 1) {
|
|
3843
|
+
if (/^\s*config:\s*$/u.test(lines[i])) { cfgAt = i; break }
|
|
3844
|
+
if (/^\s*-\s+id:/u.test(lines[i])) break
|
|
3845
|
+
}
|
|
3846
|
+
if (cfgAt === -1) return null
|
|
3847
|
+
const cfgIndent = lines[cfgAt].match(/^\s*/u)[0].length
|
|
3848
|
+
let endAt = lines.length
|
|
3849
|
+
for (let i = cfgAt + 1; i < lines.length; i += 1) {
|
|
3850
|
+
const line = lines[i]
|
|
3851
|
+
if (line.trim() === '' || /^\s*#/u.test(line)) continue
|
|
3852
|
+
if (line.match(/^\s*/u)[0].length <= cfgIndent) { endAt = i; break }
|
|
3853
|
+
}
|
|
3854
|
+
let hasNew = false
|
|
3855
|
+
let hit = -1
|
|
3856
|
+
for (let i = cfgAt + 1; i < endAt; i += 1) {
|
|
3857
|
+
if (new RegExp(`^\\s*${mig.to}:`, 'u').test(lines[i])) { hasNew = true; break }
|
|
3858
|
+
if (new RegExp(`^\\s*${mig.from}:`, 'u').test(lines[i])) hit = i
|
|
3859
|
+
}
|
|
3860
|
+
if (hasNew || hit === -1) return null
|
|
3861
|
+
lines[hit] = lines[hit].replace(new RegExp(`^(\\s*)${mig.from}:`, 'u'), `$1${mig.to}:`)
|
|
3862
|
+
try {
|
|
3863
|
+
copyFileSync(file, `${file}.bak-${Date.now()}`)
|
|
3864
|
+
writeFileSync(file, lines.join('\n'), 'utf8')
|
|
3865
|
+
} catch { return null }
|
|
3866
|
+
return { file, from: mig.from, to: mig.to, line: hit + 1, pkg: mig.pkg }
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
/** 目标版本是否 ≥ since(只比 major.minor.patch,忽略 rc 段——0.1.5-rc.1 也算已达 0.1.5 变更)。 */
|
|
3870
|
+
function isVersionAtLeast(target, since) {
|
|
3871
|
+
const t = parseFrameworkVersion(target)
|
|
3872
|
+
const s = parseFrameworkVersion(since)
|
|
3873
|
+
if (t === -1 || s === -1) return false
|
|
3874
|
+
if (t.maj !== s.maj) return t.maj > s.maj
|
|
3875
|
+
if (t.min !== s.min) return t.min > s.min
|
|
3876
|
+
return t.pat >= s.pat
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
/** 升级前扫描预设/组合文件并按目标版本迁移(返回迁移清单)。导出供测试直接调用。 */
|
|
3880
|
+
export function migrateAgentConfigsForUpgrade(profileDir, targetVersion) {
|
|
3881
|
+
const out = []
|
|
3882
|
+
if (typeof targetVersion !== 'string' || targetVersion === '') return out
|
|
3883
|
+
for (const mig of PRESET_CONFIG_MIGRATIONS) {
|
|
3884
|
+
if (!isVersionAtLeast(targetVersion, mig.since)) continue
|
|
3885
|
+
for (const file of collectAgentConfigFiles(profileDir)) {
|
|
3886
|
+
const done = migratePresetPersona(file, mig)
|
|
3887
|
+
if (done !== null) out.push(done)
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
return out
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3781
3893
|
/** 移除补丁中的单行 disabled/forced 覆盖块(兼容门解锁用)。 */
|
|
3782
3894
|
function removeDisableBlock(patchPath, rowId) {
|
|
3783
3895
|
return queuedWrite(async () => {
|
|
@@ -4880,6 +4992,39 @@ async function handle(ctx, req, res) {
|
|
|
4880
4992
|
return
|
|
4881
4993
|
}
|
|
4882
4994
|
|
|
4995
|
+
// 软件源扫描:并发探测每个 npm 源的「可达性 / 延迟 / 该源上的最新版本」。
|
|
4996
|
+
// 用于判断主源是否最优(内网私服 vs 公共镜像),结果直接在前端软件源列表里显示。
|
|
4997
|
+
if (pathname === `${ROUTE_PREFIX}/registry-scan`) {
|
|
4998
|
+
const sources = readSources()
|
|
4999
|
+
const scanPkg = '@noob-stupid/dsh-plugin-console'
|
|
5000
|
+
const encoded = encodeURIComponent(scanPkg)
|
|
5001
|
+
const scanOne = async (r) => {
|
|
5002
|
+
const started = Date.now()
|
|
5003
|
+
const base = { id: r.id, name: r.name, url: r.url, primary: r.primary === true }
|
|
5004
|
+
try {
|
|
5005
|
+
const response = await fetch(`${String(r.url).replace(/\/+$/u, '')}/${encoded}`, {
|
|
5006
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
5007
|
+
signal: AbortSignal.timeout(8000),
|
|
5008
|
+
})
|
|
5009
|
+
const ms = Date.now() - started
|
|
5010
|
+
if (!response.ok) return { ...base, ok: false, ms, status: response.status, latest: null, versions: null, error: `HTTP ${response.status}` }
|
|
5011
|
+
const data = await response.json()
|
|
5012
|
+
const latest = typeof data?.['dist-tags']?.latest === 'string' ? data['dist-tags'].latest : null
|
|
5013
|
+
const versions = data?.versions !== undefined && data.versions !== null && typeof data.versions === 'object' ? Object.keys(data.versions).length : null
|
|
5014
|
+
return { ...base, ok: true, ms, status: response.status, latest, versions, error: null }
|
|
5015
|
+
} catch (error) {
|
|
5016
|
+
const ms = Date.now() - started
|
|
5017
|
+
const message = error?.name === 'TimeoutError'
|
|
5018
|
+
? '超时(8s)'
|
|
5019
|
+
: (error instanceof Error ? error.message : String(error))
|
|
5020
|
+
return { ...base, ok: false, ms, status: null, latest: null, versions: null, error: message }
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
const results = await Promise.all(sources.registries.map((r) => scanOne(r)))
|
|
5024
|
+
sendJson(res, 200, { ok: true, pkg: scanPkg, scannedAt: Date.now(), results })
|
|
5025
|
+
return
|
|
5026
|
+
}
|
|
5027
|
+
|
|
4883
5028
|
if (pathname === `${ROUTE_PREFIX}/sources`) {
|
|
4884
5029
|
const { action } = body
|
|
4885
5030
|
const sources = readSources()
|
|
@@ -5536,6 +5681,23 @@ async function handle(ctx, req, res) {
|
|
|
5536
5681
|
const hasUpdate = target !== null
|
|
5537
5682
|
const steps = []
|
|
5538
5683
|
let upgraded = false
|
|
5684
|
+
// 1.5) 预设/config 迁移门禁(写在生成升级脚本之前):框架新版不再接受的旧字段
|
|
5685
|
+
//(如 0.1.5 的 persona.text → prefix)会让升级后**预设挂载失败、服务起不来**——
|
|
5686
|
+
// 适配门只扫插件包,扫不到预设;这里就地改名并留 .bak,避免整次升级被一个字段搞死。
|
|
5687
|
+
let configMigrations = []
|
|
5688
|
+
if (hasUpdate) {
|
|
5689
|
+
try {
|
|
5690
|
+
configMigrations = migrateAgentConfigsForUpgrade(profileDir, target)
|
|
5691
|
+
} catch (error) {
|
|
5692
|
+
steps.push(`预设配置迁移检查失败:${error instanceof Error ? error.message : String(error)}`)
|
|
5693
|
+
}
|
|
5694
|
+
for (const m of configMigrations) {
|
|
5695
|
+
steps.push(`预设字段迁移:${m.file} 第 ${m.line} 行 ${m.from} → ${m.to}(${m.pkg},已留 .bak 备份)`)
|
|
5696
|
+
}
|
|
5697
|
+
if (configMigrations.length > 0) {
|
|
5698
|
+
steps.push(`自动迁移了 ${configMigrations.length} 处框架新版不再接受的预设字段(不迁移会让升级后预设挂载失败、服务拉不起来)`)
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5539
5701
|
// 2) 自动升级框架:运行中的服务锁着 dsh/lib 目录(EBUSY 无法原地替换),
|
|
5540
5702
|
// 因此生成独立升级脚本(kill 服务 → npm 升级 → 失败回滚 → 拉起服务),后台执行。
|
|
5541
5703
|
// 与 /restart 同款自守护模式:端口无监听会自动拉起,不会留下"服务起不来"。
|
|
@@ -5552,7 +5714,7 @@ async function handle(ctx, req, res) {
|
|
|
5552
5714
|
// 避免"kill 后升级/拉起双双失败,服务永久 down"
|
|
5553
5715
|
if (!existsSync(corepackJs) || binPath === null || !existsSync(binPath)) {
|
|
5554
5716
|
steps.push(`升级已取消:无法定位 ${!existsSync(corepackJs) ? 'corepack(pnpm 通道)' : 'dsh bin.js'}(框架安装可能不完整),服务保持运行;请先修复框架再升级`)
|
|
5555
|
-
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded: false, backupDir, steps, hints: steps })
|
|
5717
|
+
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded: false, backupDir, configMigrations, steps, hints: steps })
|
|
5556
5718
|
return
|
|
5557
5719
|
}
|
|
5558
5720
|
const profileDir2 = profileDir
|
|
@@ -5607,7 +5769,13 @@ async function handle(ctx, req, res) {
|
|
|
5607
5769
|
try { mkdirSync(dirname(logFile), { recursive: true }) } catch {}
|
|
5608
5770
|
// PowerShell 字符串里反斜杠是字面量(无 \\ 转义):JSON.stringify 产生的 \\ 必须还原为 \,
|
|
5609
5771
|
// 否则所有路径无效(npm 调不起来、回滚无效、拉起失败——曾导致升级脚本空跑)
|
|
5610
|
-
|
|
5772
|
+
// PowerShell 双引号字面量:反斜杠还原 + 空串产出 ''(JSON 的 "" 被单引号包裹会变成字面量 "" ,
|
|
5773
|
+
// 曾导致 $cp = '""' → Test-Path 恒假 → 全树回滚被静默跳过)+ $ / 反引号转义(路径含 $ 不被插值)
|
|
5774
|
+
const ps = (s) => {
|
|
5775
|
+
const j = JSON.stringify(String(s)).replace(/\\\\/gu, '\\')
|
|
5776
|
+
if (j === '""') return "''"
|
|
5777
|
+
return j.replace(/`/gu, '``').replace(/\$/gu, '`$')
|
|
5778
|
+
}
|
|
5611
5779
|
const lines = [
|
|
5612
5780
|
`$state = ${ps(stateFile)}`,
|
|
5613
5781
|
`$log = ${ps(logFile)}`,
|
|
@@ -5617,8 +5785,9 @@ async function handle(ctx, req, res) {
|
|
|
5617
5785
|
// trap 捕获后:写 failed 状态 + 日志 + 尝试拉起服务 + 自删任务 + 退出
|
|
5618
5786
|
"trap {",
|
|
5619
5787
|
" try { SetState 'failed' ('升级脚本异常终止:' + $_.Exception.Message) } catch {}",
|
|
5620
|
-
|
|
5621
|
-
|
|
5788
|
+
// 事故教训(2026-09-10):只记消息无法定位是哪一行崩的 —— 连出错位置一起写日志
|
|
5789
|
+
" try { Log ('升级脚本异常终止:' + $_.Exception.Message + ' @ ' + $_.InvocationInfo.PositionMessage) } catch {}",
|
|
5790
|
+
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if (-not $c) { $bt = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $bt = (& ${ps(nodePath)} -e "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))" 2>$null | Select-Object -Last 1) } catch {}; if ($bt -ne '') { $launchLog = Join-Path (Split-Path $log) 'fw-relaunch.log'; try { Add-Content -Path $launchLog -Value ((Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' 拉起(异常兜底): ' + $bt) -Encoding UTF8 } catch {}; Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', ('"' + ${ps(nodePath)} + '" "' + $bt + '" web >> "' + $launchLog + '" 2>&1')) -WindowStyle Hidden } } } catch {}`,
|
|
5622
5791
|
` schtasks /delete /f /tn ${taskName} 2>$null`,
|
|
5623
5792
|
" exit 1",
|
|
5624
5793
|
"}",
|
|
@@ -5687,9 +5856,12 @@ async function handle(ctx, req, res) {
|
|
|
5687
5856
|
` }`,
|
|
5688
5857
|
` if ($code -eq 0) {`,
|
|
5689
5858
|
` try {`,
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5859
|
+
// 事故教训(2026-09-10):版本校验必须看「启动器可见路径」(顶层 @deepseek-ai\dsh),
|
|
5860
|
+
// 而不是 require.resolve 得到的 .pnpm 内部路径——旧逻辑查 .pnpm 里的旧副本,
|
|
5861
|
+
// 于是 pnpm 明明装好了却报「版本未更新,按失败处理」,白等两轮。
|
|
5862
|
+
` $v = (Get-Content ${ps(join(fwRoot, '@deepseek-ai', 'dsh', 'package.json'))} -Raw | ConvertFrom-Json).version`,
|
|
5863
|
+
` if ($v -ne '${target}') { Log ('pnpm 退出码 0 但顶层可见版本未更新(顶层 ' + $v + ',目标 ${target}),按失败处理'); $code = 1 }`,
|
|
5864
|
+
` } catch { Log 'pnpm 退出码 0 但无法读取顶层版本,按失败处理'; $code = 1 }`,
|
|
5693
5865
|
` }`,
|
|
5694
5866
|
` return $code`,
|
|
5695
5867
|
`}`,
|
|
@@ -5700,29 +5872,39 @@ async function handle(ctx, req, res) {
|
|
|
5700
5872
|
`function Invoke-Rollback {`,
|
|
5701
5873
|
` SetState 'rollback' '升级失败,回滚框架(全树/CLI 备份)…'`,
|
|
5702
5874
|
` Log '升级失败,回滚框架…'`,
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
`
|
|
5707
|
-
` $
|
|
5708
|
-
`
|
|
5709
|
-
`
|
|
5710
|
-
` $
|
|
5711
|
-
` $
|
|
5712
|
-
`
|
|
5875
|
+
// 事故教训(2026-09-10):整个回滚体包 try/catch —— 脚本崩在回滚里(曾报
|
|
5876
|
+
// 「无法将参数绑定到参数 Path」,回滚没跑完、旧树半新半旧),必须记录出错位置而不是
|
|
5877
|
+
// 只留一句 trap 消息;回滚失败也要明确写状态,别让人以为已回滚。
|
|
5878
|
+
` try {`,
|
|
5879
|
+
` try { $svc = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($svc) { $svc | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }; Start-Sleep -Seconds 3 } } catch {}`,
|
|
5880
|
+
` $cp = ${ps(fwCheckpoint?.dest ?? '')}`,
|
|
5881
|
+
` if ($cp -and (Test-Path (Join-Path $cp '.pnpm'))) {`,
|
|
5882
|
+
` $restored = 0`,
|
|
5883
|
+
` $entries = Get-ChildItem -Path (Join-Path $cp '.pnpm') -Directory -ErrorAction SilentlyContinue`,
|
|
5884
|
+
` foreach ($e in $entries) {`,
|
|
5885
|
+
` if ($e.Name -notlike '*+*') { continue }`,
|
|
5886
|
+
` $name = @($e.Name -split '\\+')[1] -replace '@.*$', ''`,
|
|
5887
|
+
` if (-not $name) { continue }`,
|
|
5888
|
+
` $src = Join-Path $e.FullName ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
5889
|
+
` $dst = Join-Path (Join-Path ${ps(fwRoot)} ('.pnpm\\' + $e.Name)) ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
5890
|
+
` if (Test-Path (Join-Path $src 'package.json')) { New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null; robocopy $src $dst /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null; $restored++ }`,
|
|
5891
|
+
` }`,
|
|
5892
|
+
` $topSrc = Join-Path $cp 'top-@deepseek-ai'`,
|
|
5893
|
+
` if (Test-Path $topSrc) { Remove-Item ${ps(join(fwRoot, '@deepseek-ai'))} -Recurse -Force -ErrorAction SilentlyContinue; robocopy $topSrc ${ps(join(fwRoot, '@deepseek-ai'))} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null }`,
|
|
5894
|
+
` try { Copy-Item (Join-Path $cp 'lock.yaml') ${ps(join(fwRoot, '.pnpm', 'lock.yaml'))} -Force -ErrorAction SilentlyContinue } catch {}`,
|
|
5895
|
+
` Log ('全树回滚完成:恢复 ' + $restored + ' 个版本包 + 顶层 scope')`,
|
|
5896
|
+
` $script:rolledBack = $true`,
|
|
5897
|
+
` return`,
|
|
5713
5898
|
` }`,
|
|
5714
|
-
`
|
|
5715
|
-
` if (Test-Path $
|
|
5716
|
-
`
|
|
5717
|
-
`
|
|
5718
|
-
` $script:rolledBack = $true`,
|
|
5719
|
-
`
|
|
5899
|
+
` # CLI 兜底(checkpoint 缺失或损坏)`,
|
|
5900
|
+
` if (-not (Test-Path ${ps(join(rollbackDir, 'lib', 'bin.js'))})) { Log ('回滚失败:CLI 备份也缺失(' + ${ps(rollbackDir)} + ')'); return }`,
|
|
5901
|
+
` Remove-Item ${ps(dshDir)} -Recurse -Force -ErrorAction SilentlyContinue`,
|
|
5902
|
+
` robocopy ${ps(rollbackDir)} ${ps(dshDir)} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null`,
|
|
5903
|
+
` if (Test-Path ${ps(join(dshDir, 'lib', 'bin.js'))}) { Log '回滚完成(CLI 恢复)'; $script:rolledBack = $true } else { Log '回滚失败:复制失败,请手动修复框架安装' }`,
|
|
5904
|
+
` } catch {`,
|
|
5905
|
+
` Log ('回滚过程异常:' + $_.Exception.Message + ' @ ' + $_.InvocationInfo.PositionMessage)`,
|
|
5906
|
+
` SetState 'failed' ('回滚过程异常,框架可能处于混合状态:' + $_.Exception.Message)`,
|
|
5720
5907
|
` }`,
|
|
5721
|
-
` # CLI 兜底(checkpoint 缺失或损坏)`,
|
|
5722
|
-
` if (-not (Test-Path ${ps(join(rollbackDir, 'lib', 'bin.js'))})) { Log ('回滚失败:CLI 备份也缺失(' + ${ps(rollbackDir)} + ')'); return }`,
|
|
5723
|
-
` Remove-Item ${ps(dshDir)} -Recurse -Force -ErrorAction SilentlyContinue`,
|
|
5724
|
-
` robocopy ${ps(rollbackDir)} ${ps(dshDir)} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null`,
|
|
5725
|
-
` if (Test-Path ${ps(join(dshDir, 'lib', 'bin.js'))}) { Log '回滚完成(CLI 恢复)'; $script:rolledBack = $true } else { Log '回滚失败:复制失败,请手动修复框架安装' }`,
|
|
5726
5908
|
`}`,
|
|
5727
5909
|
`$script:rolledBack = $false`,
|
|
5728
5910
|
`$code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
@@ -5735,7 +5917,28 @@ async function handle(ctx, req, res) {
|
|
|
5735
5917
|
` $code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
5736
5918
|
` if ($code -ne 0) { Log ('(停服后)npmmirror 安装失败(exit=' + $code + '),切换 registry.npmjs.org 重试一次'); $code = Install-Framework 'https://registry.npmjs.org' }`,
|
|
5737
5919
|
`}`,
|
|
5738
|
-
`if ($code -ne 0) { Invoke-Rollback } else {
|
|
5920
|
+
`if ($code -ne 0) { Invoke-Rollback } else {`,
|
|
5921
|
+
` Log '框架本体升级完成'`,
|
|
5922
|
+
// 事故教训(2026-09-10):启动器(桌面端/`npx dsh`)用的是**顶层** @deepseek-ai\dsh;
|
|
5923
|
+
// 若它是 npm 时代的真实目录,pnpm 无法把它换成链接 → 顶层永远停在旧版,
|
|
5924
|
+
// 桌面端下次自己拉服务就跑旧框架(版本错配 → 拉起失败 → 又提示升级,循环)。
|
|
5925
|
+
// 这里校验「启动器可见版本」,是实体目录就改名备份后重装一次,让 pnpm 重建链接。
|
|
5926
|
+
` $visible = ${ps(join(fwRoot, '@deepseek-ai', 'dsh'))}`,
|
|
5927
|
+
` $vv = ''`,
|
|
5928
|
+
` try { $vv = (Get-Content (Join-Path $visible 'package.json') -Raw | ConvertFrom-Json).version } catch {}`,
|
|
5929
|
+
` if ($vv -ne '${target}') {`,
|
|
5930
|
+
` $item = Get-Item $visible -Force -ErrorAction SilentlyContinue`,
|
|
5931
|
+
` if ($item -and -not $item.LinkType) {`,
|
|
5932
|
+
` $bakName = 'dsh.npm-backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss')`,
|
|
5933
|
+
` try { Rename-Item -Path $visible -NewName $bakName -ErrorAction Stop; Log ('顶层为旧版实体目录(' + $vv + ')→ 已改名备份 ' + $bakName + ',重装以重建链接…') } catch { Log ('顶层实体目录改名失败:' + $_.Exception.Message) }`,
|
|
5934
|
+
` $code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
5935
|
+
` if ($code -ne 0) { $code = Install-Framework 'https://registry.npmjs.org' }`,
|
|
5936
|
+
` $vv = ''`,
|
|
5937
|
+
` try { $vv = (Get-Content (Join-Path $visible 'package.json') -Raw | ConvertFrom-Json).version } catch {}`,
|
|
5938
|
+
` if ($vv -eq '${target}') { Log '顶层可见版本已修正为 ${target}' } else { Log ('顶层可见版本仍为 ' + $vv + '(拉起不受影响:Node 按链接真实路径解析依赖)') }`,
|
|
5939
|
+
` } else { Log ('顶层可见版本异常(' + $vv + ')且非实体目录,跳过改名(LinkType=' + $item.LinkType + ')') }`,
|
|
5940
|
+
` } else { Log '顶层可见版本校验通过' }`,
|
|
5941
|
+
`}`,
|
|
5739
5942
|
// 回滚后跳过重链/依赖修复/版本对齐(它们按新树预期运行,会对已恢复的旧树造成二次破坏)
|
|
5740
5943
|
`if (-not $script:rolledBack) {`,
|
|
5741
5944
|
// 重新链接框架配套包(事故教训):pnpm 升级到新版本时 .pnpm 目录重建,
|
|
@@ -5875,7 +6078,7 @@ async function handle(ctx, req, res) {
|
|
|
5875
6078
|
'$started = $false',
|
|
5876
6079
|
`for ($i = 0; $i -lt 20; $i++) {`,
|
|
5877
6080
|
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
5878
|
-
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { Start-Process -FilePath ${ps(nodePath)}
|
|
6081
|
+
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { $launchLog = Join-Path (Split-Path $log) 'fw-relaunch.log'; try { Add-Content -Path $launchLog -Value ((Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' 拉起(升级后): ' + $binNow) -Encoding UTF8 } catch {}; Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', ('"' + ${ps(nodePath)} + '" "' + $binNow + '" web >> "' + $launchLog + '" 2>&1')) -WindowStyle Hidden; $started = $true; Log '已发起拉起服务(输出见 fw-relaunch.log)' } else { Log 'bin.js 缺失,跳过拉起' } }`,
|
|
5879
6082
|
' Start-Sleep -Seconds 5',
|
|
5880
6083
|
'}',
|
|
5881
6084
|
// 拉起失败不再只留提示(2026-09-04 教训):自动全树回滚并再拉起一次
|
|
@@ -5886,7 +6089,7 @@ async function handle(ctx, req, res) {
|
|
|
5886
6089
|
` $started = $false`,
|
|
5887
6090
|
` for ($i = 0; $i -lt 20; $i++) {`,
|
|
5888
6091
|
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
5889
|
-
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { Start-Process -FilePath ${ps(nodePath)}
|
|
6092
|
+
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { $launchLog = Join-Path (Split-Path $log) 'fw-relaunch.log'; try { Add-Content -Path $launchLog -Value ((Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' 拉起(回滚后): ' + $binNow) -Encoding UTF8 } catch {}; Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', ('"' + ${ps(nodePath)} + '" "' + $binNow + '" web >> "' + $launchLog + '" 2>&1')) -WindowStyle Hidden; $started = $true; Log '已发起拉起服务(回滚后,输出见 fw-relaunch.log)' } else { Log 'bin.js 缺失,跳过拉起' } }`,
|
|
5890
6093
|
` Start-Sleep -Seconds 5`,
|
|
5891
6094
|
` }`,
|
|
5892
6095
|
`}`,
|
|
@@ -5949,7 +6152,7 @@ async function handle(ctx, req, res) {
|
|
|
5949
6152
|
'升级完成后请重启 DSH 服务生效——重启后 Hub 自动:备份新版本配置快照、重打框架补丁(issue #5 容错)、给出适配提示。',
|
|
5950
6153
|
'官方配套组件(@deepseek-ai/*)随框架一起升级,请勿在市场单独更新。',
|
|
5951
6154
|
]
|
|
5952
|
-
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded, backupDir, registryError, steps, hints })
|
|
6155
|
+
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded, backupDir, registryError, configMigrations, steps, hints })
|
|
5953
6156
|
return
|
|
5954
6157
|
}
|
|
5955
6158
|
|
|
@@ -6001,7 +6204,7 @@ async function handle(ctx, req, res) {
|
|
|
6001
6204
|
`$started = $false`,
|
|
6002
6205
|
`for ($i = 0; $i -lt 20; $i++) {`,
|
|
6003
6206
|
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
6004
|
-
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { Start-Process -FilePath ${ps(nodePath)}
|
|
6207
|
+
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { $launchLog = Join-Path (Split-Path $log) 'fw-relaunch.log'; try { Add-Content -Path $launchLog -Value ((Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' 拉起(回滚后): ' + $binNow) -Encoding UTF8 } catch {}; Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', ('"' + ${ps(nodePath)} + '" "' + $binNow + '" web >> "' + $launchLog + '" 2>&1')) -WindowStyle Hidden; $started = $true; Log '已发起拉起服务(回滚后,输出见 fw-relaunch.log)' } else { Log 'bin.js 缺失,跳过拉起' } }`,
|
|
6005
6208
|
` Start-Sleep -Seconds 5`,
|
|
6006
6209
|
`}`,
|
|
6007
6210
|
`if ($ok) { SetState 'done' ('已回滚到升级前版本 ${rec.from ?? '?'},服务正常') ; Log '回滚完成,服务已恢复' } else { SetState 'failed' '回滚后服务拉起失败:请手动运行 node ${resolveDshBin() ?? '<bin>'} web' ; Log '回滚后拉起失败' }`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.33",
|
|
4
4
|
"description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"lib",
|
|
36
|
-
"cordis.patch.yml"
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"SECURITY.md"
|
|
37
38
|
],
|
|
38
39
|
"license": "MIT",
|
|
39
40
|
"scripts": {
|