dsh-plugin-tool-management 0.11.0 → 0.12.0
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/CHANGELOG.md +29 -0
- package/README.md +3 -1
- package/README_EN.md +3 -1
- package/cordis.patch.yml +10 -1
- package/docs/update.md +27 -0
- package/lib/client.js +65 -3
- package/lib/compat/patch-dialect.js +173 -0
- package/lib/compat/probe.js +47 -4
- package/lib/compat/runtime-notes.js +25 -0
- package/lib/context-inject.js +72 -9
- package/lib/index.js +117 -4
- package/lib/mcp/manager.js +102 -14
- package/lib/ops/compat.js +96 -6
- package/lib/sessions/bridge.js +109 -31
- package/lib/sessions/workspace.js +59 -26
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [0.12.0] - 2026-09-20
|
|
14
|
+
|
|
15
|
+
### 新增
|
|
16
|
+
|
|
17
|
+
- **兼容页「功能总览」**:一行一个功能点(十几项,第一行是「插件是否完整挂载」),状态由三层合成 —— 装配层(listener / provider / 适配是否真的挂上)+宿主能力层(探测结果)+你自己的配置(开关 / 令牌 / 场景锁定)。取值:正常 / 降级(附原因)/ 未启用 / 受令牌限制 / 部分生效 / 不可用。点一行跳到该功能所在的页签;数据全部来自已有缓存,不新增宿主探测。
|
|
18
|
+
- **停用的 MCP 工具真的从模型可见的工具表里消失了**:`tools.restrict` 之前是在插件级上下文上调的,官方要求 agent scope、必抛 —— 异常被当成"registry race"吞掉,所以「停用」一直只有执行时拦截这一半。现在按 agent 逐个应用(跟随 agent 上线/下线),重新启用后工具会恢复可见。
|
|
19
|
+
- **补丁写入前先做一次解析校验**:复刻官方解析补丁用的 YAML 方言(js-yaml + `!!js` 标签 + 官方那两条断言),写入前判一次产物。判据是**非对称**的 —— 只有"改前能解析、改后不能解析"(=我们的改动把产物弄坏了)才拒绝写入并保留原文件与备份;改前本来就解析不过(说明复刻过期)照写 + 上报;依赖不在也照写 + 上报。校验结论会出现在兼容页,写入回执也带一条 warning。
|
|
20
|
+
- **doctor 新增三项检查**:① 插件运行时按裸模块名要的那几个宿主包(MCP 客户端 / 人设 / AGENTS.md 注入 / 技能目录)是否仍被宿主暴露;② 插件挂载心跳(`tool-management/mount.json`,含 12 个 inject 服务名)—— 官方改名任何一个 inject 服务名时插件根本不会 apply,且官方既不抛错也不打日志,这份心跳是那种情形下唯一的线索;③ 宿主版本与本插件验证过的版本不一致时显式提示。
|
|
21
|
+
- **发行物的 `!!js` 表达式自带兜底**:双挂载判定那段表达式在**每次启动**被宿主求值,而那条调用链没有任何保护;现在表达式自己包了 try/catch,出错返回 false(=不参与判定、保持挂载),绝不把启动流程拖垮。
|
|
22
|
+
|
|
23
|
+
### 变更
|
|
24
|
+
|
|
25
|
+
- **宿主补上原生删除入口时,先不切换**:单删与批删两条都改为"上报「宿主已提供原生入口」+ 继续走本插件自有完整序列(级联子会话 + spill + 记账 + 缓存行)"。切过去的代码今天不可达,写成即等于它会在官方补上入口的那次升级上首次运行、且无从事前验证。
|
|
26
|
+
- **会话删除的前置检查补齐**:转录清理还要 `readSessionHeader` 与持久化后端的 `locate()`,而它们排在「装写屏障 → flush → detach → 清缓存行」之后 —— 缺任何一个,失败点都会落在中段(转录还在、记账已清)。现在这两项与已有检查一样,在任何破坏性步骤之前先确认。
|
|
27
|
+
- **`@deepseek-ai/cordis` 的 peer 下界提到 `^4.0.2`**(对齐宿主实装);两个子智能体 spawn provider 进 devDependencies(运行时硬 require、此前在本仓库解析不到);移除没有引用的 devDependency `@deepseek-ai/dsh-user-approval`。
|
|
28
|
+
- **依赖投递链写进文档**:`npm run build` 的镜像清单**不含 `node_modules`**,新增的运行时依赖(js-yaml)需要在 profile 侧安装;装不上时校验自动降级为「跳过 + 上报」。
|
|
29
|
+
|
|
30
|
+
### 修复
|
|
31
|
+
|
|
32
|
+
- **关掉技能 / 提示词域后,官方那条注入真的停了**:此前拦不拦得住取决于启动时的注册先后 —— 没拦住的那次,官方内容照旧进上下文,界面上也看不出异常。现在位置固定,不再看运气。
|
|
33
|
+
- **投影缓存删除屏障的三处缺陷**:① 第三方也包装同一份缓存时,旧屏障退役后仍留在登记表里、被下一次删除复用 → 墓碑拦截静默失效(缓存行可能复活);现在退役即摘出登记表,且复用前先验证包装器确实在岗。② 首次使用抛 `TypeError`(宿主形状变了)时不再把宿主留在"半个包装器"状态下 —— 恢复原方法并上报。③ 安装期(`requireTable()`)抛错时上报,不再只抛一句裸错。
|
|
34
|
+
- **兼容页的删除动作可用性判定与服务端对齐**:此前客户端要求一个可选槽位(`projection.delete-native`),把本机可用的删除显示成不可用;同时服务端运行时真正要求的那条硬前提(投影缓存存储可删行)提升为能力表的正式条目 —— 界面说的和服务端做的从此同一份依据。
|
|
35
|
+
- **兼容页的宿主身份锚点与 doctor 同一套策略**:此前运行时只按"插件自己解析到的包"上溯,dev 布局下会比出假的「同一份模块」,出现"界面说 ok、doctor 说 SEPARATE COPY"。
|
|
36
|
+
- **工作区实体快照写不进去时不再静默**:写入后立刻读回,没写进去就上报(界面上的会话计数可能不刷新)。
|
|
37
|
+
- **几处"只落 console"的装配失败现在出现在兼容页**:技能 provider 装配失败、上下文注入通道装配失败 —— 此前界面看起来一切正常。
|
|
38
|
+
- **关域拦截有了顺序自检**:关域生效期间连续多轮一条都没拦到时会提示"可能没拦到(也可能是官方那几轮本来没内容)"。
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
13
42
|
## [0.11.0] - 2026-09-20
|
|
14
43
|
|
|
15
44
|
### 新增
|
package/README.md
CHANGED
|
@@ -220,7 +220,9 @@ npm run check:i18n # 词典自检
|
|
|
220
220
|
npm run doctor # 宿主兼容体检
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
`lib/` 不入版本库,克隆后先 `npm run build`。改完重启 `dsh web`
|
|
223
|
+
`lib/` 不入版本库,克隆后先 `npm run build`。改完重启 `dsh web` 才生效。运行时依赖:`fflate`(导出打包)、`js-yaml`(写宿主补丁前的解析校验,惰性加载);`@deepseek-ai/*` 一律用宿主那份。
|
|
224
|
+
|
|
225
|
+
> **部署注意**:`npm run build` 的 profile 镜像清单**不含 `node_modules`**,所以本地联调时新增的运行时依赖(如 `js-yaml`)要么在 profile 侧装一份,要么接受「补丁写入校验跳过并上报」的降级 —— 校验器装不上只影响这一道保险,不阻断写入。`npm install` 装到 profile 的正式安装不受影响(依赖会随包安装)。
|
|
224
226
|
|
|
225
227
|
## 许可证
|
|
226
228
|
|
package/README_EN.md
CHANGED
|
@@ -220,7 +220,9 @@ npm run check:i18n # dictionary self-check
|
|
|
220
220
|
npm run doctor # host compatibility check
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
`lib/` is not tracked — run `npm run build` after cloning. Changes need `dsh web` restarted. Runtime
|
|
223
|
+
`lib/` is not tracked — run `npm run build` after cloning. Changes need `dsh web` restarted. Runtime deps: `fflate` (export bundling) and `js-yaml` (parse check before writing host patches, loaded lazily); `@deepseek-ai/*` all come from the host.
|
|
224
|
+
|
|
225
|
+
> **Deployment note**: the profile mirror produced by `npm run build` does **not** include `node_modules`, so a runtime dependency added locally (e.g. `js-yaml`) must either be installed on the profile side or accepted as "patch-write check skipped and reported" — the missing checker only disables that one safety net, it never blocks writes. Regular `npm install` deployments are unaffected (dependencies are installed with the package).
|
|
224
226
|
|
|
225
227
|
## License
|
|
226
228
|
|
package/cordis.patch.yml
CHANGED
|
@@ -6,9 +6,18 @@
|
|
|
6
6
|
# Double-mount guard: inspect raw options only. Entry.disabled is a computed
|
|
7
7
|
# getter and reading it from another disabled expression can recurse at boot.
|
|
8
8
|
#
|
|
9
|
+
# Why the IIFE + try/catch: this expression is evaluated by the host at EVERY
|
|
10
|
+
# boot (`cordis-plugin-loader` Entry.disabledOf → evaluate → new Function+eval),
|
|
11
|
+
# and that call chain has no protection of its own. It reads host-internal
|
|
12
|
+
# shapes (ctx.loader.entries(), e.options.{id,name,disabled}), while the peer
|
|
13
|
+
# range on cordis carries no upper bound — so an upstream shape change would
|
|
14
|
+
# otherwise throw on the boot path, where the only outcomes are "DSH does not
|
|
15
|
+
# start" or "this row never mounts". An error therefore means `false` (not
|
|
16
|
+
# disabled → stay mounted), never a throw.
|
|
17
|
+
#
|
|
9
18
|
# The optional subagent spawn provider is resolved at runtime by the subagent
|
|
10
19
|
# service; an unavailable optional package must not prevent the host from booting.
|
|
11
20
|
- insert:
|
|
12
21
|
- id: dsh-plugin-tool-management
|
|
13
22
|
name: 'dsh-plugin-tool-management'
|
|
14
|
-
disabled: !!js "[...ctx.loader.entries()].some((e) => e.options.id !== 'dsh-plugin-tool-management' && e.options.name === 'dsh-plugin-tool-management' && e.options.disabled !== true && typeof e.options.disabled !== 'object')"
|
|
23
|
+
disabled: !!js "(function () { try { return [...ctx.loader.entries()].some((e) => e.options.id !== 'dsh-plugin-tool-management' && e.options.name === 'dsh-plugin-tool-management' && e.options.disabled !== true && typeof e.options.disabled !== 'object') } catch (err) { return false } })()"
|
package/docs/update.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| 版本 | 发布 | 主题 |
|
|
6
6
|
| ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| [0.12.0](#0120---2026-09-20) | 2026-09-20 | 兼容页「功能总览」+ 停用工具真的从模型工具表消失 + 补丁写入前做解析校验(拦"写坏 DSH")+ doctor 三项新检查(挂载心跳 / 裸模块名 / 版本栅栏)+ 删除入口先不切换 + 投影缓存屏障与删除前置检查加固 + 关域拦截顺序钉死 |
|
|
7
8
|
| [0.11.0](#0110---2026-09-20) | 2026-09-20 | 访问令牌卡片重构(可设置 / 关闭 / 修改 / 删除)+ 没解锁时锁住输入框 + 令牌安全修复 + 批量启停可撤销 + 兼容页清理旧备份 + 两页可搜索 + 体检改为真调 + 一批凭据与数据损坏修复 + 搜索与列表提速 |
|
|
8
9
|
| [0.10.0](#0100---2026-09-18) | 2026-09-18 | 注入框架改 markdown(动作句加粗前置)+ 注入采纳统计 + 人设角色框 / inherit / catalogDepth + 场景锁定补齐模型侧 + 记忆删除全走回收站 + 错误码全量双语 |
|
|
9
10
|
| [0.9.1](#091---2026-09-17) | 2026-09-17 | 注入实况 + 场景内开关同步档案 + 全选统一 + 退出场景卡死修复 |
|
|
@@ -18,6 +19,32 @@
|
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
|
|
22
|
+
## 0.12.0 - 2026-09-20
|
|
23
|
+
|
|
24
|
+
- **兼容页多了一块「功能总览」**:一行一个功能点,第一行就是「插件是否完整挂载」;状态是装配层 + 宿主能力层 + 你当前配置的合成结果(正常 / 降级 / 未启用 / 受令牌限制 / 部分生效 / 不可用),点一行跳到对应页签。它回答的是"现在到底哪一项能用、为什么不能用"。
|
|
25
|
+
|
|
26
|
+
- **停用的 MCP 工具真的从模型可见的工具表里消失了**:以前「停用」只在调用时拦截,模型照样看得见这个名字,还会去调、然后被拒。现在按 agent 逐个摘掉;重新启用后恢复可见。
|
|
27
|
+
|
|
28
|
+
- **改写宿主配置前先自检一次**:复刻官方解析补丁的那套 YAML 方言,写入前判一次产物。只有"改前能解析、改后不能解析"(=我们的改动弄坏了)才拒绝写入并保留原文件与备份;复刻过期或依赖缺失一律照写 + 上报(不拿能力换保守)。这条正是针对"写坏 `cordis.patch.yml` → DSH 起不来"那类事故。
|
|
29
|
+
|
|
30
|
+
- **发行物自带的那段 `!!js` 表达式包了兜底**:它在每次启动被官方求值、且那条链上没有任何保护;现在出错只返回 false,不会把启动流程拖垮。
|
|
31
|
+
|
|
32
|
+
- **宿主补上原生删除入口时先不切换**:改为上报 + 继续走本插件自有完整序列(级联子会话 + spill + 记账 + 缓存行)。切换要等人工核对过原生语义 —— 那段代码今天不可达,不可事前验证的代码不写。
|
|
33
|
+
|
|
34
|
+
- **删除的前置检查补齐了**:转录清理需要的两个接口(会话头部读取、持久化 `locate()`)以前排在破坏性步骤之后,缺了会留下"转录还在、记账已清"的半删态;现在和别的检查一样,动手之前先确认。
|
|
35
|
+
|
|
36
|
+
- **关掉技能 / 提示词域后,官方那条注入真的停了**:此前拦不拦得住看启动时的注册先后,可能没拦住;现在位置固定,不再看运气。
|
|
37
|
+
|
|
38
|
+
- **投影缓存删除屏障的三处缺陷修掉**:第三方也包装同一份缓存时旧屏障会"退役了还被复用"(墓碑拦截静默失效、缓存行可能复活)、复用前不校验、首次使用抛错会留半个包装器。现在退役即摘出、复用先验证、出错就恢复原方法并上报。
|
|
39
|
+
|
|
40
|
+
- **兼容页显示与真实能力对齐**:删除动作的可用性判定以前把本机可用的删除显示成不可用;身份锚点以前在 dev 布局下会比出假的「同一份模块」(界面 ok、doctor 说不一致)。两处都改成与服务端/doctor 同一份依据。
|
|
41
|
+
|
|
42
|
+
- **几处"静默降级"改成看得见**:技能 provider 与注入通道装配失败(以前只落 console)、工作区实体快照写不进去、关域拦截因注册顺序变化而失效 —— 现在都会出现在兼容页的降级项里。
|
|
43
|
+
|
|
44
|
+
- **doctor 多了三项检查**:运行时裸模块名是否仍被宿主暴露;插件挂载心跳(含 12 个 inject 服务名,插件没挂上时唯一的线索);宿主版本与本插件验证过的版本不一致时显式提示。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
21
48
|
## 0.11.0 - 2026-09-20
|
|
22
49
|
|
|
23
50
|
- **访问令牌那块整个重排了,按作用域拆成两块**:「访问令牌」只管本次启动(状态胶囊 + 解锁框),「令牌管理」只管配置文件 —— 设置/修改、保护开关、删除、清除令牌四行静态清单,位置尺寸统一。所有写配置的操作都在弹窗里完成(要输一次令牌确认),页面不再有会展开收起的东西;成功就是弹窗收起 + 状态行更新,失败错误显示在弹窗内。
|
package/lib/client.js
CHANGED
|
@@ -770,6 +770,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
770
770
|
try { fn() } catch (e) { /* 单个订阅者出错不影响其它 */ }
|
|
771
771
|
}
|
|
772
772
|
}
|
|
773
|
+
/** 兼容页「功能总览」的行内跳转:跳到该功能所在的页签(注册表在 ToolsSection 里,见上)。 */
|
|
774
|
+
function jumpToTab(tab) {
|
|
775
|
+
try { if (navigateToTab) navigateToTab(tab) } catch (e) { /* 跳转失败不影响本页 */ }
|
|
776
|
+
}
|
|
773
777
|
function consumeTokenFocus() {
|
|
774
778
|
const pending = tokenFocusPending
|
|
775
779
|
tokenFocusPending = false
|
|
@@ -807,9 +811,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
807
811
|
routes.archive = ok('workspace.archive-native') ? 'native' : (ok('workspace.enqueue') && ok('workspace.set-state') ? 'adapter' : 'none')
|
|
808
812
|
routes.unarchive = ok('workspace.unarchive-native') ? 'native' : (ok('workspace.enqueue') && ok('workspace.set-state') ? 'adapter' : 'none')
|
|
809
813
|
routes.batch = ok('workspace.batch-native') ? 'native' : (ok('workspace.enqueue') && ok('workspace.set-state') ? 'adapter' : 'none')
|
|
814
|
+
// 与服务端 OPERATION_ROUTES 同源(src/compat/probe.ts):
|
|
815
|
+
// · delete 不要求 `projection.delete-native` —— 宿主自带删除屏障是可选槽位,
|
|
816
|
+
// 要求它会把 rc.2 上的删除显示成不可用(服务端有意不要求);
|
|
817
|
+
// · 但要求 `projection.table-delete` —— 那是运行时的硬前提(表不可删就没法安全删行)。
|
|
810
818
|
routes.delete = ok('workspace.delete-native') ? 'native'
|
|
811
819
|
: (ok('workspace.enqueue') && ok('workspace.set-state') && ok('workspace.index-header')
|
|
812
|
-
&& ok('sessions.detach-live') && ok('sessions.cold-announce') && ok('projection.
|
|
820
|
+
&& ok('sessions.detach-live') && ok('sessions.cold-announce') && ok('projection.write')
|
|
821
|
+
&& ok('projection.table-delete') ? 'adapter' : 'none')
|
|
813
822
|
return routes
|
|
814
823
|
}
|
|
815
824
|
|
|
@@ -825,6 +834,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
825
834
|
const [live, setLive] = React.useState(null)
|
|
826
835
|
const [liveOpen, setLiveOpen] = React.useState('')
|
|
827
836
|
const [liveCopied, setLiveCopied] = React.useState(false)
|
|
837
|
+
// 功能总览(B6):按功能点看"现在到底能不能用"。数据来自 feature-overview(只读聚合)。
|
|
838
|
+
const [features, setFeatures] = React.useState(null)
|
|
828
839
|
const [busy, setBusy] = React.useState(false)
|
|
829
840
|
const [error, setError] = React.useState(null)
|
|
830
841
|
// 访问令牌(用户裁定 2026-09-18:入口放兼容页)。它不属于"宿主体检",但**必须有一个
|
|
@@ -1062,6 +1073,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1062
1073
|
.then(function (r) { if (alive !== false && r && r.ok) setLive(r) })
|
|
1063
1074
|
.catch(function () { /* 读不到 → 不渲染这一节 */ })
|
|
1064
1075
|
}
|
|
1076
|
+
// 功能总览(B6):服务端按功能点聚合(装配层 + 宿主能力层 + 用户配置层)。
|
|
1077
|
+
// 它是只读的派生视图,取不到就不渲染这一节,绝不影响上面那条结论。
|
|
1078
|
+
const loadFeatures = function (alive) {
|
|
1079
|
+
apiCall('feature-overview', {})
|
|
1080
|
+
.then(function (r) { if (alive !== false && r && r.ok) setFeatures(r) })
|
|
1081
|
+
.catch(function () { /* 同上 */ })
|
|
1082
|
+
}
|
|
1065
1083
|
React.useEffect(function () {
|
|
1066
1084
|
let alive = true
|
|
1067
1085
|
setBusy(true)
|
|
@@ -1071,6 +1089,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1071
1089
|
loadReach(alive)
|
|
1072
1090
|
loadInject(alive)
|
|
1073
1091
|
loadLive(alive)
|
|
1092
|
+
loadFeatures(alive)
|
|
1074
1093
|
loadToken(alive)
|
|
1075
1094
|
loadBackups(alive)
|
|
1076
1095
|
return function () { alive = false }
|
|
@@ -1083,6 +1102,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1083
1102
|
loadReach(true)
|
|
1084
1103
|
loadInject(true)
|
|
1085
1104
|
loadLive(true)
|
|
1105
|
+
loadFeatures(true)
|
|
1086
1106
|
loadToken(true)
|
|
1087
1107
|
loadBackups(true)
|
|
1088
1108
|
}
|
|
@@ -1599,6 +1619,41 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1599
1619
|
: (usableCount === findings.length ? t('compat.caps.all') : t('compat.caps.substituted')),
|
|
1600
1620
|
blockedCount > 0)))
|
|
1601
1621
|
|
|
1622
|
+
// 版本栅栏(B3):实际宿主与本插件验证过的版本不同时说出来 —— 否则"照旧版本的印象
|
|
1623
|
+
// 判断"会让人把探测结果当成 bug。只提示、不拦(peer 保持无上界是既定决策)。
|
|
1624
|
+
const hostVersion = (data.host && data.host.version) || ''
|
|
1625
|
+
if (hostVersion && data.verifiedVersion && hostVersion !== data.verifiedVersion) {
|
|
1626
|
+
push(React.createElement('p', { className: 'dsm-help' },
|
|
1627
|
+
t('compat.host.mismatch')
|
|
1628
|
+
.replace('{host}', String(hostVersion))
|
|
1629
|
+
.replace('{verified}', String(data.verifiedVersion))))
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// 功能总览(B6):一行一个功能点,状态 = 装配层 + 宿主能力层 + 用户配置层 的合成
|
|
1633
|
+
// (服务端聚合,见 feature-overview)。行可点,跳到该功能所在的页签。
|
|
1634
|
+
if (features && Array.isArray(features.rows) && features.rows.length) {
|
|
1635
|
+
push(section(t('compat.features'), t('compat.features.hint'),
|
|
1636
|
+
React.createElement('div', { className: 'dsm-compat-mod-list dsm-compat-cards' },
|
|
1637
|
+
features.rows.map(function (row) {
|
|
1638
|
+
const cls = row.state === 'ok' ? 'dsm-compat-pill-ok'
|
|
1639
|
+
: (row.state === 'disabled' || row.state === 'locked') ? ''
|
|
1640
|
+
: 'dsm-compat-pill-warn'
|
|
1641
|
+
return React.createElement('div', {
|
|
1642
|
+
className: 'dsm-compat-mod-row' + (row.tab ? ' dsm-compat-row-link' : ''),
|
|
1643
|
+
key: row.key,
|
|
1644
|
+
role: row.tab ? 'button' : undefined,
|
|
1645
|
+
tabIndex: row.tab ? 0 : undefined,
|
|
1646
|
+
onClick: row.tab ? function () { jumpToTab(row.tab) } : undefined,
|
|
1647
|
+
},
|
|
1648
|
+
React.createElement('span', { className: 'dsm-compat-name' }, row.label),
|
|
1649
|
+
React.createElement('span', { className: 'dsm-compat-pill ' + cls },
|
|
1650
|
+
t('compat.feature.state.' + String(row.state || 'unknown'))),
|
|
1651
|
+
React.createElement('span', { className: 'dsm-compat-mod dsm-compat-mod-self' },
|
|
1652
|
+
React.createElement('span', { className: 'dsm-compat-label' }, t('compat.reason')),
|
|
1653
|
+
row.detail))
|
|
1654
|
+
}))))
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1602
1657
|
// 阻塞项放最前:这是唯一"必须动手"的东西。
|
|
1603
1658
|
if (blockers.length) {
|
|
1604
1659
|
push(section(t('compat.blockers'), null,
|
|
@@ -1635,6 +1690,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1635
1690
|
React.createElement('span', { className: 'dsm-compat-pill dsm-compat-pill-warn' },
|
|
1636
1691
|
item.fallback === 'native-entry' ? t('compat.fallback.native')
|
|
1637
1692
|
: item.fallback === 'refuse-operation' ? t('compat.fallback.refuse')
|
|
1693
|
+
: item.fallback === 'inform-only' ? t('compat.fallback.inform')
|
|
1638
1694
|
: t('compat.fallback.blocked')),
|
|
1639
1695
|
React.createElement('span', { className: 'dsm-compat-mod dsm-compat-mod-self' },
|
|
1640
1696
|
React.createElement('span', { className: 'dsm-compat-label' }, t('compat.reason')),
|
|
@@ -2112,9 +2168,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2112
2168
|
"compat.degraded": "已降级能力", "compat.degraded.hint": "缺什么 + 后果", "compat.reason": "原因:",
|
|
2113
2169
|
"compat.others": "其它能力项", "compat.others.hint": "不算降级:宿主没有这些入口时,由插件自己顶上",
|
|
2114
2170
|
"compat.fallback.substituted": "插件适配层(宿主无此入口)",
|
|
2115
|
-
"compat.fallback.native": "改走原生入口", "compat.fallback.refuse": "该操作会被拒绝", "compat.fallback.blocked": "相关按钮已禁用",
|
|
2171
|
+
"compat.fallback.native": "改走原生入口", "compat.fallback.refuse": "该操作会被拒绝", "compat.fallback.blocked": "相关按钮已禁用", "compat.fallback.inform": "不影响写入",
|
|
2116
2172
|
"compat.modules": "模块实体(插件 vs 宿主)", "compat.modules.hint": "同一份模块才谈得上适配", "compat.module.same": "同一份模块", "compat.module.separate": "两份拷贝(需修复)", "compat.module.unknown": "无法比较", "compat.modules.unverified": "有 {count} 个包没能比对(宿主的解析锚点里找不到:{names})—— 这不是「两份拷贝」,而是这项校验没做成。可运行 node scripts/doctor.mjs 查看。",
|
|
2117
2173
|
"compat.blockers": "阻塞项(按此修复)",
|
|
2174
|
+
"compat.host.mismatch": "实际宿主 {host} 与本插件验证过的 {verified} 不同:能力按实际逐项探测,下面出现降级项时以实际能力为准,不要按旧版本的印象判断。",
|
|
2175
|
+
"compat.features": "功能总览", "compat.features.hint": "一行一个功能点:装配层 + 宿主能力层 + 你当前配置的合成结果;点一行跳到对应页签",
|
|
2176
|
+
"compat.feature.state.ok": "正常", "compat.feature.state.degraded": "降级(看原因)", "compat.feature.state.disabled": "未启用", "compat.feature.state.locked": "受令牌限制", "compat.feature.state.unavailable": "不可用", "compat.feature.state.partial": "部分生效", "compat.feature.state.unknown": "判断不了",
|
|
2118
2177
|
"compat.reach": "预设注入边界", "compat.reach.hint": "每个预设下这些内容到不到得了模型",
|
|
2119
2178
|
"compat.reach.suppressing": "压制型",
|
|
2120
2179
|
"compat.inject": "注入", "compat.inject.hint": "本插件注入给模型的内容(不变不重发)",
|
|
@@ -2456,9 +2515,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2456
2515
|
"compat.degraded": "Degraded capabilities", "compat.degraded.hint": "what is missing + the consequence", "compat.reason": "Reason:",
|
|
2457
2516
|
"compat.others": "Other capability slots", "compat.others.hint": "not degraded: the plugin supplies its own implementation when the host has no such entry",
|
|
2458
2517
|
"compat.fallback.substituted": "Plugin adapter (no host entry)",
|
|
2459
|
-
"compat.fallback.native": "Falls back to native entry", "compat.fallback.refuse": "Operation refused", "compat.fallback.blocked": "Affected buttons disabled",
|
|
2518
|
+
"compat.fallback.native": "Falls back to native entry", "compat.fallback.refuse": "Operation refused", "compat.fallback.blocked": "Affected buttons disabled", "compat.fallback.inform": "Writes unaffected",
|
|
2460
2519
|
"compat.modules": "Module identity (plugin vs host)", "compat.modules.hint": "adapting only works on one shared module", "compat.module.same": "same module", "compat.module.separate": "separate copy (repair needed)", "compat.module.unknown": "not comparable", "compat.modules.unverified": "{count} package(s) could not be compared (not resolvable from the host anchor: {names}) — that is not \"a separate copy\", it means the check could not run. Try node scripts/doctor.mjs.",
|
|
2461
2520
|
"compat.blockers": "Blockers (fix these)",
|
|
2521
|
+
"compat.host.mismatch": "The running host {host} differs from the verified {verified}: capabilities are probed as installed, so trust the degraded list below over any assumption about that version.",
|
|
2522
|
+
"compat.features": "Feature overview", "compat.features.hint": "one row per feature point: assembly + host capability + your current configuration; click a row to open its tab",
|
|
2523
|
+
"compat.feature.state.ok": "OK", "compat.feature.state.degraded": "Degraded (see reason)", "compat.feature.state.disabled": "Off", "compat.feature.state.locked": "Token required", "compat.feature.state.unavailable": "Unavailable", "compat.feature.state.partial": "Partly active", "compat.feature.state.unknown": "Unknown",
|
|
2462
2524
|
"compat.reach": "Preset injection reach", "compat.reach.hint": "whether these contents reach the model under each preset",
|
|
2463
2525
|
"compat.reach.suppressing": "suppressing",
|
|
2464
2526
|
"compat.inject": "Injection", "compat.inject.hint": "what this plugin injects for the model (unchanged text is not resent)",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// 官方补丁方言(`cordis.patch.yml`)的**只读复刻** —— 写入宿主配置前的最后一道校验。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它:本插件 19 个调用面都在**文本级**改宿主的补丁文件(插块 / 删块 / 换行),
|
|
4
|
+
// 而官方解析这份文件用的是 js-yaml + 自定义 `!!js` 标签(`dsh-app-boot/lib/index.js:17-31`),
|
|
5
|
+
// 并且把「顶层必须是数组」「每项必须是映射」写成显式断言(同文件 `parsePatchList`,:1196-1200)。
|
|
6
|
+
// 解析失败 = DSH **下次启动直接起不来**(`CHANGELOG.md` 里记过这次事故)。三条写入路径
|
|
7
|
+
// (`profiles/<名>/cordis.patch.yml`、`~/.dsh/cordis.patch.yml`、bundle 的 `cordis.patch.yml`)
|
|
8
|
+
// 走的是同一份方言(前者 `loadOverlayPatches`、后者 `loadOptionalPatches` → `parsePatchList`;
|
|
9
|
+
// `userPatchesSchema === entryListSchema`,实测于 dsh-app-boot 0.1.5-rc.2)。
|
|
10
|
+
//
|
|
11
|
+
// 官方**没有**导出这份 schema(导出清单见同文件 :1575,无 `entryListSchema`),所以只能复刻。
|
|
12
|
+
// 复刻**必然会过期**(官方加方言 / 换 js-yaml 大版本),因此判定策略是**非对称**的:
|
|
13
|
+
// · 改前能解析、改后被我们改成不能解析 → 是我们的改动弄坏了它 → 拒绝写入(原文件与备份不动);
|
|
14
|
+
// · 改前本来就解析不过 → 说明复刻过期,不是我们的错 → 照写 + 上报,绝不拦;
|
|
15
|
+
// · 依赖(js-yaml)不在 → 跳过校验 + 上报。
|
|
16
|
+
// 一律拒绝会把「校验器过期」变成「MCP 写路径整体停摆」(含重启恢复写,见 request-gate.ts
|
|
17
|
+
// 里那处「恢复写失败会把服务器永久留在停用态」),那是拿能力换保守。
|
|
18
|
+
//
|
|
19
|
+
// `judgePatchText` / `decidePatchWrite` 是纯函数,契约测试直接钉上面几条分支;IO 只有
|
|
20
|
+
// `checkPatchWrite` 一处,它同时把结论记进运行时上报通道(兼容页据此出一行)与回执队列。
|
|
21
|
+
import { clearRuntimeNote, noteRuntime } from './runtime-notes.js';
|
|
22
|
+
/** 官方 `.js` 标签名(逐字取自 dsh-app-boot/lib/index.js:17)。 */
|
|
23
|
+
const JS_EXPR_TAG = 'tag:yaml.org,2002:js';
|
|
24
|
+
/** 官方 `isJsExpr`(cordis-plugin-loader/lib/index.js:302-304)的等价物。 */
|
|
25
|
+
const isJsExpr = (value) => value instanceof Object && '__jsExpr' in value;
|
|
26
|
+
function messageOf(error) {
|
|
27
|
+
return error instanceof Error ? error.message : String(error);
|
|
28
|
+
}
|
|
29
|
+
// 同一份 yaml 模块只 extend 一次(官方是在模块顶层 extend 一次的等价位)。
|
|
30
|
+
const schemas = new WeakMap();
|
|
31
|
+
function schemaOf(yaml) {
|
|
32
|
+
const cached = schemas.get(yaml);
|
|
33
|
+
if (cached !== undefined)
|
|
34
|
+
return cached;
|
|
35
|
+
// 与官方逐字等价的选项对象(dsh-app-boot/lib/index.js:17-23):load 只用到 resolve /
|
|
36
|
+
// construct;predicate / represent 属于 dump 方向,复刻它们是让这份定义与官方对齐。
|
|
37
|
+
const exprType = new yaml.Type(JS_EXPR_TAG, {
|
|
38
|
+
kind: 'scalar',
|
|
39
|
+
resolve: (data) => typeof data === 'string',
|
|
40
|
+
construct: (data) => ({ __jsExpr: data }),
|
|
41
|
+
predicate: isJsExpr,
|
|
42
|
+
represent: (data) => data.__jsExpr,
|
|
43
|
+
});
|
|
44
|
+
const schema = yaml.JSON_SCHEMA.extend(exprType);
|
|
45
|
+
schemas.set(yaml, schema);
|
|
46
|
+
return schema;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 判一份补丁文本官方解析得动吗(纯函数:不碰 IO、不记状态)。
|
|
50
|
+
*
|
|
51
|
+
* @param yaml - 注入的 yaml 模块(测试可传真模块;运行时是惰性 import 来的那份)。
|
|
52
|
+
* @param content - 补丁文件全文。
|
|
53
|
+
* @returns 判定结果;`unparseable` 的 detail 带官方会抛的那句错,直接可展示。
|
|
54
|
+
*/
|
|
55
|
+
export function judgePatchText(yaml, content) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = yaml.load(content, { schema: schemaOf(yaml) });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return { status: 'unparseable', detail: messageOf(error) };
|
|
62
|
+
}
|
|
63
|
+
// 官方 parsePatchList 的两条显式断言:顶层数组(:1199)、每项是映射(:1200)。
|
|
64
|
+
// 空文件(含只有空白的文件)在这里是 undefined → 判「解析不过」—— 与官方一致:
|
|
65
|
+
// 文件**不存在**官方容忍(`ENOENT` → 没有这一层),文件**在但空**官方是抛错的。
|
|
66
|
+
if (!Array.isArray(parsed)) {
|
|
67
|
+
return { status: 'unparseable', detail: '顶层不是 YAML 数组(官方 parsePatchList 的显式断言)' };
|
|
68
|
+
}
|
|
69
|
+
const bad = parsed.findIndex((entry) => typeof entry !== 'object' || entry === null || Array.isArray(entry));
|
|
70
|
+
if (bad >= 0) {
|
|
71
|
+
return { status: 'unparseable', detail: `第 ${bad + 1} 项不是映射(官方 parsePatchList 的显式断言)` };
|
|
72
|
+
}
|
|
73
|
+
return { status: 'ok' };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 非对称策略本体(纯函数)。三个分支就是本模块存在的理由,契约测试逐个钉住。
|
|
77
|
+
*
|
|
78
|
+
* @param before - 改前内容的判定;`null` 表示**没有可用基线**(文件不存在或读不到内容)。
|
|
79
|
+
* @param after - 本次要写入内容的判定。
|
|
80
|
+
* @returns 是否放行 + 要不要上报 + 拒绝时的说明。
|
|
81
|
+
*/
|
|
82
|
+
export function decidePatchWrite(before, after) {
|
|
83
|
+
if (after.status === 'ok')
|
|
84
|
+
return { allow: true, report: null };
|
|
85
|
+
if (after.status === 'no-dep')
|
|
86
|
+
return { allow: true, report: { kind: 'no-dep', detail: after.detail } };
|
|
87
|
+
// 改后解析不过:只有「本来就没有基线」或「改前是好的」才拦 —— 这两种都是我们引入的坏内容。
|
|
88
|
+
if (before !== null && before.status === 'unparseable') {
|
|
89
|
+
return {
|
|
90
|
+
allow: true,
|
|
91
|
+
report: {
|
|
92
|
+
kind: 'replica-outdated',
|
|
93
|
+
detail: `改前内容本来就解析不过(${before.detail}),本次写入按「复刻过期」放行`,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
allow: false,
|
|
99
|
+
report: null,
|
|
100
|
+
error: `写入被拒绝:新内容不是合法的补丁列表(${after.detail})。官方解析器会因此让 DSH 起不来,`
|
|
101
|
+
+ '已保留原文件与备份。',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ---------- 运行时依赖:惰性加载 + 结果缓存(`undefined` 未试过 / `null` 加载不到) ----------
|
|
105
|
+
let dialect;
|
|
106
|
+
let depError = '';
|
|
107
|
+
async function loadDialect() {
|
|
108
|
+
if (dialect !== undefined)
|
|
109
|
+
return dialect;
|
|
110
|
+
try {
|
|
111
|
+
// 与宿主实装同一份 js-yaml(本机实测 4.3.2)。只在写补丁时加载:插件加载期不碰它,
|
|
112
|
+
// 装不上也绝不因此让插件挂掉。
|
|
113
|
+
dialect = (await import('js-yaml'));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
dialect = null;
|
|
117
|
+
depError = messageOf(error);
|
|
118
|
+
}
|
|
119
|
+
return dialect;
|
|
120
|
+
}
|
|
121
|
+
// ---------- 状态:兼容页那一行(走运行时上报通道)+ 写入回执的 warning(消费式) ----------
|
|
122
|
+
/** 回执队列状态:`pendingWarnings` 只在「本次请求的写入没做成交验」时有货。 */
|
|
123
|
+
const pendingWarnings = [];
|
|
124
|
+
// 正常情况下每请求都会被取空;上限只是防止某个不取回的调用路径把它撑大。
|
|
125
|
+
const WARN_QUEUE_MAX = 8;
|
|
126
|
+
function record(report) {
|
|
127
|
+
if (report === null) {
|
|
128
|
+
clearRuntimeNote('patch-write-guard');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
noteRuntime({
|
|
132
|
+
id: 'patch-write-guard',
|
|
133
|
+
label: '补丁写入校验',
|
|
134
|
+
kind: 'write',
|
|
135
|
+
fallback: 'inform-only',
|
|
136
|
+
detail: (report.kind === 'no-dep' ? '校验依赖不可用' : '复刻可能已过期')
|
|
137
|
+
+ `:${report.detail}(写入照常进行,仅少一道「把启动配置写坏」的拦截)`,
|
|
138
|
+
});
|
|
139
|
+
pendingWarnings.push(report.kind === 'no-dep'
|
|
140
|
+
? '补丁写入未做解析校验(依赖不可用):' + report.detail
|
|
141
|
+
: '补丁写入的解析校验放行(可能是复刻过期):' + report.detail);
|
|
142
|
+
if (pendingWarnings.length > WARN_QUEUE_MAX)
|
|
143
|
+
pendingWarnings.splice(0, pendingWarnings.length - WARN_QUEUE_MAX);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 写入前的完整判定(唯一一处 IO/依赖入口)。
|
|
147
|
+
*
|
|
148
|
+
* 判完把结论记进 note / 回执队列:兼容页据此出「补丁校验不可用」一行,写入回执据此带 warning。
|
|
149
|
+
*
|
|
150
|
+
* @param previous - 改前全文(`''` 视为没有基线:文件不存在,或读不到内容)。
|
|
151
|
+
* @param next - 本次要写入的全文。
|
|
152
|
+
* @returns 判定;`allow=false` 时调用方必须中止写入。
|
|
153
|
+
*/
|
|
154
|
+
export async function checkPatchWrite(previous, next) {
|
|
155
|
+
const yaml = await loadDialect();
|
|
156
|
+
if (yaml === null) {
|
|
157
|
+
const decision = {
|
|
158
|
+
allow: true,
|
|
159
|
+
report: { kind: 'no-dep', detail: `无法加载 js-yaml:${depError}` },
|
|
160
|
+
};
|
|
161
|
+
record(decision.report);
|
|
162
|
+
return decision;
|
|
163
|
+
}
|
|
164
|
+
const before = previous === '' ? null : judgePatchText(yaml, previous);
|
|
165
|
+
const after = judgePatchText(yaml, next);
|
|
166
|
+
const decision = decidePatchWrite(before, after);
|
|
167
|
+
record(decision.report);
|
|
168
|
+
return decision;
|
|
169
|
+
}
|
|
170
|
+
/** 写入回执取用:取走并清空本次累积的 warning(每次请求取一次)。 */
|
|
171
|
+
export function takePatchGuardWarnings() {
|
|
172
|
+
return pendingWarnings.splice(0, pendingWarnings.length);
|
|
173
|
+
}
|
package/lib/compat/probe.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* leave the rest working.
|
|
31
31
|
*/
|
|
32
32
|
import { createRequire } from 'node:module';
|
|
33
|
-
import { readFileSync, realpathSync } from 'node:fs';
|
|
33
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
34
34
|
import { dirname, join } from 'node:path';
|
|
35
35
|
/** Packages whose physical module identity matters to this plugin. */
|
|
36
36
|
export const IDENTITY_PACKAGES = [
|
|
@@ -402,6 +402,31 @@ const CAPABILITY_SPECS = [
|
|
|
402
402
|
fallback: 'disable-destructive',
|
|
403
403
|
methods: ['write', 'put', 'requireTable'],
|
|
404
404
|
},
|
|
405
|
+
{
|
|
406
|
+
// B2:此前它只是 bridge 运行时那句拒绝里的**临时 id**(`acquireCacheGuard` 里现场拼的),
|
|
407
|
+
// 于是客户端压根不知道它 —— 宿主表不可删时,"路由说可用、点下去必拒"(V13)。提升为
|
|
408
|
+
// 能力表的正式条目后,路由判定与运行时前提同一份依据。
|
|
409
|
+
id: 'projection.table-delete',
|
|
410
|
+
label: '投影缓存行删除',
|
|
411
|
+
kind: 'delete',
|
|
412
|
+
owner: 'projectionCache',
|
|
413
|
+
fallback: 'disable-destructive',
|
|
414
|
+
// 探测内容就是运行时那句硬前提:`requireTable().delete` 在不在。
|
|
415
|
+
probe: (target) => {
|
|
416
|
+
if (typeof target?.requireTable !== 'function')
|
|
417
|
+
return '宿主投影缓存缺少 requireTable';
|
|
418
|
+
let table;
|
|
419
|
+
try {
|
|
420
|
+
table = target.requireTable();
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
return `宿主投影缓存 requireTable() 抛错:${String(error)}`;
|
|
424
|
+
}
|
|
425
|
+
if (typeof table?.delete !== 'function')
|
|
426
|
+
return '宿主投影缓存存储不支持安全删除(table.delete 缺失)';
|
|
427
|
+
return undefined;
|
|
428
|
+
},
|
|
429
|
+
},
|
|
405
430
|
{
|
|
406
431
|
id: 'projection.delete-native',
|
|
407
432
|
label: '投影缓存删除屏障',
|
|
@@ -455,6 +480,10 @@ export const OPERATION_ROUTES = {
|
|
|
455
480
|
adapter: [
|
|
456
481
|
'workspace.enqueue', 'workspace.set-state', 'workspace.index-header',
|
|
457
482
|
'sessions.detach-live', 'sessions.cold-announce', 'projection.write',
|
|
483
|
+
// B2:运行时硬前提(`table.delete`)也算一条路由要求 —— 不算进来的话,宿主表不可删时
|
|
484
|
+
// 路由说可用、点下去必拒。batch 的 adapter 路由**有意**保持只有 enqueue/set-state:
|
|
485
|
+
// 它的逐条失败会在结果里按 sessionId 报出来,此处不放宽也不收紧。
|
|
486
|
+
'projection.table-delete',
|
|
458
487
|
],
|
|
459
488
|
},
|
|
460
489
|
list: { native: [], adapter: ['workspace.read-state', 'workspace.read-table', 'workspace.index-shape'] },
|
|
@@ -706,20 +735,34 @@ export function assessHost(ctx) {
|
|
|
706
735
|
* 调用方:从锚点解析不到的包会被判成 `same = null`,见 assessHost 里的 `unverified`。
|
|
707
736
|
*/
|
|
708
737
|
function hostPackageRoot() {
|
|
738
|
+
// 与 doctor 的 `findHost` **同一套策略**(先看 `$DSH_HOME/profiles/node_modules/@deepseek-ai`,
|
|
739
|
+
// 再从插件自身位置逐级上溯,并确认那一层里真有 `dsh/package.json`)。此前运行时只按"插件
|
|
740
|
+
// 自己解析到的包"上溯,dev 布局下会把仓库里的副本当成宿主锚点、比出假的 `true` —— 于是
|
|
741
|
+
// 界面说 ok、doctor 说 SEPARATE COPY(V8)。两处口径分裂本身就是缺陷。
|
|
742
|
+
const home = process.env.DSH_HOME || join(process.env.USERPROFILE || process.env.HOME || '', '.dsh');
|
|
743
|
+
const candidates = [];
|
|
744
|
+
if (home !== '')
|
|
745
|
+
candidates.push(join(home, 'profiles', 'node_modules', '@deepseek-ai'));
|
|
709
746
|
for (const anchor of IDENTITY_PACKAGES) {
|
|
710
747
|
const resolved = realPathOf(safeResolve(anchor));
|
|
711
748
|
if (resolved === null)
|
|
712
749
|
continue;
|
|
713
750
|
let dir = dirname(resolved);
|
|
714
|
-
for (let i = 0; i <
|
|
715
|
-
if (dir.endsWith(join('node_modules', '@deepseek-ai')))
|
|
716
|
-
|
|
751
|
+
for (let i = 0; i < 4; i += 1) {
|
|
752
|
+
if (dir.endsWith(join('node_modules', '@deepseek-ai'))) {
|
|
753
|
+
candidates.push(dir);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
717
756
|
const parent = dirname(dir);
|
|
718
757
|
if (parent === dir)
|
|
719
758
|
break;
|
|
720
759
|
dir = parent;
|
|
721
760
|
}
|
|
722
761
|
}
|
|
762
|
+
for (const candidate of candidates) {
|
|
763
|
+
if (existsSync(join(candidate, 'dsh', 'package.json')))
|
|
764
|
+
return candidate;
|
|
765
|
+
}
|
|
723
766
|
return null;
|
|
724
767
|
}
|
|
725
768
|
/** Human-readable summary line for logs and the settings page header. */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 运行时降级上报通道 —— 「插件自己发现自己降级了」的统一出口。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它:有一批降级**没有抛错可挂**:它们发生在装配期或探测期(第三方包装接管了本插件的
|
|
4
|
+
// 运行时适配、注入域装配失败、启动期表达式或服务名解析出问题),失败当下的表现只是"某个功能
|
|
5
|
+
// 少了一半"或者"静默失效"。此前这些点只落 console / logger,用户在界面上看不到,要等事故之后
|
|
6
|
+
// 才倒查(审查报告 §3 记了多处)。这里给它们一个共用出口:`noteRuntime` 记一条,
|
|
7
|
+
// `compat-status` 把全部条目并进 findings —— 兼容页因此成为"降级总账"。
|
|
8
|
+
//
|
|
9
|
+
// 与 `probe.ts` 的 CAPABILITY_SPECS 分工:那张表是**写死的宿主能力清单**(每次探测重算),
|
|
10
|
+
// 这里是**运行时事件**(谁在什么时候发现了什么),两者在 compat-status 里合并展示。
|
|
11
|
+
//
|
|
12
|
+
// 键是 `id`:同一主题重复上报只覆盖不堆积(它表达的是"现在的状态",不是日志)。
|
|
13
|
+
const notes = new Map();
|
|
14
|
+
/** 记一条(同 id 覆盖)。 */
|
|
15
|
+
export function noteRuntime(note) {
|
|
16
|
+
notes.set(note.id, { ...note, at: Date.now() });
|
|
17
|
+
}
|
|
18
|
+
/** 该主题已恢复正常 → 收掉这一行。 */
|
|
19
|
+
export function clearRuntimeNote(id) {
|
|
20
|
+
notes.delete(id);
|
|
21
|
+
}
|
|
22
|
+
/** 当前全部运行时降级(`compat-status` 取用)。 */
|
|
23
|
+
export function runtimeNotes() {
|
|
24
|
+
return [...notes.values()];
|
|
25
|
+
}
|