@noob-stupid/dsh-plugin-console 0.3.32 → 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/index.js +184 -33
- 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/index.js
CHANGED
|
@@ -3797,6 +3797,99 @@ function checkpointFrameworkTree(fwRoot, destRoot) {
|
|
|
3797
3797
|
return { dest, mirrored }
|
|
3798
3798
|
}
|
|
3799
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
|
+
|
|
3800
3893
|
/** 移除补丁中的单行 disabled/forced 覆盖块(兼容门解锁用)。 */
|
|
3801
3894
|
function removeDisableBlock(patchPath, rowId) {
|
|
3802
3895
|
return queuedWrite(async () => {
|
|
@@ -5588,6 +5681,23 @@ async function handle(ctx, req, res) {
|
|
|
5588
5681
|
const hasUpdate = target !== null
|
|
5589
5682
|
const steps = []
|
|
5590
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
|
+
}
|
|
5591
5701
|
// 2) 自动升级框架:运行中的服务锁着 dsh/lib 目录(EBUSY 无法原地替换),
|
|
5592
5702
|
// 因此生成独立升级脚本(kill 服务 → npm 升级 → 失败回滚 → 拉起服务),后台执行。
|
|
5593
5703
|
// 与 /restart 同款自守护模式:端口无监听会自动拉起,不会留下"服务起不来"。
|
|
@@ -5604,7 +5714,7 @@ async function handle(ctx, req, res) {
|
|
|
5604
5714
|
// 避免"kill 后升级/拉起双双失败,服务永久 down"
|
|
5605
5715
|
if (!existsSync(corepackJs) || binPath === null || !existsSync(binPath)) {
|
|
5606
5716
|
steps.push(`升级已取消:无法定位 ${!existsSync(corepackJs) ? 'corepack(pnpm 通道)' : 'dsh bin.js'}(框架安装可能不完整),服务保持运行;请先修复框架再升级`)
|
|
5607
|
-
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 })
|
|
5608
5718
|
return
|
|
5609
5719
|
}
|
|
5610
5720
|
const profileDir2 = profileDir
|
|
@@ -5659,7 +5769,13 @@ async function handle(ctx, req, res) {
|
|
|
5659
5769
|
try { mkdirSync(dirname(logFile), { recursive: true }) } catch {}
|
|
5660
5770
|
// PowerShell 字符串里反斜杠是字面量(无 \\ 转义):JSON.stringify 产生的 \\ 必须还原为 \,
|
|
5661
5771
|
// 否则所有路径无效(npm 调不起来、回滚无效、拉起失败——曾导致升级脚本空跑)
|
|
5662
|
-
|
|
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
|
+
}
|
|
5663
5779
|
const lines = [
|
|
5664
5780
|
`$state = ${ps(stateFile)}`,
|
|
5665
5781
|
`$log = ${ps(logFile)}`,
|
|
@@ -5669,8 +5785,9 @@ async function handle(ctx, req, res) {
|
|
|
5669
5785
|
// trap 捕获后:写 failed 状态 + 日志 + 尝试拉起服务 + 自删任务 + 退出
|
|
5670
5786
|
"trap {",
|
|
5671
5787
|
" try { SetState 'failed' ('升级脚本异常终止:' + $_.Exception.Message) } catch {}",
|
|
5672
|
-
|
|
5673
|
-
|
|
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 {}`,
|
|
5674
5791
|
` schtasks /delete /f /tn ${taskName} 2>$null`,
|
|
5675
5792
|
" exit 1",
|
|
5676
5793
|
"}",
|
|
@@ -5739,9 +5856,12 @@ async function handle(ctx, req, res) {
|
|
|
5739
5856
|
` }`,
|
|
5740
5857
|
` if ($code -eq 0) {`,
|
|
5741
5858
|
` try {`,
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
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 }`,
|
|
5745
5865
|
` }`,
|
|
5746
5866
|
` return $code`,
|
|
5747
5867
|
`}`,
|
|
@@ -5752,29 +5872,39 @@ async function handle(ctx, req, res) {
|
|
|
5752
5872
|
`function Invoke-Rollback {`,
|
|
5753
5873
|
` SetState 'rollback' '升级失败,回滚框架(全树/CLI 备份)…'`,
|
|
5754
5874
|
` Log '升级失败,回滚框架…'`,
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
`
|
|
5759
|
-
` $
|
|
5760
|
-
`
|
|
5761
|
-
`
|
|
5762
|
-
` $
|
|
5763
|
-
` $
|
|
5764
|
-
`
|
|
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`,
|
|
5765
5898
|
` }`,
|
|
5766
|
-
`
|
|
5767
|
-
` if (Test-Path $
|
|
5768
|
-
`
|
|
5769
|
-
`
|
|
5770
|
-
` $script:rolledBack = $true`,
|
|
5771
|
-
`
|
|
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)`,
|
|
5772
5907
|
` }`,
|
|
5773
|
-
` # CLI 兜底(checkpoint 缺失或损坏)`,
|
|
5774
|
-
` if (-not (Test-Path ${ps(join(rollbackDir, 'lib', 'bin.js'))})) { Log ('回滚失败:CLI 备份也缺失(' + ${ps(rollbackDir)} + ')'); return }`,
|
|
5775
|
-
` Remove-Item ${ps(dshDir)} -Recurse -Force -ErrorAction SilentlyContinue`,
|
|
5776
|
-
` robocopy ${ps(rollbackDir)} ${ps(dshDir)} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null`,
|
|
5777
|
-
` if (Test-Path ${ps(join(dshDir, 'lib', 'bin.js'))}) { Log '回滚完成(CLI 恢复)'; $script:rolledBack = $true } else { Log '回滚失败:复制失败,请手动修复框架安装' }`,
|
|
5778
5908
|
`}`,
|
|
5779
5909
|
`$script:rolledBack = $false`,
|
|
5780
5910
|
`$code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
@@ -5787,7 +5917,28 @@ async function handle(ctx, req, res) {
|
|
|
5787
5917
|
` $code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
5788
5918
|
` if ($code -ne 0) { Log ('(停服后)npmmirror 安装失败(exit=' + $code + '),切换 registry.npmjs.org 重试一次'); $code = Install-Framework 'https://registry.npmjs.org' }`,
|
|
5789
5919
|
`}`,
|
|
5790
|
-
`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
|
+
`}`,
|
|
5791
5942
|
// 回滚后跳过重链/依赖修复/版本对齐(它们按新树预期运行,会对已恢复的旧树造成二次破坏)
|
|
5792
5943
|
`if (-not $script:rolledBack) {`,
|
|
5793
5944
|
// 重新链接框架配套包(事故教训):pnpm 升级到新版本时 .pnpm 目录重建,
|
|
@@ -5927,7 +6078,7 @@ async function handle(ctx, req, res) {
|
|
|
5927
6078
|
'$started = $false',
|
|
5928
6079
|
`for ($i = 0; $i -lt 20; $i++) {`,
|
|
5929
6080
|
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
5930
|
-
` 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 缺失,跳过拉起' } }`,
|
|
5931
6082
|
' Start-Sleep -Seconds 5',
|
|
5932
6083
|
'}',
|
|
5933
6084
|
// 拉起失败不再只留提示(2026-09-04 教训):自动全树回滚并再拉起一次
|
|
@@ -5938,7 +6089,7 @@ async function handle(ctx, req, res) {
|
|
|
5938
6089
|
` $started = $false`,
|
|
5939
6090
|
` for ($i = 0; $i -lt 20; $i++) {`,
|
|
5940
6091
|
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
5941
|
-
` 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 缺失,跳过拉起' } }`,
|
|
5942
6093
|
` Start-Sleep -Seconds 5`,
|
|
5943
6094
|
` }`,
|
|
5944
6095
|
`}`,
|
|
@@ -6001,7 +6152,7 @@ async function handle(ctx, req, res) {
|
|
|
6001
6152
|
'升级完成后请重启 DSH 服务生效——重启后 Hub 自动:备份新版本配置快照、重打框架补丁(issue #5 容错)、给出适配提示。',
|
|
6002
6153
|
'官方配套组件(@deepseek-ai/*)随框架一起升级,请勿在市场单独更新。',
|
|
6003
6154
|
]
|
|
6004
|
-
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 })
|
|
6005
6156
|
return
|
|
6006
6157
|
}
|
|
6007
6158
|
|
|
@@ -6053,7 +6204,7 @@ async function handle(ctx, req, res) {
|
|
|
6053
6204
|
`$started = $false`,
|
|
6054
6205
|
`for ($i = 0; $i -lt 20; $i++) {`,
|
|
6055
6206
|
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
6056
|
-
` 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 缺失,跳过拉起' } }`,
|
|
6057
6208
|
` Start-Sleep -Seconds 5`,
|
|
6058
6209
|
`}`,
|
|
6059
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": {
|