dsh-plugin-tool-management 0.11.0 → 0.12.1
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 +42 -0
- package/README.md +4 -2
- package/README_EN.md +4 -2
- package/cordis.patch.yml +10 -1
- package/docs/update.md +38 -0
- package/lib/client.js +88 -12
- 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/mcp/state-section.js +1 -1
- package/lib/memories/constants.js +1 -1
- package/lib/ops/compat.js +96 -6
- package/lib/sessions/bridge.js +109 -31
- package/lib/sessions/workspace.js +59 -26
- package/lib/skills/catalog.js +1 -1
- package/lib/subagents/catalog.js +1 -1
- package/package.json +9 -6
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,48 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [0.12.1] - 2026-09-22
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- **子智能体页的人设名不再被挤成两三个字符**:以前名字和描述挤在同一横排里、右边还压着开关和两颗按钮,长名字显示成 `co...`。现在分成两行,名字独占一行、装不下才按省略号截断(悬停看全名)。
|
|
18
|
+
|
|
19
|
+
### 变更
|
|
20
|
+
|
|
21
|
+
- **客户端类型检查进了门禁**:`typecheck:client` 此前没有任何地方调用,而且是红的。修掉那处错误后并入 `test` 与 `prepublishOnly`。
|
|
22
|
+
- **四个注入上限抬高**:记忆段预算 64 KiB → **256 KiB**;技能目录 50 条 → **60 条**;子智能体 40 条 → **60 条**;MCP 50 条 → **60 条**。描述截断窗口不动(技能与子智能体仍 500 字、MCP 备注仍 200 字)—— 技能这项与官方 `dsh-tool-skill` 的默认值同源,抬成两套会让同一技能在不同预设下长度不一致。提示词域未动。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [0.12.0] - 2026-09-20
|
|
27
|
+
|
|
28
|
+
### 新增
|
|
29
|
+
|
|
30
|
+
- **兼容页「功能总览」**:一行一个功能点(十几项,第一行是「插件是否完整挂载」),状态由三层合成 —— 装配层(listener / provider / 适配是否真的挂上)+宿主能力层(探测结果)+你自己的配置(开关 / 令牌 / 场景锁定)。取值:正常 / 降级(附原因)/ 未启用 / 受令牌限制 / 部分生效 / 不可用。点一行跳到该功能所在的页签;数据全部来自已有缓存,不新增宿主探测。
|
|
31
|
+
- **停用的 MCP 工具真的从模型可见的工具表里消失了**:`tools.restrict` 之前是在插件级上下文上调的,官方要求 agent scope、必抛 —— 异常被当成"registry race"吞掉,所以「停用」一直只有执行时拦截这一半。现在按 agent 逐个应用(跟随 agent 上线/下线),重新启用后工具会恢复可见。
|
|
32
|
+
- **补丁写入前先做一次解析校验**:复刻官方解析补丁用的 YAML 方言(js-yaml + `!!js` 标签 + 官方那两条断言),写入前判一次产物。判据是**非对称**的 —— 只有"改前能解析、改后不能解析"(=我们的改动把产物弄坏了)才拒绝写入并保留原文件与备份;改前本来就解析不过(说明复刻过期)照写 + 上报;依赖不在也照写 + 上报。校验结论会出现在兼容页,写入回执也带一条 warning。
|
|
33
|
+
- **doctor 新增三项检查**:① 插件运行时按裸模块名要的那几个宿主包(MCP 客户端 / 人设 / AGENTS.md 注入 / 技能目录)是否仍被宿主暴露;② 插件挂载心跳(`tool-management/mount.json`,含 12 个 inject 服务名)—— 官方改名任何一个 inject 服务名时插件根本不会 apply,且官方既不抛错也不打日志,这份心跳是那种情形下唯一的线索;③ 宿主版本与本插件验证过的版本不一致时显式提示。
|
|
34
|
+
- **发行物的 `!!js` 表达式自带兜底**:双挂载判定那段表达式在**每次启动**被宿主求值,而那条调用链没有任何保护;现在表达式自己包了 try/catch,出错返回 false(=不参与判定、保持挂载),绝不把启动流程拖垮。
|
|
35
|
+
|
|
36
|
+
### 变更
|
|
37
|
+
|
|
38
|
+
- **宿主补上原生删除入口时,先不切换**:单删与批删两条都改为"上报「宿主已提供原生入口」+ 继续走本插件自有完整序列(级联子会话 + spill + 记账 + 缓存行)"。切过去的代码今天不可达,写成即等于它会在官方补上入口的那次升级上首次运行、且无从事前验证。
|
|
39
|
+
- **会话删除的前置检查补齐**:转录清理还要 `readSessionHeader` 与持久化后端的 `locate()`,而它们排在「装写屏障 → flush → detach → 清缓存行」之后 —— 缺任何一个,失败点都会落在中段(转录还在、记账已清)。现在这两项与已有检查一样,在任何破坏性步骤之前先确认。
|
|
40
|
+
- **`@deepseek-ai/cordis` 的 peer 下界提到 `^4.0.2`**(对齐宿主实装);两个子智能体 spawn provider 进 devDependencies(运行时硬 require、此前在本仓库解析不到);移除没有引用的 devDependency `@deepseek-ai/dsh-user-approval`。
|
|
41
|
+
- **依赖投递链写进文档**:`npm run build` 的镜像清单**不含 `node_modules`**,新增的运行时依赖(js-yaml)需要在 profile 侧安装;装不上时校验自动降级为「跳过 + 上报」。
|
|
42
|
+
|
|
43
|
+
### 修复
|
|
44
|
+
|
|
45
|
+
- **关掉技能 / 提示词域后,官方那条注入真的停了**:此前拦不拦得住取决于启动时的注册先后 —— 没拦住的那次,官方内容照旧进上下文,界面上也看不出异常。现在位置固定,不再看运气。
|
|
46
|
+
- **投影缓存删除屏障的三处缺陷**:① 第三方也包装同一份缓存时,旧屏障退役后仍留在登记表里、被下一次删除复用 → 墓碑拦截静默失效(缓存行可能复活);现在退役即摘出登记表,且复用前先验证包装器确实在岗。② 首次使用抛 `TypeError`(宿主形状变了)时不再把宿主留在"半个包装器"状态下 —— 恢复原方法并上报。③ 安装期(`requireTable()`)抛错时上报,不再只抛一句裸错。
|
|
47
|
+
- **兼容页的删除动作可用性判定与服务端对齐**:此前客户端要求一个可选槽位(`projection.delete-native`),把本机可用的删除显示成不可用;同时服务端运行时真正要求的那条硬前提(投影缓存存储可删行)提升为能力表的正式条目 —— 界面说的和服务端做的从此同一份依据。
|
|
48
|
+
- **兼容页的宿主身份锚点与 doctor 同一套策略**:此前运行时只按"插件自己解析到的包"上溯,dev 布局下会比出假的「同一份模块」,出现"界面说 ok、doctor 说 SEPARATE COPY"。
|
|
49
|
+
- **工作区实体快照写不进去时不再静默**:写入后立刻读回,没写进去就上报(界面上的会话计数可能不刷新)。
|
|
50
|
+
- **几处"只落 console"的装配失败现在出现在兼容页**:技能 provider 装配失败、上下文注入通道装配失败 —— 此前界面看起来一切正常。
|
|
51
|
+
- **关域拦截有了顺序自检**:关域生效期间连续多轮一条都没拦到时会提示"可能没拦到(也可能是官方那几轮本来没内容)"。
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
13
55
|
## [0.11.0] - 2026-09-20
|
|
14
56
|
|
|
15
57
|
### 新增
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ dsh plugin --profile web add dsh-plugin-tool-management@latest
|
|
|
89
89
|
- **「进/出模式」与「启用场景」是两个独立状态轴**:界面开关是唯一入口(两轴齐动)。绕过界面直接调 HTTP API 时要注意:`scene-mode-set{scene:null}` 只退运行时快照,**不清空启用集** —— 记忆与提示词仍按该场景注入;要彻底退出还需 `rules-set-active{scenes:[]}`。
|
|
90
90
|
- **导入**:`.md` / `.zip`(目录名 = 场景,bundle 带附件),同名跳过绝不覆盖,超限逐条回报。
|
|
91
91
|
- **导出**:勾选记忆打包成 zip,保留「场景/名称」层级;bundle 型连目录里的附件一起打进去。只读源文件。
|
|
92
|
-
- **注入预算**:默认
|
|
92
|
+
- **注入预算**:默认 256 KiB,放不下的跳过并列出清单。删除进回收站。
|
|
93
93
|
- **子代理会话不注入记忆**:记忆只在**顶层会话**注入 —— 它是"父会话的现场",不是子代理完成任务所需的事实;子代理的上下文只留「角色 + 任务」,要记忆可用 `memory_manager_list/read` 自己取,相关事实应由父代理写进 `task`。其余域不受影响(MCP / 技能 / 提示词照常注入,人设目录按各自的 `catalogDepth`)。
|
|
94
94
|
- **场景锁定**:锁定后 MCP / 技能 / 子智能体 / 记忆 / 提示词五个域的**增删改**整体只读,界面禁用 + 服务端守卫双侧拦截;未启动不能上锁,锁定中不能关闭,先解锁再改。**冻结的是"内容",不是"场景本身"** —— 场景的新建 / 删除 / 改名 / 提示词绑定 / 启用切换 / 回收站的恢复与永久删除、MCP 重启、导出、注入设置与令牌设置都不在冻结清单内(它们不是"场景档案里的内容")。其中「启用切换」在锁定场景正在生效时会被拒绝 —— 清空启用集合会让模型侧的写门禁失去判据(运行时却还是那个场景的档案态)。
|
|
95
95
|
- **删除场景 = 连记忆一起删**:场景记录、档案与全部记忆进同一条回收站条目,恢复按原路径整条放回;使用中的场景拒绝删除。场景名可改(连带目录与档案,记忆正文不动)。
|
|
@@ -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
|
@@ -89,7 +89,7 @@ The model can manage everything above via 14 tools (`mcp_manager_*` / `skill_man
|
|
|
89
89
|
- **"Entering/leaving the mode" and "enabling a scene" are two independent state axes**: the UI toggle is the only entry point (it moves both axes at once). If you bypass the UI and call the HTTP API directly, note that `scene-mode-set{scene:null}` only drops the runtime snapshot and does **not clear the enabled set** — memories and the prompt keep being injected for that scene; a full exit also needs `rules-set-active{scenes:[]}`.
|
|
90
90
|
- **Import**: `.md` / `.zip` (dir name = scene, bundles carry attachments), same names skipped never overwritten, over-limit items reported.
|
|
91
91
|
- **Export**: pick memories and zip them, keeping the `scene/name` layout; bundle memories bring their attachments along. Sources are read-only.
|
|
92
|
-
- **Injection budget**: default
|
|
92
|
+
- **Injection budget**: default 256 KiB, oversized memories skipped with a list. Deletes go to recycle bin.
|
|
93
93
|
- **Subagent sessions do not receive memories**: memories are injected into the **top-level session only** — they are the parent's situation, not the facts a child needs; a child's context stays "persona + task", it can read memories on demand with `memory_manager_list/read`, and relevant facts belong in the `task`. Other domains are unaffected (MCP / skills / prompts still inject; the persona catalog follows each persona's `catalogDepth`).
|
|
94
94
|
- **Scene lock**: once locked, create/update/delete across MCP / skills / subagents / memories / prompts is read-only — UI disabled plus a server-side guard; a scene must be running to lock, and a locked scene can't be closed until unlocked. **What freezes is the content, not the scene itself**: creating/deleting/renaming a scene, rebinding its prompt, switching the active set, recycle-bin restore and purge, MCP restart, export, injection settings and token settings are all outside the frozen list (they are not "content of the scene profile"). Switching the active set *is* refused while a locked scene is in effect — clearing it would leave the model-side write guard with nothing to check while the runtime still runs that scene's profile.
|
|
95
95
|
- **Deleting a scene deletes its memories too**: the scene record, profile and every memory go into one recycle-bin entry, restored as a whole; a running scene refuses deletion. Scene names are renameable (dir and profile follow, memory bodies untouched).
|
|
@@ -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,8 @@
|
|
|
4
4
|
|
|
5
5
|
| 版本 | 发布 | 主题 |
|
|
6
6
|
| ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| [0.12.1](#0121---2026-09-22) | 2026-09-22 | 子智能体列表行:人设名与描述分行 + 客户端类型检查进门禁 + 四个注入上限抬高 |
|
|
8
|
+
| [0.12.0](#0120---2026-09-20) | 2026-09-20 | 兼容页「功能总览」+ 停用工具真的从模型工具表消失 + 补丁写入前做解析校验(拦"写坏 DSH")+ doctor 三项新检查(挂载心跳 / 裸模块名 / 版本栅栏)+ 删除入口先不切换 + 投影缓存屏障与删除前置检查加固 + 关域拦截顺序钉死 |
|
|
7
9
|
| [0.11.0](#0110---2026-09-20) | 2026-09-20 | 访问令牌卡片重构(可设置 / 关闭 / 修改 / 删除)+ 没解锁时锁住输入框 + 令牌安全修复 + 批量启停可撤销 + 兼容页清理旧备份 + 两页可搜索 + 体检改为真调 + 一批凭据与数据损坏修复 + 搜索与列表提速 |
|
|
8
10
|
| [0.10.0](#0100---2026-09-18) | 2026-09-18 | 注入框架改 markdown(动作句加粗前置)+ 注入采纳统计 + 人设角色框 / inherit / catalogDepth + 场景锁定补齐模型侧 + 记忆删除全走回收站 + 错误码全量双语 |
|
|
9
11
|
| [0.9.1](#091---2026-09-17) | 2026-09-17 | 注入实况 + 场景内开关同步档案 + 全选统一 + 退出场景卡死修复 |
|
|
@@ -18,6 +20,42 @@
|
|
|
18
20
|
|
|
19
21
|
---
|
|
20
22
|
|
|
23
|
+
## 0.12.1 - 2026-09-22
|
|
24
|
+
|
|
25
|
+
- **子智能体页的列表行改了版式**:人设名与描述分成两行。以前名字和描述挤在同一横排里、右边还压着开关和两颗按钮,名字只剩两三个字符宽(显示成 `co...`);现在名字独占一行、装不下才按省略号截断(悬停看全名)。
|
|
26
|
+
|
|
27
|
+
- **客户端的类型检查进了门禁**:`typecheck:client` 以前只是定义在那里、没有任何地方调用,而且是红的。修掉后并进了 `npm test` 与发布前置 —— 客户端改动不再只有语法检查这一道网。
|
|
28
|
+
|
|
29
|
+
- **四个注入上限抬高了**:记忆段预算 64 KiB → 256 KiB;技能目录 50 条 → 60 条;子智能体 40 条 → 60 条;MCP 50 条 → 60 条。描述截断窗口没动(技能与官方同源,抬成两套会让同一技能在不同预设下长度不一致)。提示词域也没动。
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 0.12.0 - 2026-09-20
|
|
34
|
+
|
|
35
|
+
- **兼容页多了一块「功能总览」**:一行一个功能点,第一行就是「插件是否完整挂载」;状态是装配层 + 宿主能力层 + 你当前配置的合成结果(正常 / 降级 / 未启用 / 受令牌限制 / 部分生效 / 不可用),点一行跳到对应页签。它回答的是"现在到底哪一项能用、为什么不能用"。
|
|
36
|
+
|
|
37
|
+
- **停用的 MCP 工具真的从模型可见的工具表里消失了**:以前「停用」只在调用时拦截,模型照样看得见这个名字,还会去调、然后被拒。现在按 agent 逐个摘掉;重新启用后恢复可见。
|
|
38
|
+
|
|
39
|
+
- **改写宿主配置前先自检一次**:复刻官方解析补丁的那套 YAML 方言,写入前判一次产物。只有"改前能解析、改后不能解析"(=我们的改动弄坏了)才拒绝写入并保留原文件与备份;复刻过期或依赖缺失一律照写 + 上报(不拿能力换保守)。这条正是针对"写坏 `cordis.patch.yml` → DSH 起不来"那类事故。
|
|
40
|
+
|
|
41
|
+
- **发行物自带的那段 `!!js` 表达式包了兜底**:它在每次启动被官方求值、且那条链上没有任何保护;现在出错只返回 false,不会把启动流程拖垮。
|
|
42
|
+
|
|
43
|
+
- **宿主补上原生删除入口时先不切换**:改为上报 + 继续走本插件自有完整序列(级联子会话 + spill + 记账 + 缓存行)。切换要等人工核对过原生语义 —— 那段代码今天不可达,不可事前验证的代码不写。
|
|
44
|
+
|
|
45
|
+
- **删除的前置检查补齐了**:转录清理需要的两个接口(会话头部读取、持久化 `locate()`)以前排在破坏性步骤之后,缺了会留下"转录还在、记账已清"的半删态;现在和别的检查一样,动手之前先确认。
|
|
46
|
+
|
|
47
|
+
- **关掉技能 / 提示词域后,官方那条注入真的停了**:此前拦不拦得住看启动时的注册先后,可能没拦住;现在位置固定,不再看运气。
|
|
48
|
+
|
|
49
|
+
- **投影缓存删除屏障的三处缺陷修掉**:第三方也包装同一份缓存时旧屏障会"退役了还被复用"(墓碑拦截静默失效、缓存行可能复活)、复用前不校验、首次使用抛错会留半个包装器。现在退役即摘出、复用先验证、出错就恢复原方法并上报。
|
|
50
|
+
|
|
51
|
+
- **兼容页显示与真实能力对齐**:删除动作的可用性判定以前把本机可用的删除显示成不可用;身份锚点以前在 dev 布局下会比出假的「同一份模块」(界面 ok、doctor 说不一致)。两处都改成与服务端/doctor 同一份依据。
|
|
52
|
+
|
|
53
|
+
- **几处"静默降级"改成看得见**:技能 provider 与注入通道装配失败(以前只落 console)、工作区实体快照写不进去、关域拦截因注册顺序变化而失效 —— 现在都会出现在兼容页的降级项里。
|
|
54
|
+
|
|
55
|
+
- **doctor 多了三项检查**:运行时裸模块名是否仍被宿主暴露;插件挂载心跳(含 12 个 inject 服务名,插件没挂上时唯一的线索);宿主版本与本插件验证过的版本不一致时显式提示。
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
21
59
|
## 0.11.0 - 2026-09-20
|
|
22
60
|
|
|
23
61
|
- **访问令牌那块整个重排了,按作用域拆成两块**:「访问令牌」只管本次启动(状态胶囊 + 解锁框),「令牌管理」只管配置文件 —— 设置/修改、保护开关、删除、清除令牌四行静态清单,位置尺寸统一。所有写配置的操作都在弹窗里完成(要输一次令牌确认),页面不再有会展开收起的东西;成功就是弹窗收起 + 状态行更新,失败错误显示在弹窗内。
|
package/lib/client.js
CHANGED
|
@@ -89,6 +89,16 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
|
|
|
89
89
|
.dsm-summary{display:grid;grid-template-columns:repeat(var(--dsm-stat-cols,3),minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dsm-stat:last-child{border-right:0}.dsm-stat strong,.dsm-stat-num{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dsm-filters{display:flex;gap:9px}.dsm-search{flex:1}.dsm-source-filter{width:210px;flex:none}
|
|
90
90
|
.dsm-control,.dsm-select-trigger{box-sizing:border-box;width:100%;min-height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dsm-control::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dsm-control{min-height:160px;padding-top:9px;resize:vertical;line-height:20px}.dsm-select{position:relative}.dsm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dsm-select-menu{position:absolute;z-index:40;top:calc(100% + 5px);right:0;left:0;display:flex;max-height:260px;padding:5px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dsm-option{padding:8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dsm-option:hover,.dsm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
|
|
91
91
|
.dsm-sources{display:flex;flex-direction:column;gap:9px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover,.dsm-row:hover,.dsm-trash-row:hover,.dsm-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:14px;font-weight:650;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px;flex:none}.dsm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsm-source-actions{display:flex;align-items:center;gap:6px;margin-left:8px}.dsm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}.dsm-source-note{padding:9px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
|
|
92
|
+
/* ── 子智能体行:人设名与描述分行(用户 2026-09-22 报)──────────────────────────
|
|
93
|
+
原来名字与描述同在 .dsm-source-head-main 那一条**横向** flex 里、各自单行省略,
|
|
94
|
+
右侧还压着开关和两颗按钮 —— 名字被挤到只剩几个字符(截图里显示成 "co...")。
|
|
95
|
+
改成纵向两行:**名字独占一行、仍单行省略**(全名靠 title),描述另起一行、单行省略。
|
|
96
|
+
只作用于本页:.dsm-source-head-main 还被技能 / 记忆 / MCP / 会话四页的分组头共用,
|
|
97
|
+
那些行没有描述,不动。同权重靠书写顺序取胜,所以必须排在上面那条 .dsm-source 规则之后。 */
|
|
98
|
+
.dsm-persona-main{box-sizing:border-box;min-height:48px;padding:6px 0;flex-direction:column;align-items:flex-start;justify-content:center;gap:0}
|
|
99
|
+
.dsm-persona-name-row{display:flex;width:100%;min-width:0;align-items:center;gap:7px}
|
|
100
|
+
.dsm-persona-name-row .dsm-tag{flex:none}
|
|
101
|
+
.dsm-persona-desc{width:100%;margin-top:1px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
92
102
|
.dsm-table-head,.dsm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dsm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-row:last-child{border-bottom:0}.dsm-main{min-width:0}.dsm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dsm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dsm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dsm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dsm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dsm-enabled{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dsm-disabled{color:var(--dsm-warn);font-size:12px;white-space:nowrap}.dsm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dsm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
|
|
93
103
|
.dsm-switch{position:relative;width:34px;height:20px;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}.dsm-switch:after{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dsm-switch-on{background:var(--dsw-alias-state-success-primary)}.dsm-switch-on:after{transform:translateX(14px)}.dsm-switch:disabled{opacity:.45;cursor:default}.dsm-trash-row{display:flex;min-height:48px;align-items:center;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;font-size:13px;cursor:pointer}.dsm-trash-count{margin-left:auto;padding:2px 7px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px}.dsm-empty{padding:25px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dsm-feedback{padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-undo{display:flex;align-items:center;gap:10px}.dsm-undo-text{min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsm-undo-close{padding:0;border:0;background:transparent;font:inherit;font-size:18px;cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary)}.dsm-undo-close:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsm-undo.dsm-warning{background:var(--dsm-warn-bg)}.dsm-undo.dsm-warning .dsm-undo-text{font-weight:570}.dsm-undo.dsm-warning .dsm-undo-close{color:var(--dsm-warn)}.dsm-warning{border-color:var(--dsm-warn);color:var(--dsm-warn)}.dsm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
94
104
|
.dsm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dsm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dsm-modal-sm,.dsm-modal-import{width:min(480px,100%)!important}.dsm-modal-md{width:min(560px,100%)!important}.dsm-modal-lg,.dsm-modal-wide{width:min(720px,100%)!important}.dsm-modal-list{width:min(560px,100%)!important;height:min(640px,calc(100vh - 48px))!important;max-height:none!important;overflow:hidden}.dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:10px}.dsm-modal-list .dsm-modal-body{flex:1;overflow:auto;overscroll-behavior:contain}.dsm-modal-import{padding:24px}.dsm-modal-head{display:flex;align-items:flex-start;gap:12px}
|
|
@@ -770,6 +780,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
770
780
|
try { fn() } catch (e) { /* 单个订阅者出错不影响其它 */ }
|
|
771
781
|
}
|
|
772
782
|
}
|
|
783
|
+
/** 兼容页「功能总览」的行内跳转:跳到该功能所在的页签(注册表在 ToolsSection 里,见上)。 */
|
|
784
|
+
function jumpToTab(tab) {
|
|
785
|
+
try { if (navigateToTab) navigateToTab(tab) } catch (e) { /* 跳转失败不影响本页 */ }
|
|
786
|
+
}
|
|
773
787
|
function consumeTokenFocus() {
|
|
774
788
|
const pending = tokenFocusPending
|
|
775
789
|
tokenFocusPending = false
|
|
@@ -807,9 +821,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
807
821
|
routes.archive = ok('workspace.archive-native') ? 'native' : (ok('workspace.enqueue') && ok('workspace.set-state') ? 'adapter' : 'none')
|
|
808
822
|
routes.unarchive = ok('workspace.unarchive-native') ? 'native' : (ok('workspace.enqueue') && ok('workspace.set-state') ? 'adapter' : 'none')
|
|
809
823
|
routes.batch = ok('workspace.batch-native') ? 'native' : (ok('workspace.enqueue') && ok('workspace.set-state') ? 'adapter' : 'none')
|
|
824
|
+
// 与服务端 OPERATION_ROUTES 同源(src/compat/probe.ts):
|
|
825
|
+
// · delete 不要求 `projection.delete-native` —— 宿主自带删除屏障是可选槽位,
|
|
826
|
+
// 要求它会把 rc.2 上的删除显示成不可用(服务端有意不要求);
|
|
827
|
+
// · 但要求 `projection.table-delete` —— 那是运行时的硬前提(表不可删就没法安全删行)。
|
|
810
828
|
routes.delete = ok('workspace.delete-native') ? 'native'
|
|
811
829
|
: (ok('workspace.enqueue') && ok('workspace.set-state') && ok('workspace.index-header')
|
|
812
|
-
&& ok('sessions.detach-live') && ok('sessions.cold-announce') && ok('projection.
|
|
830
|
+
&& ok('sessions.detach-live') && ok('sessions.cold-announce') && ok('projection.write')
|
|
831
|
+
&& ok('projection.table-delete') ? 'adapter' : 'none')
|
|
813
832
|
return routes
|
|
814
833
|
}
|
|
815
834
|
|
|
@@ -825,6 +844,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
825
844
|
const [live, setLive] = React.useState(null)
|
|
826
845
|
const [liveOpen, setLiveOpen] = React.useState('')
|
|
827
846
|
const [liveCopied, setLiveCopied] = React.useState(false)
|
|
847
|
+
// 功能总览(B6):按功能点看"现在到底能不能用"。数据来自 feature-overview(只读聚合)。
|
|
848
|
+
const [features, setFeatures] = React.useState(null)
|
|
828
849
|
const [busy, setBusy] = React.useState(false)
|
|
829
850
|
const [error, setError] = React.useState(null)
|
|
830
851
|
// 访问令牌(用户裁定 2026-09-18:入口放兼容页)。它不属于"宿主体检",但**必须有一个
|
|
@@ -1062,6 +1083,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1062
1083
|
.then(function (r) { if (alive !== false && r && r.ok) setLive(r) })
|
|
1063
1084
|
.catch(function () { /* 读不到 → 不渲染这一节 */ })
|
|
1064
1085
|
}
|
|
1086
|
+
// 功能总览(B6):服务端按功能点聚合(装配层 + 宿主能力层 + 用户配置层)。
|
|
1087
|
+
// 它是只读的派生视图,取不到就不渲染这一节,绝不影响上面那条结论。
|
|
1088
|
+
const loadFeatures = function (alive) {
|
|
1089
|
+
apiCall('feature-overview', {})
|
|
1090
|
+
.then(function (r) { if (alive !== false && r && r.ok) setFeatures(r) })
|
|
1091
|
+
.catch(function () { /* 同上 */ })
|
|
1092
|
+
}
|
|
1065
1093
|
React.useEffect(function () {
|
|
1066
1094
|
let alive = true
|
|
1067
1095
|
setBusy(true)
|
|
@@ -1071,6 +1099,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1071
1099
|
loadReach(alive)
|
|
1072
1100
|
loadInject(alive)
|
|
1073
1101
|
loadLive(alive)
|
|
1102
|
+
loadFeatures(alive)
|
|
1074
1103
|
loadToken(alive)
|
|
1075
1104
|
loadBackups(alive)
|
|
1076
1105
|
return function () { alive = false }
|
|
@@ -1083,6 +1112,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1083
1112
|
loadReach(true)
|
|
1084
1113
|
loadInject(true)
|
|
1085
1114
|
loadLive(true)
|
|
1115
|
+
loadFeatures(true)
|
|
1086
1116
|
loadToken(true)
|
|
1087
1117
|
loadBackups(true)
|
|
1088
1118
|
}
|
|
@@ -1599,6 +1629,41 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1599
1629
|
: (usableCount === findings.length ? t('compat.caps.all') : t('compat.caps.substituted')),
|
|
1600
1630
|
blockedCount > 0)))
|
|
1601
1631
|
|
|
1632
|
+
// 版本栅栏(B3):实际宿主与本插件验证过的版本不同时说出来 —— 否则"照旧版本的印象
|
|
1633
|
+
// 判断"会让人把探测结果当成 bug。只提示、不拦(peer 保持无上界是既定决策)。
|
|
1634
|
+
const hostVersion = (data.host && data.host.version) || ''
|
|
1635
|
+
if (hostVersion && data.verifiedVersion && hostVersion !== data.verifiedVersion) {
|
|
1636
|
+
push(React.createElement('p', { className: 'dsm-help' },
|
|
1637
|
+
t('compat.host.mismatch')
|
|
1638
|
+
.replace('{host}', String(hostVersion))
|
|
1639
|
+
.replace('{verified}', String(data.verifiedVersion))))
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// 功能总览(B6):一行一个功能点,状态 = 装配层 + 宿主能力层 + 用户配置层 的合成
|
|
1643
|
+
// (服务端聚合,见 feature-overview)。行可点,跳到该功能所在的页签。
|
|
1644
|
+
if (features && Array.isArray(features.rows) && features.rows.length) {
|
|
1645
|
+
push(section(t('compat.features'), t('compat.features.hint'),
|
|
1646
|
+
React.createElement('div', { className: 'dsm-compat-mod-list dsm-compat-cards' },
|
|
1647
|
+
features.rows.map(function (row) {
|
|
1648
|
+
const cls = row.state === 'ok' ? 'dsm-compat-pill-ok'
|
|
1649
|
+
: (row.state === 'disabled' || row.state === 'locked') ? ''
|
|
1650
|
+
: 'dsm-compat-pill-warn'
|
|
1651
|
+
return React.createElement('div', {
|
|
1652
|
+
className: 'dsm-compat-mod-row' + (row.tab ? ' dsm-compat-row-link' : ''),
|
|
1653
|
+
key: row.key,
|
|
1654
|
+
role: row.tab ? 'button' : undefined,
|
|
1655
|
+
tabIndex: row.tab ? 0 : undefined,
|
|
1656
|
+
onClick: row.tab ? function () { jumpToTab(row.tab) } : undefined,
|
|
1657
|
+
},
|
|
1658
|
+
React.createElement('span', { className: 'dsm-compat-name' }, row.label),
|
|
1659
|
+
React.createElement('span', { className: 'dsm-compat-pill ' + cls },
|
|
1660
|
+
t('compat.feature.state.' + String(row.state || 'unknown'))),
|
|
1661
|
+
React.createElement('span', { className: 'dsm-compat-mod dsm-compat-mod-self' },
|
|
1662
|
+
React.createElement('span', { className: 'dsm-compat-label' }, t('compat.reason')),
|
|
1663
|
+
row.detail))
|
|
1664
|
+
}))))
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1602
1667
|
// 阻塞项放最前:这是唯一"必须动手"的东西。
|
|
1603
1668
|
if (blockers.length) {
|
|
1604
1669
|
push(section(t('compat.blockers'), null,
|
|
@@ -1635,6 +1700,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1635
1700
|
React.createElement('span', { className: 'dsm-compat-pill dsm-compat-pill-warn' },
|
|
1636
1701
|
item.fallback === 'native-entry' ? t('compat.fallback.native')
|
|
1637
1702
|
: item.fallback === 'refuse-operation' ? t('compat.fallback.refuse')
|
|
1703
|
+
: item.fallback === 'inform-only' ? t('compat.fallback.inform')
|
|
1638
1704
|
: t('compat.fallback.blocked')),
|
|
1639
1705
|
React.createElement('span', { className: 'dsm-compat-mod dsm-compat-mod-self' },
|
|
1640
1706
|
React.createElement('span', { className: 'dsm-compat-label' }, t('compat.reason')),
|
|
@@ -2112,9 +2178,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2112
2178
|
"compat.degraded": "已降级能力", "compat.degraded.hint": "缺什么 + 后果", "compat.reason": "原因:",
|
|
2113
2179
|
"compat.others": "其它能力项", "compat.others.hint": "不算降级:宿主没有这些入口时,由插件自己顶上",
|
|
2114
2180
|
"compat.fallback.substituted": "插件适配层(宿主无此入口)",
|
|
2115
|
-
"compat.fallback.native": "改走原生入口", "compat.fallback.refuse": "该操作会被拒绝", "compat.fallback.blocked": "相关按钮已禁用",
|
|
2181
|
+
"compat.fallback.native": "改走原生入口", "compat.fallback.refuse": "该操作会被拒绝", "compat.fallback.blocked": "相关按钮已禁用", "compat.fallback.inform": "不影响写入",
|
|
2116
2182
|
"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
2183
|
"compat.blockers": "阻塞项(按此修复)",
|
|
2184
|
+
"compat.host.mismatch": "实际宿主 {host} 与本插件验证过的 {verified} 不同:能力按实际逐项探测,下面出现降级项时以实际能力为准,不要按旧版本的印象判断。",
|
|
2185
|
+
"compat.features": "功能总览", "compat.features.hint": "一行一个功能点:装配层 + 宿主能力层 + 你当前配置的合成结果;点一行跳到对应页签",
|
|
2186
|
+
"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
2187
|
"compat.reach": "预设注入边界", "compat.reach.hint": "每个预设下这些内容到不到得了模型",
|
|
2119
2188
|
"compat.reach.suppressing": "压制型",
|
|
2120
2189
|
"compat.inject": "注入", "compat.inject.hint": "本插件注入给模型的内容(不变不重发)",
|
|
@@ -2456,9 +2525,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2456
2525
|
"compat.degraded": "Degraded capabilities", "compat.degraded.hint": "what is missing + the consequence", "compat.reason": "Reason:",
|
|
2457
2526
|
"compat.others": "Other capability slots", "compat.others.hint": "not degraded: the plugin supplies its own implementation when the host has no such entry",
|
|
2458
2527
|
"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",
|
|
2528
|
+
"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
2529
|
"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
2530
|
"compat.blockers": "Blockers (fix these)",
|
|
2531
|
+
"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.",
|
|
2532
|
+
"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",
|
|
2533
|
+
"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
2534
|
"compat.reach": "Preset injection reach", "compat.reach.hint": "whether these contents reach the model under each preset",
|
|
2463
2535
|
"compat.reach.suppressing": "suppressing",
|
|
2464
2536
|
"compat.inject": "Injection", "compat.inject.hint": "what this plugin injects for the model (unchanged text is not resent)",
|
|
@@ -2896,6 +2968,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2896
2968
|
* 令牌没验过照样打得开(见 request-gate.ts 的白名单)。
|
|
2897
2969
|
*/
|
|
2898
2970
|
let tokenBanner = null
|
|
2971
|
+
// 文本节点单独用闭包变量持有,不挂在 DOM 节点上(`tokenBanner._text` 是把节点当
|
|
2972
|
+
// 储物格,客户端唯一的类型检查会报 TS2339)。横幅只创建一次、从不复位,两者同生命周期。
|
|
2973
|
+
let tokenBannerText = null
|
|
2899
2974
|
// 手动关闭标记:横幅固定在视口顶部,会盖在宿主设置窗口的标题区上 —— 给一个出口,
|
|
2900
2975
|
// 关掉后本次锁态期间不再出现;解锁(locked 变 false)即重置,下次再锁照常出现。
|
|
2901
2976
|
// 输入框上的锁与行内说明不受影响,信息不丢。
|
|
@@ -2914,10 +2989,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2914
2989
|
tokenBanner.setAttribute('role', 'status')
|
|
2915
2990
|
// 文案与关闭键分开挂:下面语言切换是往文本节点整体覆写,textContent 写在
|
|
2916
2991
|
// 根节点上会把关闭键抹掉。
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
tokenBanner.appendChild(
|
|
2920
|
-
tokenBanner._text = bannerText
|
|
2992
|
+
tokenBannerText = document.createElement('span')
|
|
2993
|
+
tokenBannerText.className = 'dsm-token-banner-text'
|
|
2994
|
+
tokenBanner.appendChild(tokenBannerText)
|
|
2921
2995
|
const bannerClose = document.createElement('button')
|
|
2922
2996
|
bannerClose.type = 'button'
|
|
2923
2997
|
bannerClose.className = 'dsm-token-banner-close'
|
|
@@ -2931,7 +3005,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2931
3005
|
document.body.appendChild(tokenBanner)
|
|
2932
3006
|
}
|
|
2933
3007
|
// 语言切换后文案要跟着变(锁态没变、setTokenLock 不会通知,见下面的 locale 订阅)。
|
|
2934
|
-
|
|
3008
|
+
tokenBannerText.textContent = t('compat.token.banner')
|
|
2935
3009
|
tokenBanner.style.display = tokenBannerDismissed ? 'none' : ''
|
|
2936
3010
|
} catch (e) { /* DOM 不可用 → 只是少一条说明,不影响门禁本身 */ }
|
|
2937
3011
|
}
|
|
@@ -7489,10 +7563,12 @@ function enabledMemoryCount(rules) {
|
|
|
7489
7563
|
: React.createElement('div', { className: 'dsm-sources', ref: flipRef }, orderedSubagents.map(function (p) {
|
|
7490
7564
|
return React.createElement('div', { key: p.name, className: 'dsm-source', 'data-flip-key': p.name, 'data-flip-on': p.enabled !== false ? '1' : '0' },
|
|
7491
7565
|
React.createElement('div', { className: 'dsm-source-head' },
|
|
7492
|
-
React.createElement('div', { className: 'dsm-source-head-main' },
|
|
7493
|
-
React.createElement('
|
|
7494
|
-
|
|
7495
|
-
|
|
7566
|
+
React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
|
|
7567
|
+
React.createElement('div', { className: 'dsm-persona-name-row' },
|
|
7568
|
+
React.createElement('span', { className: 'dsm-source-title', title: p.name }, p.name),
|
|
7569
|
+
p.enabled === false ? React.createElement('span', { className: 'dsm-tag', title: t('subagents.disabled.hint') }, t('subagents.disabled')) : null),
|
|
7570
|
+
// 没有描述就不渲染这一行:空 div 仍占一行行高,行会变成一条无内容的空隙。
|
|
7571
|
+
p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
|
|
7496
7572
|
React.createElement('div', { className: 'dsm-source-actions' },
|
|
7497
7573
|
React.createElement(Switch, { on: p.enabled !== false, disabled: busy || anyLocked, label: t('subagents.toggle') + ' ' + p.name, onClick: function () { togglePersona(p) } }),
|
|
7498
7574
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || anyLocked, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
|
|
@@ -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
|
+
}
|