dsh-plugin-tool-management 0.20.3 → 0.21.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -1
  3. package/README_EN.md +4 -1
  4. package/docs/update.md +1 -0
  5. package/lib/client.js +117 -11
  6. package/lib/compat/capability-specs.js +60 -1
  7. package/lib/compat/host-contracts.js +72 -0
  8. package/lib/compat/host-probe.js +59 -0
  9. package/lib/compat/host-roots.js +1 -1
  10. package/lib/compat/probe.js +84 -13
  11. package/lib/context-inject-frame.js +134 -26
  12. package/lib/context-inject.js +12 -9
  13. package/lib/http-api.js +3 -2
  14. package/lib/http-fence.js +26 -1
  15. package/lib/index.js +5 -2
  16. package/lib/mcp/manager.js +6 -1
  17. package/lib/mcp/state-section.js +75 -34
  18. package/lib/memories/constants.js +39 -7
  19. package/lib/memories/projection.js +18 -18
  20. package/lib/memories/service.js +8 -7
  21. package/lib/memories/snapshot.js +27 -17
  22. package/lib/ops/compat.js +64 -2
  23. package/lib/ops/memory.js +2 -2
  24. package/lib/ops/prompts.js +30 -2
  25. package/lib/request-gate.js +5 -4
  26. package/lib/skills/catalog.js +2 -2
  27. package/lib/subagents/catalog.js +4 -4
  28. package/lib/subagents/child-preset.js +120 -0
  29. package/lib/subagents/persona-parse.js +9 -9
  30. package/lib/subagents/run-provider.js +218 -62
  31. package/lib/subagents/tool-filter.js +12 -12
  32. package/lib/subagents/tools.js +29 -22
  33. package/lib/tools/deps.js +4 -4
  34. package/lib/tools/mcp.js +34 -34
  35. package/lib/tools/memory.js +34 -34
  36. package/lib/tools/prompt.js +5 -5
  37. package/lib/tools/remove.js +17 -17
  38. package/lib/tools/scene.js +66 -66
  39. package/lib/tools/skills.js +21 -21
  40. package/lib/tools/subagent.js +13 -13
  41. package/lib/tools/table.js +2 -2
  42. package/package.json +25 -24
@@ -0,0 +1,72 @@
1
+ // 宿主**非 peer** 契约的共享清单:运行时探测(compat/probe.ts 的 runtime-only 上报)与
2
+ // `scripts/doctor.mjs` 都要读它们,所以只有这一份。
3
+ //
4
+ // 为什么单独一层:这些包**不在**本插件的 `peerDependencies` 里 —— 宿主不要求插件依赖它们,
5
+ // 但插件在运行时按**裸模块名**直接 require(MCP 客户端、人设、全局指令、技能目录)。它们有
6
+ // 两个共同点让它们必须被看见:
7
+ // (1) 官方改名或撤掉其中一个,对应功能**静默降级**(不抛错,只是少一半);
8
+ // (2) 它们不参与模块身份比对(不是硬依赖),所以"两份不同拷贝"那一栏根本看不到它们。
9
+ //
10
+ // 此前这份清单只活在 doctor 里:命令行看得到,兼容页看不到。而兼容页正是用户升级 DSH 后
11
+ // 第一眼看的界面 —— 于是同一个"官方把某个包撤了"的事实,CLI 报得出来、界面报不出来。
12
+ // 提升到这里之后两边读同一份(doctor 从 lib/ import,不再自己维护一份)。
13
+ /**
14
+ * `~/.dsh/AGENTS.md` 的**增量刷新**契约 —— 本插件"应用提示词预设"依赖它。
15
+ *
16
+ * 插件把预设正文写进全局 `AGENTS.md`,然后靠官方**每一步**重新读盘(对比 stat 的 version 与
17
+ * 内容 SHA-1)决定要不要把新正文送进模型。官方一旦改成事件驱动、加 touchedPaths 白名单、或改
18
+ * digest 口径,"应用预设"就会**静默失效**(旧正文继续注入),零报错。
19
+ */
20
+ export const AGENTS_MD_REFRESH_PACKAGE = '@deepseek-ai/dsh-agent-instructions';
21
+ export const AGENTS_MD_REFRESH_MARKERS = [
22
+ { needle: 'agentInstructionsHook', why: '每步重读全局 AGENTS.md 的钩子入口' },
23
+ { needle: 'trimmedInstructionDigest', why: '内容同一性 = 去掉首尾空白后的 SHA-1' },
24
+ { needle: 'instructionContentSha1', why: 'SHA-1 摘要口径' },
25
+ { needle: 'sameInstructionChange', why: '按 {action, scope, path, digest} 判「有没有变化」' },
26
+ ];
27
+ /**
28
+ * 运行时按**裸模块名** require 的宿主包,以及每个包守着的契约。
29
+ *
30
+ * 与 `AGENTS_MD_REFRESH_MARKERS` 的分工:那一份是"同一个包内的若干标识符",这一份是
31
+ * "若干不同的包各要求什么"。合起来才是"官方撤掉了什么"的完整清单。
32
+ */
33
+ export const BARE_HOST_MODULE_CONTRACTS = [
34
+ {
35
+ package: '@deepseek-ai/dsh-mcp-client',
36
+ feature: 'MCP 客户端(MCP 服务器装配的宿主出口)',
37
+ markers: [],
38
+ },
39
+ {
40
+ package: '@deepseek-ai/dsh-persona',
41
+ feature: '人设(子代理人格的读取与解析)',
42
+ markers: [],
43
+ },
44
+ {
45
+ package: AGENTS_MD_REFRESH_PACKAGE,
46
+ feature: 'AGENTS.md 注入(应用提示词预设)',
47
+ markers: AGENTS_MD_REFRESH_MARKERS,
48
+ },
49
+ {
50
+ package: '@deepseek-ai/dsh-tool-skill',
51
+ feature: '技能目录(官方技能挂载的宿主出口)',
52
+ markers: [],
53
+ },
54
+ ];
55
+ /** doctor 的 CLI 用:裸模块名清单(顺序与上面一致)。 */
56
+ export const BARE_HOST_MODULE_NAMES = BARE_HOST_MODULE_CONTRACTS.map((item) => item.package);
57
+ /**
58
+ * 官方 JSONL 会话目录布局的 canary —— 历史适配器(`src/sessions/workspace.ts` →
59
+ * `jsonlSessionDirectory`)硬编码了它。
60
+ *
61
+ * 这些是**行为常量**,不是装饰:任何一个变了,适配器就再也不能确认某个转写目录属于它正要删的
62
+ * 那个会话,删除会退化成"只删工件、保留父目录",而唯一征兆只是插件侧一行日志。那种退化被刻意
63
+ * 做成非致命(工件仍然删掉,且删后经 `persistence.stat` 复核),所以对着一台没活着的宿主做静态
64
+ * 检查,是用户在自己翻出孤儿目录之前唯一能知道漂移的办法。
65
+ */
66
+ export const JSONL_LAYOUT_PACKAGE = '@deepseek-ai/dsh-session-persistence-jsonl';
67
+ export const JSONL_LAYOUT_MARKERS = [
68
+ { needle: 'session-persistence-jsonl', why: '适配器的后端名门槛' },
69
+ { needle: '_no-cwd', why: '会话头没有 cwd 时落进的桶' },
70
+ { needle: '251', why: '项目段截断长度' },
71
+ { needle: 'padStart(4', why: '~XXXX UTF-16 转义编码' },
72
+ ];
@@ -177,12 +177,18 @@ const SESSIONS_CLASS = { pkg: '@deepseek-ai/dsh-session', target: 'SessionStore'
177
177
  // (dsh-settings 导出 SettingsForms;dsh-agent-preset-registry 导出 AgentPresetRegistry)。
178
178
  const SETTINGS_CLASS = { pkg: '@deepseek-ai/dsh-settings', target: 'SettingsForms' };
179
179
  const ROSTER_CLASS = { pkg: '@deepseek-ai/dsh-agent-preset-registry', target: 'AgentPresetRegistry' };
180
+ // 0.2.1 的两块契约面(tools.restrict / connection.admit)也都声明在官方类上,所以同样接入
181
+ // 「参考副本 + 文本比对」:官方改了实现,比对的结论从"一致"变成"漂移",界面上看得见。
182
+ const TOOLS_CLASS = { pkg: '@deepseek-ai/dsh-tools', target: 'ToolRuntime' };
183
+ const CONNECTION_CLASS = { pkg: '@deepseek-ai/dsh-client-connection', target: 'HostConnectionService' };
180
184
  export const OWNER_CLASSES = {
181
185
  workspace: WORKSPACE_CLASS,
182
186
  projectionCache: PROJECTION_CLASS,
183
187
  sessions: SESSIONS_CLASS,
184
188
  settings: SETTINGS_CLASS,
185
189
  presetRoster: ROSTER_CLASS,
190
+ tools: TOOLS_CLASS,
191
+ connection: CONNECTION_CLASS,
186
192
  };
187
193
  /** The named export's prototype, or undefined when the package is absent/trimmed. */
188
194
  function classPrototypeOf(ref) {
@@ -257,5 +263,58 @@ export function referencePrototypes() {
257
263
  const roster = classPrototypeOf(ROSTER_CLASS);
258
264
  if (roster !== undefined)
259
265
  out.roster = roster;
266
+ // 0.2.1 的两块:`dsh-tools` 是本插件的 peerDependency(参考副本拿得到);
267
+ // `dsh-client-connection` **不在** peerDependencies 里(宿主不要求插件直接依赖它),
268
+ // 所以参考副本常态取不到 —— textMatch 记 undefined,那条能力退化成纯存在性检查。
269
+ // 这是如实的能力边界:能验"admit 在不在",验不了"实现漂没漂"。
270
+ const tools = classPrototypeOf(TOOLS_CLASS);
271
+ if (tools !== undefined)
272
+ out.tools = tools;
273
+ const connection = classPrototypeOf(CONNECTION_CLASS);
274
+ if (connection !== undefined)
275
+ out.connection = connection;
260
276
  return out;
261
277
  }
278
+ /**
279
+ * 从**宿主侧**能不能解析到一个包(不 import,只看解析)。
280
+ *
281
+ * 为什么需要它:有一部分官方包**不是**本插件的 peer(宿主不要求插件依赖它们),插件自己的
282
+ * `node_modules` 里就没有 —— `safeResolve`/`classPrototypeOf` 一律取不到。运行时探测不看
283
+ * 插件侧(它看的是 ctx 上的活服务),所以只有 doctor 这种"没有活宿主"的静态视图会被误导:
284
+ * 它从插件锚点解析,解析不到就把一条**运行时确实存在**的能力记成 `missing`
285
+ * (2026-10-17 `connection.admit` 就这么误报过:`dsh-client-connection` 不在插件依赖图里,
286
+ * 而它承载的 `admit()` 在 0.2.1 的 Web 组合里一定有)。
287
+ *
288
+ * 判据必须是宿主侧:逐个宿主安装锚点试着解析(纯 `require.resolve`,不 import),只要有
289
+ * **一个**能解析到就算能查。与 `safeResolve` 的分工:那个回答"本插件能不能 import 它",
290
+ * 这个回答"宿主装不装它" —— 后者才是静态能力表该用的口径。
291
+ */
292
+ export function resolvableFromHost(specifier) {
293
+ for (const anchor of hostResolveAnchors()) {
294
+ try {
295
+ createRequire(anchor).resolve(specifier);
296
+ return true;
297
+ }
298
+ catch {
299
+ /* try the next anchor */
300
+ }
301
+ }
302
+ return false;
303
+ }
304
+ /**
305
+ * 宿主安装的解析锚点(`package.json` 路径,按候选顺序)。
306
+ *
307
+ * 与 probe.ts 的 `hostPackageRoot()` 共用同一份 `hostRootCandidates()`,但这里**不要求**
308
+ * `dsh/package.json` 在场:即使那一层只剩空壳(0.1.5 时代的悬空 junction 树,本机正是如此),
309
+ * 它下面的 `node_modules/@deepseek-ai/<pkg>` 仍可能解析得到。判据各取所需 —— probe.ts 要
310
+ * "像不像一份完整安装",这里只问"这个包从这里能不能解析到"。
311
+ */
312
+ function hostResolveAnchors() {
313
+ return hostRootCandidatesFn().map((dir) => join(dirname(dir), 'package.json'));
314
+ }
315
+ // `hostRootCandidates()` 的注入点:由 probe.ts 在模块加载时填进来。不反向 import 是为了
316
+ // 不成环(probe.ts → host-probe.ts → probe.ts),而"宿主在哪"的口径仍然只有一份。
317
+ let hostRootCandidatesFn = () => [];
318
+ export function provideHostRootCandidates(fn) {
319
+ hostRootCandidatesFn = fn;
320
+ }
@@ -20,7 +20,7 @@ export const IDENTITY_PACKAGES = [
20
20
  '@deepseek-ai/dsh-storage-domain',
21
21
  '@deepseek-ai/dsh-spill-local',
22
22
  ];
23
- export const VERIFIED_HOST_VERSION = '0.2.0-rc.2';
23
+ export const VERIFIED_HOST_VERSION = '0.2.1-rc.2';
24
24
  export const OPERATION_ROUTES = {
25
25
  archive: { native: ['workspace.archive-native'], adapter: ['workspace.enqueue', 'workspace.set-state'] },
26
26
  unarchive: { native: ['workspace.unarchive-native'], adapter: ['workspace.enqueue', 'workspace.set-state'] },
@@ -30,17 +30,23 @@
30
30
  * leave the rest working.
31
31
  */
32
32
  import { createRequire } from 'node:module';
33
- import { existsSync } from 'node:fs';
33
+ import { existsSync, readFileSync } from 'node:fs';
34
34
  import { dirname, join } from 'node:path';
35
35
  import { fileURLToPath } from 'node:url';
36
36
  import { CAPABILITY_SPECS } from './capability-specs.js';
37
- import { isAsarPath, isFn, OWNER_CLASSES, realPathWithReason, referencePrototypes, safeResolve, textMatchOf, versionOf, versionOfPackageAt, } from './host-probe.js';
37
+ import { BARE_HOST_MODULE_CONTRACTS } from './host-contracts.js';
38
+ import { isAsarPath, isFn, OWNER_CLASSES, realPathWithReason, referencePrototypes, provideHostRootCandidates, safeResolve, textMatchOf, versionOf, versionOfPackageAt, } from './host-probe.js';
38
39
  // 宿主安装根的发现与「该走哪条恢复路」的判定 2026-10-01 整段搬到 ./host-roots.js,正文一字
39
40
  // 未改(moved-verify 逐字对拍)。probe.js 的公开面照旧:搬走的那 12 件全部从这里再出口 ——
40
41
  // index.ts / ops/compat.ts / sessions/bridge.ts 都按 ./compat/probe.js 引它们,少一件 equiv
41
42
  // 对拍就报「导出面丢失」。
42
43
  export { asarHostPackageRoot, findingsById, hostRootCandidates, IDENTITY_PACKAGES, npxCacheHostRoots, OPERATION_ROUTES, recoveryFor, refusalsFor, routeFor, VERIFIED_HOST_VERSION, } from './host-roots.js';
43
44
  import { hostRootCandidates, IDENTITY_PACKAGES, routeFor, VERIFIED_HOST_VERSION } from './host-roots.js';
45
+ // 「宿主在哪」这份口径只有一份(host-roots.ts 的 hostRootCandidates)。静态能力表里有条目要
46
+ // 按宿主侧解析结果决定**这一轮能不能查**(`staticWhen`,见 connection.admit 那条),而
47
+ // capability-specs.ts 不能反向 import 本文件(probe → specs → probe 成环)。所以在这里把
48
+ // 函数本体注入 host-probe.ts:它只调用、不 import,加载顺序天然正确(本文件先被求值)。
49
+ provideHostRootCandidates(hostRootCandidates);
44
50
  /** Packages whose physical module identity matters to this plugin. */
45
51
  /**
46
52
  * 本插件是不是从**源码检出**跑起来的(仓库里那份,`scripts/host-deps.mjs` 就躺在旁边)。
@@ -63,9 +69,15 @@ export function isSourceInstall() {
63
69
  * `0.1.8-rc.1` / `0.2.1-rc.1` / `0.3.0-rc.1` 全部 false)—— 范围只能**逐代枚举并集**,
64
70
  * 每适配一代新 rc 就补一段(见 README 的升级清单)。
65
71
  *
66
- * 上界刻意不设:宿主跨代升级由**运行时能力探测**兜底(`assessHost` 逐项探测并降级),
67
- * 而不是在安装期拒绝整包。所以这里用不带上界的 `>=`,**不用** `^`(`^0.2.0` 的隐含上界
68
- * `<0.3.0` 会在宿主发稳定版 0.3.0 时把插件挡在门外 —— 与"不设上界"自相矛盾)。
72
+ * 每一代各自**带上界**(0.1.7 → `<0.2.0-rc.1`、0.2.0 → `<0.2.1-rc.2`、
73
+ * 0.2.1 → `<0.2.2`):宿主跨 minor 升级时不给"装进去再说",而是**主动发一版**。新的 minor
74
+ * 已经动过 SubagentStartRequest、身份比对这些会静默改行为的契约,旧适配继续跑在一份新宿主上,
75
+ * 失败会以"功能莫名不工作"的形式出现 —— 那时用户无法判断是插件问题还是版本问题。装上被拒 +
76
+ * 一版新适配,优于装上然后部分功能悄悄失效。
77
+ * 上界**必须逐段写**:只在最后一段挂 `<0.2.2` 是无效的 —— `>=0.2.0-rc.1` 那一支没有上界,
78
+ * 并集仍会匹配 0.3.0 / 1.0.0(实测:那种写法下 0.3.0 satisfies 为 true)。`^` 更不能用:
79
+ * `^0.2.0` 的隐含上界是 `<0.3.0`,而 0.2.1-rc.2 这个已适配版本自己都满足不了它(prerelease
80
+ * 只在同元组比较器带 prerelease 时才匹配)。
69
81
  *
70
82
  * 2026-09-30 审查 §5 F9 + §5.4:这一段此前与 `package.json` 的 peerDependencies **漂移**成
71
83
  * 两种写法(这里是 `^` 三段、那里是 `>=` 两段,且下界不同)。现在两处是**同一个字符串**,
@@ -80,16 +92,13 @@ export function isSourceInstall() {
80
92
  * 0.2.0-rc.2(桌面版首发)已实测通过:`assessHost` 全通过,宿主版本读得出来,
81
93
  * 归档宿主的身份判定见 `hostPackageRoot` / `isAsarPath`。所以它同时进范围与「已验证」。
82
94
  */
83
- export const EXPECTED_PEER_RANGE = '>=0.1.7-rc.2 || >=0.2.0-rc.1';
95
+ export const EXPECTED_PEER_RANGE = '>=0.1.7-rc.2 <0.2.0-rc.1 || >=0.2.0-rc.1 <0.2.1-rc.2 || >=0.2.1-rc.2 <0.2.2';
84
96
  /** The release this plugin's adapters were last verified against. */
85
97
  /**
86
- * peer range 的下界。
98
+ * peer range 的下界。仍从 EXPECTED_PEER_RANGE 派生,避免两处手写漂移。
87
99
  *
88
- * 刻意不带 semver 上界:官方持续发版,硬上界会在宿主跨 minor 升级时直接挡住
89
- * 插件安装(pnpm peer 校验失败)。上界改为由**运行时能力探测**兜底 ——
90
- * 宿主版本高于已验证范围时,`assessHost` 会逐项探测并把缺失能力降级/禁用,
91
- * 而不是在安装期拒绝整包。
92
- * 从 EXPECTED_PEER_RANGE 派生,避免两处手写漂移。
100
+ * 注意它取的是范围里的**最低**那个版本号(`>=0.1.7-rc.2`),不是已验证版本 —— 与上界收敛
101
+ * 不冲突:这条讲的是"最早支持哪一代",上界讲的是"当前适配覆盖到哪一代"。
93
102
  *
94
103
  * 2026-09-29 起**不再出现在界面上**(用户裁定:删掉「最低要求 ≥ …」那一行 —— 它是安装期
95
104
  * 门槛,摆在"当前宿主"卡片下面会被读成对当前宿主的断言)。保留此导出是因为它进了
@@ -223,6 +232,11 @@ export const SUBSTITUTED_CAPABILITIES = CAPABILITY_SPECS
223
232
  export const CAPABILITY_STATIC = CAPABILITY_SPECS.map((spec) => {
224
233
  const ref = OWNER_CLASSES[spec.owner];
225
234
  const methods = spec.methods;
235
+ // `staticWhen` 是"这一轮能不能静态检查"的门槛。它说不的时候,`check` 必须是 null ——
236
+ // 否则 doctor 会从插件锚点解析一个本插件不依赖的包,然后把"解析不到"写成"宿主缺失"
237
+ // (2026-10-17 `connection.admit` 的误报正是这条路径)。null 让它落到 `runtime-only`:
238
+ // 「只能对活的宿主查」是事实,"missing" 不是。
239
+ const checkableByStaticView = spec.staticWhen === undefined || spec.staticWhen();
226
240
  return {
227
241
  id: spec.id,
228
242
  label: spec.label,
@@ -230,7 +244,7 @@ export const CAPABILITY_STATIC = CAPABILITY_SPECS.map((spec) => {
230
244
  optional: spec.optional === true,
231
245
  // A `fields`-only spec (instance Maps) and a spec whose judgement lives in
232
246
  // its `probe` have nothing a prototype can answer — they stay runtime-only.
233
- check: ref !== undefined && methods !== undefined && methods.length > 0
247
+ check: checkableByStaticView && ref !== undefined && methods !== undefined && methods.length > 0
234
248
  ? { pkg: ref.pkg, target: ref.target, members: methods }
235
249
  : null,
236
250
  };
@@ -272,6 +286,15 @@ export function assessHost(ctx) {
272
286
  const registry = unwrap(get('workspaceRegistry'));
273
287
  const cache = unwrap(get('sessionProjectionCache'));
274
288
  const sessions = unwrap(ctx.sessions !== undefined ? ctx.sessions : get('sessions'));
289
+ // tools 走 ctx 属性优先(它在插件 inject 清单里)。它有两层取法:宿主平面的 ctx.tools
290
+ // 与**子代理 scope 里**的那个(restrict 在子 scope 上才作用于该子代理,run-provider.ts
291
+ // 就是 `childAgent.ctx.get('tools')` 取的)。探测只能看宿主平面那一层 —— 子 scope 还没建。
292
+ const tools = unwrap(ctx.tools !== undefined ? ctx.tools : get('tools'));
293
+ // agent 作用域:探测对象是 agent 自己的 ctx(`on` 挂在它上面)。这里没有活的 agent 可看,
294
+ // 所以拿宿主平面的 ctx 代答 —— 同一个类实例链上的成员存在性,`on` 改名时两边一起改。
295
+ // 局限如实标注:探得到"on 在不在",探不到"agent/created 是否仍串行"。
296
+ const agentContext = ctx;
297
+ const connection = unwrap(get('connection'));
275
298
  // settings 走 ctx 属性优先(插件 inject 清单里的正式服务),roster 走服务名查找
276
299
  // ('agentPresets' 与 preset-reach.ts 的 presetRosterOf 同名 —— 注入矩阵实际用的就是它)。
277
300
  const settings = unwrap(ctx.settings !== undefined ? ctx.settings : get('settings'));
@@ -287,6 +310,9 @@ export function assessHost(ctx) {
287
310
  settings: { target: settings, reference: references.settings },
288
311
  presetRoster: { target: roster, reference: references.roster },
289
312
  fs: { target: fsService, reference: undefined },
313
+ tools: { target: tools, reference: references.tools },
314
+ agentContext: { target: agentContext, reference: undefined },
315
+ connection: { target: connection, reference: references.connection },
290
316
  };
291
317
  const findings = CAPABILITY_SPECS.map((spec) => {
292
318
  const slot = targets[spec.owner];
@@ -308,6 +334,8 @@ export function assessHost(ctx) {
308
334
  let pluginRealpathNote = null;
309
335
  let asarHostVersion = null;
310
336
  let asarPluginVersion = null;
337
+ // 宿主非 peer 契约只跟"宿主装在哪"有关,与下面那个逐包 identity 循环无关,先量一次。
338
+ const contracts = hostContractStatus(hostRoot);
311
339
  for (const name of IDENTITY_PACKAGES) {
312
340
  const resolved = safeResolve(name);
313
341
  modules[name] = resolved;
@@ -424,6 +452,49 @@ export function assessHost(ctx) {
424
452
  degraded,
425
453
  mayDelete: delta,
426
454
  generatedAt: Date.now(),
455
+ ...(contracts === null ? {} : { contracts }),
456
+ };
457
+ }
458
+ /**
459
+ * 宿主**非 peer** 契约的实况:插件按裸模块名 require 的那几个官方包,宿主还暴不暴露、
460
+ * 入口里那几条 canary 还在不在。
461
+ *
462
+ * 为什么在探测里量而不是只在 doctor 里量:doctor 是"没有活宿主"的静态视图,而兼容页看到的是
463
+ * 正在跑的宿主。升级 DSH 后第一眼该看见的正是这一块 —— 官方撤掉某个包时**不抛错**,功能只是
464
+ * 少一半,除此之外没有任何信号。
465
+ *
466
+ * 口径从宿主安装锚点解析(不是插件侧):这些包**不是**本插件的 peer,插件 `node_modules`
467
+ * 里通常没有,从插件侧解析只会得到一律"解析不到",那不是一个结论。
468
+ *
469
+ * @param hostRoot - `.../node_modules/@deepseek-ai` 或 `null`(找不到宿主)。
470
+ */
471
+ function hostContractStatus(hostRoot) {
472
+ if (hostRoot === null)
473
+ return null;
474
+ const packages = BARE_HOST_MODULE_CONTRACTS.map((contract) => {
475
+ let resolved = null;
476
+ try {
477
+ resolved = createRequire(join(dirname(hostRoot), 'package.json')).resolve(contract.package);
478
+ }
479
+ catch {
480
+ resolved = null;
481
+ }
482
+ let missingMarkers = [];
483
+ if (resolved !== null && contract.markers.length > 0) {
484
+ try {
485
+ const source = readFileSync(resolved, 'utf8');
486
+ missingMarkers = contract.markers.filter((marker) => !source.includes(marker.needle)).map((marker) => marker.needle);
487
+ }
488
+ catch { /* 入口读不到时只报"解析不到",不额外造一条 canary 结论 */ }
489
+ }
490
+ return { package: contract.package, feature: contract.feature, resolved, missingMarkers };
491
+ });
492
+ return {
493
+ packages,
494
+ missing: packages.filter((item) => item.resolved === null).map((item) => item.package),
495
+ drifted: packages
496
+ .filter((item) => item.missingMarkers.length > 0)
497
+ .map((item) => ({ package: item.package, missingMarkers: item.missingMarkers })),
427
498
  };
428
499
  }
429
500
  /**
@@ -8,11 +8,23 @@
8
8
  * 为什么不能并进 `cue`(那能省下整整一行 ≈16 tok/段):用户 2026-09-18 定过"权威声明单独
9
9
  * 成句" —— 它和动作句是两种东西(一句说"什么时候用它",一句说"以哪份为准"),合并后容易
10
10
  * 被一眼带过。所以这里的收益只有约 6 tok/轮,**主要收益是消除四种措辞**,不是省字节。
11
+ *
12
+ * 2026-10-18(用户裁定):**模型侧模板一律英文** —— 框架标题、取代声明、`how` 行、清空 /
13
+ * 截断 / 未列出这些提示。**用户自己写的内容一律原样注入**(场景名与说明、记忆正文、MCP
14
+ * 备注、技能与人设的描述、`AGENTS.md` 正文、路径),一个字符都不动。理由:官方两条注入行
15
+ * (`Instructions from:` 与 skill catalog)本来就是英文,混着中文只让模型多付一次对齐成本。
16
+ *
17
+ * 同日晚些时候二次收紧(用户:「让 agent 更能注意到信息,信息高效,也不要冗余」):这一代
18
+ * 每个句子都按「少一个词不改行为就删」过了一遍 —— 权威声明去掉自我指涉与场合限定
19
+ * (`This replaces … in this session` → `Supersedes … of the same kind`),六个 `how` 行
20
+ * 去掉「This preset has no …」这类铺垫、改成先说要做什么。**没有新增任何一行**:2026-09-23
21
+ * 那条原则(注入只讲当前情况、不写解释与动作句)仍然生效,加字只会稀释指令。
11
22
  */
12
- export const SUPERSEDE_NOTE = '本份取代本次会话中更早注入的同类内容。';
23
+ export const SUPERSEDE_NOTE = 'Supersedes earlier injected content of the same kind.';
13
24
  export const DOMAIN_FRAME = {
14
25
  scene: {
15
- // 场景段只有标题 + 正文 + 权威声明(**没有 cue 是六个域的共同决定**,理由见 `DomainFrame`)。
26
+ // 场景段此前只有标题 + 正文 + 权威声明(没有 cue 是六个域的共同决定,理由见 `DomainFrame`);
27
+ // 2026-10-18 起多了一句 `how`(定义 + 后果),为什么加、加的是哪一句,见本段末尾。
16
28
  //
17
29
  // 这一段的演进值得记下来,因为每一次都是被实际注入推着改的:
18
30
  // ① 最早它把「场景说明」当**约定**授权("一律照办,覆盖你的默认做法")—— 而它的实例
@@ -24,33 +36,96 @@ export const DOMAIN_FRAME = {
24
36
  // 知道没用的信息,反推更是浪费 token」。于是三句全删。
25
37
  //
26
38
  // 现在这一段只回答一个问题:**当前处在哪个场景、它是什么**(`**「代码」—— 写代码**`)。
27
- // 它还比别的段少一层:因果句也删了 —— 它解释的是"另外四段是怎么产生的"(机制),
28
- // 不是当前情况本身,而且"清单里没有 ≠ 本机没有"这层反推被用户明确判为浪费。
29
- title: '当前场景',
39
+ //
40
+ // 2026-10-18(用户看到实际注入后问「agent 怎么知道这个场景是做什么的」):把②那次删掉的
41
+ // 定义**加回来,但只加一句**,且换掉当时失败的那个写法。
42
+ // - 当时失败的是"场景是用户给这台机器配的工作模式"——**只说是什么,不说它带来了什么**,
43
+ // 模型读完仍无动作可做。这里补上后果:记忆 / 提示词 / 工具 / 人设都已经按它筛过。
44
+ // - 与①的区别(那条授权语不能回来):这里不授权"照办",只说"你拿到的这些东西为什么长这样"。
45
+ // 用户自己写的说明仍然只是注解,不是约定。
46
+ // - 为什么这句能过"不写机制"那条原则:它解释的不是宿主怎么算的,而是**模型眼前这份上下文
47
+ // 为什么缺了几样**(`## Scene: 编程` 分组、看不到别的场景的记忆、工具比别处少)。
48
+ // 这是它做判断时要用的事实,不是反推。
49
+ // - 代价 ≈24 tok/轮,只在这一域。
50
+ title: 'Current scene',
51
+ how: "A scene is the user's working mode: the memories, prompt, tools and personas you are given are already filtered to this one.",
30
52
  supersede: SUPERSEDE_NOTE,
31
53
  },
32
54
  memory: {
33
- title: '当前记忆',
55
+ title: 'Current memories',
34
56
  supersede: SUPERSEDE_NOTE,
35
57
  },
36
58
  mcp: {
37
- title: '当前 MCP 服务器',
59
+ title: 'Current MCP servers',
38
60
  // how 只剩"怎么用备注"这半句:前半个分句「工具名是 `mcp__<服务器>__<工具>`」删掉了
39
61
  // —— 模型自己的工具表里就是这个命名(`mcp__context7__xxx`),告诉它格式是零信息量。
40
- how: '带「用户提示:」的行是用户写给这台服务器的决策提示,选服务器之前先看一眼。',
62
+ //
63
+ // 2026-10-18 补两句(用户同一轮裁定:三个点了工具的域都要有触发条件):
64
+ // ① **什么时候想起这张表** —— 模型看不到 server 的连线状态,缺工具时它只会两种反应:
65
+ // 说"没有这个工具",或反复重试。这一域存在的意义就是给它第三种(表里有、只是没连上)。
66
+ // ② `not connected now` 那一档的读法 —— 不写清"此刻没有注册任何工具",这一行会被读成
67
+ // "可用但状态存疑",于是它照旧去调一个必然失败的工具名。
68
+ //
69
+ // 同轮二次修正(用户:「mcp 如果用户没写备注不是白写了吗」):两句**都改成按需发**。
70
+ // 上一版是常量串,于是"一台上线服务器、没有备注"这种最常见的情形里,整段 = 三行说明
71
+ // 解释一个不存在的东西 + 一行 `- **github**` —— 说明比内容长,且每轮都付。
72
+ // 现在:备注句只在真有备注时发,离线句只在真有 `not connected now` 档时发,两者都没有
73
+ // 就**不发行**(`undefined`)。正文只有名字时,标题加名字本身已经把话说完了。
74
+ how: ({ text }) => {
75
+ const parts = [];
76
+ if (text.includes('user hint: ')) {
77
+ parts.push("`user hint:` marks the user's own note on that server — read it before choosing one.");
78
+ }
79
+ const offline = text.includes('(not connected now)');
80
+ const muted = text.includes('(no tools enabled here)');
81
+ // 两个状态标记合起来讲一次:各写一句会把"别当成缺失"重复两遍(2026-10-18 审查收口)。
82
+ if (offline && muted) {
83
+ parts.push('Status markers: `not connected now` = no tools registered right now; `no tools enabled here` = up, but every tool is switched off. Neither means the server is missing.');
84
+ }
85
+ else if (offline) {
86
+ parts.push('A server marked `not connected now` registers no tools right now: check this list before telling the user a server is missing or a tool does not exist.');
87
+ }
88
+ else if (muted) {
89
+ parts.push('A server marked `no tools enabled here` is up, but every one of its tools is switched off — do not report it as missing.');
90
+ }
91
+ return parts.length > 0 ? parts.join(' ') : undefined;
92
+ },
41
93
  supersede: SUPERSEDE_NOTE,
42
94
  },
43
95
  skills: {
44
- title: '当前技能目录',
96
+ title: 'Current skills',
45
97
  // 两句都只在预设没挂官方 `skill` 工具时出现,差别只在点名不点名那个取正文的工具
46
98
  // (工具被用户在兼容页关掉时不点名 —— 点名一个模型手里没有的工具只会让它去猜名字)。
47
- how: (toolHidden) => toolHidden('skill_manager_read')
48
- ? '本预设没有官方 `skill` 工具:目录只有摘要,读完再照做。'
49
- : '本预设没有官方 `skill` 工具:要正文用 `skill_manager_read`(按名字直接给正文与路径);目录只有摘要,读完再照做。',
50
- supersede: `${SUPERSEDE_NOTE.slice(0, -1)};只列当前可调用的技能。`,
99
+ //
100
+ // 2026-10-18 补「什么时候动手」这半句(用户对比官方 skill catalog 后提出模板太单薄):
101
+ // 上一版只有"读完再用"(**已经决定用之后**的纪律),没有"任务对得上就去读"(**怎么想起它**)。
102
+ // 官方那条行两者都有(`if … the task clearly matches … call the skill tool … before taking
103
+ // task actions`),而这一域恰恰只在官方那条行**缺席**时才注入 —— 也就是模型手上既没有
104
+ // `skill` 工具、也没人告诉过它技能的调用时机,缺的正是这一句。同理补上"描述不是指令"
105
+ // (官方对应句 `do not infer or follow a skill's instructions until it has been loaded`),
106
+ // 免得它拿 500 字摘要当正文照做。
107
+ // 不点名工具那一支改说"没有装载入口":上一版在那里仍写着"read a skill's body before
108
+ // following it",等于让模型去读一份它根本读不到的东西。
109
+ //
110
+ // 首句定义(用户 2026-10-18 第二轮裁定"加"):官方那条行以 `A skill is a reusable set of
111
+ // task-specific instructions.` 开头 —— 这句不是背景介绍,是后面两半句的**前提**:不懂"skill
112
+ // 是按需加载的任务指令集"时,"任务对得上就读正文"读不出任何动作,只会被理解成"一个名字 +
113
+ // 一句简介"。所以这里破例把定义放在动作句之前(正常纪律是先说要做什么,见 `SUPERSEDE_NOTE`
114
+ // 那段注释);措辞照抄官方,模型对它有现成先验。**只在官方载体缺席时发**(本域的唯一条件),
115
+ // 官方在时整段不注入,不存在两份定义。
116
+ how: ({ toolHidden }) => toolHidden('skill_manager_read')
117
+ ? 'A skill is a reusable set of task-specific instructions. No official `skill` tool and no skill reader here: names and summaries only, and no way to load a body — treat them as a capability list, not as instructions.'
118
+ : 'A skill is a reusable set of task-specific instructions. No official `skill` tool here: when a task clearly matches one or the user names one, read its body with `skill_manager_read` before acting. Summaries only — never treat a description as instructions.',
119
+ // 英文下不能再靠 `slice(0, -1)` 拼句(那会把句末句点削掉,接出
120
+ // `…in this session Only skills…`):整句照发,续句自带首字母大写。
121
+ // 边界陈述 2026-10-18 拆出来并改口:原来写 `Only currently callable skills are listed.`,
122
+ // 而取正文的工具被关掉时**一条都调不到**,那句话在那个分支里是假的。改成 `enabled`
123
+ // —— 两支都成立(它们确实都是"启用中"的),也不再依赖工具在不在手里。
124
+ supersede: SUPERSEDE_NOTE,
125
+ boundary: 'Only enabled skills are listed.',
51
126
  },
52
127
  subagents: {
53
- title: '当前子智能体',
128
+ title: 'Current subagents',
54
129
  // 分界规则(2026-09-17 方案 C,本机实测 session-ee722e23 逼出来的):官方那两个
55
130
  // 委派工具(`subagent` / `subagent_fork`)不带人设,而此前没有任何一句话说明何时该
56
131
  // 用谁 —— 模型在"审查刚读过的 README"时选了 `subagent_fork`(fork 能继承已读内容、
@@ -62,13 +137,24 @@ export const DOMAIN_FRAME = {
62
137
  // Modes 段与 `inherit` 参数说明里**各有一份**,三份都在每轮上下文里 = 同一件事付三次
63
138
  // token。留这两份(它们就在"要传这个参数"的那一刻),这里只留 how 行独有的那半句 ——
64
139
  // 与官方两个委派工具的分界规则。
65
- how: (toolHidden) => toolHidden('subagent_manager_run')
66
- ? '本会话没有带人设的委派工具;官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。'
67
- : '贴合人设的任务一律用 `subagent_manager_run`;官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。',
140
+ // 2026-10-18 补触发条件(同上):此前只说了"带人设的活走哪条通道"(选哪条),
141
+ // 没说"什么算带人设的活"(要不要委派)。**例子刻意不写**——`subagent_manager_run` 的描述里
142
+ // 已经有 "a review, an investigation, a piece of writing 且细节不必占自己的上下文",两句写在同一份
143
+ // 上下文里就是同一件事付两次。
144
+ // 不点名那一支补"只能参考、调不到":上一版只说官方那两个不带人设,而"目录里有人设 ≠
145
+ // 能委派"这层没人说 —— 模型会拿官方工具硬塞一个人设名进去。
146
+ how: ({ toolHidden }) => toolHidden('subagent_manager_run')
147
+ ? 'No persona-carrying delegation tool here; official `subagent` / `subagent_fork` carry no persona — use them only when none fits or for background work. The personas listed here are reference only, not callable.'
148
+ : 'Persona work goes through `subagent_manager_run` — when a task matches one of these personas, delegate it there. Official `subagent` / `subagent_fork` carry no persona; use them only when none fits or for background work.',
68
149
  supersede: SUPERSEDE_NOTE,
69
150
  },
70
151
  prompt: {
71
- title: '当前提示词',
152
+ title: 'Current prompt',
153
+ // 2026-10-18(审查 E):这一域此前只有 `Instructions from: <path>` 一行文件头,没有一句话
154
+ // 说明**这些规则是要遵循的**。记忆域有授权语、场景域有定义,唯独效力最强的这一域空着 ——
155
+ // 而官方那条行给的是**降权**措辞("可作参考"),我们当初刻意没抄,于是等于什么都没说。
156
+ // 现在补一句正面的:要遵循,且用户当场说的优先。
157
+ how: 'These are the instructions in force for this machine; follow them unless the user says otherwise in this conversation.',
72
158
  supersede: SUPERSEDE_NOTE,
73
159
  },
74
160
  };
@@ -87,7 +173,7 @@ export const DOMAIN_FRAME = {
87
173
  export const HOW_NAMED_TOOLS = ['skill_manager_read', 'subagent_manager_run'];
88
174
  /** 域声明里没登记的 key(理论上到不了这里):给一个不出错的通用框架。 */
89
175
  export const fallbackFrame = (label) => ({
90
- title: `当前的${label}`,
176
+ title: `Current ${label}`,
91
177
  supersede: SUPERSEDE_NOTE,
92
178
  });
93
179
  export const domainFrame = (key, label) => DOMAIN_FRAME[key] ?? fallbackFrame(label);
@@ -131,34 +217,56 @@ export function escapeFrameBody(body) {
131
217
  * 同时把**正文也包进标记里**(此前只有引导语在标记内、正文裸奔)—— 官方两条注入行都是
132
218
  * 整条包住的,正文里的用户自由文本更需要这层来源标记。
133
219
  */
134
- export function renderDomainText(section, toolHidden = () => false) {
220
+ export function renderDomainText(section, toolHidden = () => false, replacing = true) {
135
221
  const frame = domainFrame(section.key, section.label);
136
222
  // 层级(2026-09-23 用户看到实际注入后指出「记忆内的场景怎么都是 ## 标题」):**`#` 一级给板块**
137
223
  // (场景 / 记忆 / MCP / 技能 / 子智能体 / 提示词),域正文里的 `##` 才是它的下一层
138
- // (记忆段的 `## 场景:X`、超预算时的 `## 未注入的参考信息`)。此前标题也是 `##`,两者平级,
224
+ // (记忆段的 `## Scene: X`、超预算时的 `## Not injected (…)`)。此前标题也是 `##`,两者平级,
139
225
  // 模型读不出主次 —— 而"哪些内容归在哪个板块/场景下"正是它做判断时要用的结构。
140
226
  //
141
227
  // 开标签后**必须空一行**:markdown 里 `#` 紧跟在一行文字后面只是**段落续行**,不会被渲染成
142
228
  // 标题 —— 用户截图里 `## 本机当前的场景` 就是这么被吞掉的(和 `<system-reminder>` 挤成一段)。
143
229
  // 收尾同理:正文末尾先归一成单个空行,免得 `</system-reminder>` 粘在最后一行上。
144
- // 结构:标题 → 补充说明(有才发)→ 权威声明 → 正文。没有 cue 那一行(见 `DomainFrame` 的注释)。
230
+ // 结构:标题 → 补充说明(有才发)→ 权威声明(只在替换时)→ 边界陈述(有才发)→ 正文。
231
+ // 没有 cue 那一行(见 `DomainFrame` 的注释)。
232
+ const info = { toolHidden, text: section.text, replacing };
145
233
  const lines = [FRAME_OPEN, '', `# ${frame.title}`];
146
- const how = typeof frame.how === 'function' ? frame.how(toolHidden) : frame.how;
234
+ const how = typeof frame.how === 'function' ? frame.how(info) : frame.how;
147
235
  if (how !== undefined)
148
236
  lines.push(how);
149
- lines.push(frame.supersede, '', escapeFrameBody(section.text).replace(/\n+$/, ''), '', FRAME_CLOSE);
237
+ // 权威声明只在**替换**时发(2026-10-18 审查 D):本会话的第一份没有旧内容可取代,那句话
238
+ // 只是占一行。边界陈述反过来 —— 它不依赖"有没有旧的",第一份就要发。
239
+ if (replacing)
240
+ lines.push(frame.supersede);
241
+ if (frame.boundary !== undefined)
242
+ lines.push(frame.boundary);
243
+ lines.push('', escapeFrameBody(section.text).replace(/\n+$/, ''), '', FRAME_CLOSE);
150
244
  return lines.join('\n');
151
245
  }
152
246
  /** 「已清空」通知正文:某个域曾经注入过、现在没有内容时发一条(纯函数,测试用)。 */
153
- export function clearedDomainText(key, label) {
247
+ export function clearedDomainText(key, label, reason) {
154
248
  const frame = domainFrame(key, label);
249
+ // 通知的措辞按**原因**分叉(2026-10-18 审查)。原来只有一句「同一类内容不再有效」,
250
+ // 而"这一域为什么消失"有四种,其中一种下那句话是**假话**:
251
+ // - `official`:官方载体接管了(技能目录由 `dsh-tool-skill` 送、提示词由
252
+ // `dsh-agent-instructions` 送)。此时同类内容**仍然有效、而且更强**,只是换了个发送方。
253
+ // 说"不再有效"会让模型把官方那一份也当成作废 —— 标准预设里这正是最常见的一次切换。
254
+ // - `empty`:真的没有内容了(全部删掉 / 场景切走)。原来的话在这里是对的。
255
+ // - `off` / `child`:是**故意不送**(用户关掉这一域,或该域不适用于子会话),内容本身
256
+ // 可能还在,所以只能说"不再展示",不能说"不存在"。
257
+ // - `error` 走不到这里:那条路径刻意不发「已清空」(见调用方注释)。
258
+ const body = reason === 'official'
259
+ ? '**Replaced** — the official channel now sends this content; earlier injected content of the same kind is stale.'
260
+ : reason === 'off' || reason === 'child'
261
+ ? '**Cleared** — this reminder is not sent here; earlier injected content of the same kind no longer applies.'
262
+ : '**Cleared** — earlier injected content of the same kind no longer applies.';
155
263
  // 形状与 `renderDomainText` 一致(开标签后空行、板块 `#`、收尾空行)—— 这两条都是同一个
156
264
  // 通道发出去的消息,层级与留白不该有两套。
157
265
  return [
158
266
  FRAME_OPEN,
159
267
  '',
160
268
  `# ${frame.title}`,
161
- '**已清空** —— 本次会话中此前注入的同类内容不再有效。',
269
+ body,
162
270
  '',
163
271
  FRAME_CLOSE,
164
272
  ].join('\n');
@@ -40,7 +40,8 @@
40
40
  // 判定放在域声明的 `applicableTo` 上,通道只负责问一句 —— "哪个域对哪类会话不成立"
41
41
  // 是域的语义,不是通道的机制。
42
42
  // ⚠️ 2026-09-17 方案 A 纠正:这**不是**"子会话不能委派"。官方 `dsh-tool-subagent`
43
- // 默认 `maxDepth: 3`,provider 只在传了该值时才校验 —— 子代理本来就能继续嵌套
43
+ // 默认 `maxDepth: 1`(0.2.1 实测核对:`dsh-subagent/lib/index.js` 的 Config 默认值),
44
+ // provider 只在传了该值时才校验 —— 子代理本来就能继续嵌套
44
45
  // (用户实测确认)。这个抑制只是让常驻目录不进入子会话、减少噪声;委派能力由官方
45
46
  // 决定,本插件不干预(我们也不再传 `maxDepth`)。
46
47
  // - **采纳遥测**:注入只解决"到没到",不解决"用没用"。本模块按域统计
@@ -376,12 +377,14 @@ export function createContextInjector(deps) {
376
377
  const toolHidden = hiddenNow === null ? () => false : (name) => hiddenNow.has(name);
377
378
  for (const section of sections) {
378
379
  published.add(section.key);
379
- const text = renderDomainText(section, toolHidden);
380
- const current = visible.get(section.key);
381
- if (current !== undefined && current.text === text)
382
- continue;
383
380
  // `current !== undefined` = 该域在可见表面上已有一条更早的己方消息 → 这次是**替换**,
384
381
  // 界面上的动作标签据此从「已载入」变成「已更新」(见 domainMessage 的 baseline/changes)。
382
+ // 这个判据同时决定框架里那句 `Supersedes …` 发不发(2026-10-18:第一份没有旧内容可取代)。
383
+ const current = visible.get(section.key);
384
+ const replacing = current !== undefined;
385
+ const text = renderDomainText(section, toolHidden, replacing);
386
+ if (replacing && current.text === text)
387
+ continue;
385
388
  let files;
386
389
  try {
387
390
  files = domainOf.get(section.key)?.files?.();
@@ -389,7 +392,7 @@ export function createContextInjector(deps) {
389
392
  catch {
390
393
  files = undefined;
391
394
  }
392
- additions.push(domainMessage(section, text, files, current !== undefined));
395
+ additions.push(domainMessage(section, text, files, replacing));
393
396
  appended.push(section.key);
394
397
  injectedKeys.add(section.key);
395
398
  }
@@ -409,7 +412,7 @@ export function createContextInjector(deps) {
409
412
  const label = labelOf.get(key) ?? key;
410
413
  if (previous.form === 'notice')
411
414
  continue;
412
- additions.push(clearedMessage(key, label));
415
+ additions.push(clearedMessage(key, label, lastReasons[key]));
413
416
  appended.push(key);
414
417
  clearedKeys.add(key);
415
418
  }
@@ -524,9 +527,9 @@ function domainMessage(section, text, files, replacing) {
524
527
  source,
525
528
  });
526
529
  }
527
- function clearedMessage(key, label) {
530
+ function clearedMessage(key, label, reason) {
528
531
  return createUserMessage({
529
- content: [{ type: 'text', text: clearedDomainText(key, label) }],
532
+ content: [{ type: 'text', text: clearedDomainText(key, label, reason) }],
530
533
  source: { kind: INJECT_KIND_OF[key], form: 'notice', summary: `${label}已清空` },
531
534
  });
532
535
  }