dsh-plugin-tool-management 0.19.1 → 0.20.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +915 -829
  2. package/README.md +237 -254
  3. package/README_EN.md +234 -223
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/images/logo.gif +0 -0
  21. package/docs/update.md +32 -30
  22. package/lib/client.js +9480 -6970
  23. package/lib/compat/preset-reach.js +5 -2
  24. package/lib/context-inject-frame.js +13 -8
  25. package/lib/hub.js +104 -11
  26. package/lib/index.js +72 -24
  27. package/lib/mcp/manager.js +45 -13
  28. package/lib/mcp/secret-guard.js +2 -2
  29. package/lib/mcp/state-section.js +1 -1
  30. package/lib/memories/archive-engine.js +4 -5
  31. package/lib/memories/constants.js +11 -4
  32. package/lib/memories/service.js +6 -1
  33. package/lib/op-registry.js +13 -11
  34. package/lib/ops/memory.js +42 -4
  35. package/lib/ops/scene-records.js +43 -9
  36. package/lib/ops/sessions.js +67 -46
  37. package/lib/ops/trash.js +107 -8
  38. package/lib/request-gate.js +9 -2
  39. package/lib/scene-prompt-sync.js +48 -23
  40. package/lib/sessions/history.js +32 -22
  41. package/lib/sessions/workspace.js +0 -10
  42. package/lib/skills/catalog.js +17 -5
  43. package/lib/skills/core.js +189 -7
  44. package/lib/skills/frontmatter.js +31 -0
  45. package/lib/skills/manager-state.js +73 -3
  46. package/lib/skills/service.js +53 -13
  47. package/lib/skills/skill-roots.js +54 -4
  48. package/lib/skills/skill-trash.js +118 -12
  49. package/lib/skills/upload-pipeline.js +110 -26
  50. package/lib/subagents/catalog.js +9 -5
  51. package/lib/subagents/service.js +44 -6
  52. package/lib/subagents/tools.js +45 -15
  53. package/lib/tool-table-switch.js +6 -6
  54. package/lib/tools/deps.js +43 -0
  55. package/lib/tools/mcp.js +74 -27
  56. package/lib/tools/memory.js +49 -9
  57. package/lib/tools/prompt.js +38 -9
  58. package/lib/tools/remove.js +206 -0
  59. package/lib/tools/scene.js +155 -32
  60. package/lib/tools/skills.js +162 -55
  61. package/lib/tools/subagent.js +65 -31
  62. package/lib/tools/table.js +39 -12
  63. package/lib/trash-id.js +16 -0
  64. package/lib/trash-retention.js +3 -3
  65. package/package.json +115 -114
  66. package/lib/mcp/inspect-probe.js +0 -199
@@ -2,16 +2,21 @@
2
2
  // skill_manager_list / skill_manager_read / skill_manager_switch / skill_manager_save。
3
3
  //
4
4
  // create → save:`skill-create` 对同名是**拒绝**的,所以"建还是改"的判定只能在工具这一侧
5
- // (先读一次 `skill-state`)。改的分支走新加的 `skill-update` op,它只认 hub 里的胜出者。
5
+ // (先读一次 `skill-state`)。改的分支走新加的 `skill-update` op,它只认本插件可写的两个根
6
+ // (hub = 导入落点 / dsh = 新建默认落点,2026-10-15 裁定)里的胜出者。
6
7
  import { toKebab } from '../skills/core.js';
7
- import { text } from './deps.js';
8
+ import { locateSkillRoot, text } from './deps.js';
8
9
  export function buildSkillTools(deps) {
9
10
  const { defineTool, register } = deps;
10
11
  // Skill-facing tools: list, toggle, create. create is gated by the
11
12
  // tools/pre-execute hook below (the model must ask before writing files).
12
13
  register(defineTool({
13
14
  name: 'skill_manager_list',
14
- description: 'List skills with enabled state, effective/shadowed status and source file path. The「本机技能目录」reminder carries callable skills and summaries only; use this for entries that are off, for each entry\'s source-file path, and to see which source wins a name collision. Defaults to enabled only; all=true for every entry. A copy marked "shadowed by <root>" stays inactive even if enabled.',
15
+ // `— it is absent when the preset carries the official \`skill\` tool` 删掉
16
+ // (2026-10-15 注入文本审查):可行动的那半句已经写在 `skill_manager_read` 的描述里
17
+ // ("Not sent when the official `skill` tool is mounted — use that instead."),
18
+ // 这里再预告一遍同一个条件,只是同一件事付两次 token。
19
+ description: 'List skills with enabled state, effective/shadowed status and source file path. The「当前技能目录」reminder carries callable skills and summaries only; use this for entries that are off, for each entry\'s source-file path, and to see which source wins a name collision. Defaults to enabled only; all=true for every entry. A copy marked "shadowed-by:<root>" stays inactive even if enabled; "preferred" marks the copy that wins a name collision.',
15
20
  parameters: {
16
21
  all: { type: 'boolean', description: 'Include disabled and shadowed entries (default false).' },
17
22
  },
@@ -43,17 +48,18 @@ export function buildSkillTools(deps) {
43
48
  const enabled = skill.enabled !== undefined
44
49
  ? skill.enabled === true
45
50
  : (skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false);
46
- const marks = [];
47
- if (skill.shadowedBy && skill.shadowedBy.root)
48
- marks.push('shadowed by ' + skill.shadowedBy.root);
49
- if (skill.preferred === true)
50
- marks.push('preferred');
51
+ // 带标签的 `k: v` 串(2026-10-14 对抗性审查 P1-1):此前是无列头的 pipe 串,两个标记
52
+ // (`shadowed by <root>` / `preferred`)还会挤在同一格里、都没有标签 —— 模型得猜哪段
53
+ // 是字段、哪段是标记,以及 `preferred` 到底是什么(它此前**全仓没有定义**)。
54
+ // 与 `mcp_manager_list` 的 `server:` / `level:` / `user-hint:` 同款。
51
55
  rows.push({
52
56
  root: String(root.key || ''),
53
57
  enabled,
54
- text: (skill.name || skill.declaredName || '') + ' | ' + (root.key || '') + ' | ' + (enabled ? 'enabled' : 'disabled') +
55
- (marks.length ? ' | ' + marks.join(' ') : '') +
56
- (skill.path ? ' | ' + skill.path : ''),
58
+ text: 'skill:' + (skill.name || skill.declaredName || '') + ' | root:' + (root.key || '') +
59
+ ' | ' + (enabled ? 'enabled' : 'disabled') +
60
+ (skill.shadowedBy && skill.shadowedBy.root ? ' | shadowed-by:' + skill.shadowedBy.root : '') +
61
+ (skill.preferred === true ? ' | preferred' : '') +
62
+ (skill.path ? ' | path:' + skill.path : ''),
57
63
  });
58
64
  }
59
65
  }
@@ -78,7 +84,7 @@ export function buildSkillTools(deps) {
78
84
  // 已在上面标出胜出者),再走 `skill-detail` 读那一份 —— 读到的永远是目录列出来的那一份。
79
85
  register(defineTool({
80
86
  name: 'skill_manager_read',
81
- description: 'Read one skill\'s full SKILL.md body by name — what the「本机技能目录」reminder deliberately leaves out (it has names and summaries only). Resolves name collisions the same way, so you get the copy actually in effect; pass root only to inspect a specific (e.g. shadowed) source. Read-only.',
87
+ description: 'Read one skill\'s full SKILL.md body by name — what the「当前技能目录」reminder deliberately leaves out (it has names and summaries only). Also lists the other files living in that skill folder (references/, scripts/), which the body often points at. Resolves name collisions the same way, so you get the copy actually in effect; pass root only to inspect a specific (e.g. shadowed) source. Not sent when the official `skill` tool is mounted — use that instead.',
82
88
  parameters: {
83
89
  name: { type: 'string', required: true, description: 'Skill name, exactly as it appears in the catalog (kebab-case).' },
84
90
  root: { type: 'string', description: 'Source root key (dsh/hub/agents/codex/claude, a project key, or a custom one). Default: the winning copy of this name.' },
@@ -117,8 +123,9 @@ export function buildSkillTools(deps) {
117
123
  }
118
124
  if (!hits.length) {
119
125
  // 「去调 skill_manager_list」那句:那条工具在出厂默认里就是关着的,点名一条模型没有的
120
- // 工具只会让它白跑一趟(执行侧拦住,而它看不出为什么)。
121
- const listHint = deps.toolVisible('skill_manager_list')
126
+ // 工具只会让它白跑一趟(执行侧拦住,而它看不出为什么)。判据按 **agent**(2026-10-14
127
+ // 对抗性审查 P0-1 的同一条纪律):人设用 `toolsDeny` 砍掉它时,全局开关看着是开的。
128
+ const listHint = deps.toolVisibleFor('skill_manager_list', exec && exec.agent)
122
129
  ? ' — call skill_manager_list (pass all=true) to see what is installed'
123
130
  : '';
124
131
  throw new Error('no skill named "' + wanted + '"' + (rootWanted ? ' in source "' + rootWanted + '"' : '') + listHint);
@@ -141,10 +148,24 @@ export function buildSkillTools(deps) {
141
148
  const dup = hits.filter((hit) => !hit.shadowedBy).length > 1
142
149
  ? hits.filter((hit) => !hit.shadowedBy).map((hit) => hit.rootKey).filter((k) => k !== picked.rootKey)
143
150
  : [];
151
+ // 附件清单(2026-10-16):技能正文常写着「详见 references/xxx.md」,而这一条工具是模型
152
+ // 读技能的唯一入口 —— 不列出来,它就会因为"看到的目录里没有那个文件"而凭空猜内容。
153
+ // 只报名与大小、不报正文:正文要走宿主文件工具,这里塞进 transcript 太重。
154
+ const attachments = Array.isArray(detail.attachments) ? detail.attachments : [];
155
+ const shown = attachments.slice(0, 20);
156
+ const attLine = shown.length
157
+ ? 'Attachments in this skill folder: ' + shown
158
+ .map((a) => String(a.path) + ' (' + Number(a.size || 0) + ' bytes)')
159
+ .join(', ')
160
+ + (attachments.length > shown.length ? ' … ' + (attachments.length - shown.length) + ' more' : '')
161
+ + (detail.attachmentsTruncated ? ' (listing capped at 200)' : '')
162
+ + ' — read them with a file tool if the body refers to one\n'
163
+ : '';
144
164
  const header = 'Skill ' + wanted + ' · source ' + picked.rootKey + (marks.length ? ' (' + marks.join('; ') + ')' : '') + '\n' +
145
165
  'Path: ' + (detail.path || picked.path || '(unknown)') + '\n' +
146
166
  'Description: ' + String(detail.description || '(none)').replace(/\s+/g, ' ').trim() + '\n' +
147
167
  (dup.length ? 'Other callable copies with the same name: ' + dup.join(', ') + ' (this one wins; read them only to compare)\n' : '') +
168
+ attLine +
148
169
  '--- SKILL.md body ---\n';
149
170
  // 注入边界:正文读进来就永久留在这一轮的 transcript 里,压制型预设下目录也可能不在 ——
150
171
  // 与 skill_manager_list 同一句说明,别让模型以为"读过就等于技能开着"。
@@ -154,12 +175,21 @@ export function buildSkillTools(deps) {
154
175
  }));
155
176
  register(defineTool({
156
177
  name: 'skill_manager_switch',
157
- description: 'Enable or disable one DSH skill (policy only; source files are never modified), or a whole source folder via `source`. Enabling a shadowed copy has no effect. Only when the user asks or approves.',
178
+ description: 'Enable or disable one DSH skill, or a whole source folder via `source`. Pass `prefer` (with `name` + `root`) to make that copy win a same-name collision, or to clear the recorded choice. Enabling a shadowed copy has no effect. Reversible. Only when the user asks or approves.',
158
179
  parameters: {
159
180
  name: { type: 'string', description: 'Skill name (kebab-case).' },
160
- enabled: { type: 'boolean', required: true, description: 'true to enable, false to disable.' },
181
+ // `enabled` 从 required 改成"要么给 enabled、要么给 prefer"(2026-10-15):
182
+ // prefer 是一次独立的写(`skill-prefer`),模型不该为了改首选来源先被迫拨一次开关。
183
+ // 缺省仍然**拒绝**(execute 里那条守卫),不然 `args.enabled ? enable : disable`
184
+ // 会把"忘了给"静默当成"停用"。
185
+ enabled: { type: 'boolean', description: 'true to enable, false to disable. Required unless you only pass `prefer`.' },
161
186
  source: { type: 'string', description: 'A source root key to switch as a whole (dsh/hub/agents/codex/claude or a project key).' },
162
- root: { type: 'string', description: 'Source root key the skill lives in (with `name`); default dsh.' },
187
+ root: { type: 'string', description: 'Source root key the skill lives in (with `name`). Default: whichever source holds that skill (all sources are searched). Required when using `prefer`.' },
188
+ // 同名首选来源(2026-10-15 注入文本审查)。此前这条工具的回执写着
189
+ // "or make this root the preferred copy **in the panel**" —— 明明一个布尔能解决,
190
+ // 却把用户推去界面点。而 `skill-prefer` / `skill-unprefer` 两个 op 早就在(面板在用)。
191
+ // 代价为零:本工具在出厂默认名单里就是关着的,加参数不进默认上下文。
192
+ prefer: { type: 'boolean', description: 'true = the copy in `root` wins this name\'s collision; false = drop the recorded choice. Needs `name` and an explicit `root`. Omit = leave the choice alone.' },
163
193
  },
164
194
  output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
165
195
  async execute(args) {
@@ -168,10 +198,28 @@ export function buildSkillTools(deps) {
168
198
  throw new Error(blocked);
169
199
  const name = String((args && args.name) || '').trim();
170
200
  const source = String((args && args.source) || '').trim();
201
+ // `prefer` 的三态与它自己的前置条件(2026-10-15):省略 = 不碰首选表;
202
+ // true / false = 设 / 撤。**必须点名 root** —— 默认值 `dsh` 在"同名首选"这件事上
203
+ // 恰恰是最容易猜错的一格(要选赢的往往是**非 dsh** 那一份),所以这里不接受默认值。
204
+ const preferGiven = Boolean(args && args.prefer !== undefined);
205
+ const prefer = args && args.prefer === true;
206
+ if (preferGiven && typeof args.prefer !== 'boolean')
207
+ throw new Error('prefer 必须是 true 或 false');
171
208
  if (name && source)
172
209
  throw new Error('name 与 source 只能给一个:name 是单个技能,source 是整个来源目录');
210
+ if (preferGiven && source)
211
+ throw new Error('prefer 只对单个技能有意义:给 name,不要给 source');
173
212
  if (!name && !source)
174
213
  throw new Error('缺少参数:name(单个技能)或 source(整个来源目录)');
214
+ if (preferGiven && !name)
215
+ throw new Error('缺少参数:prefer 需要 name');
216
+ if (preferGiven && String((args && args.root) || '').trim() === '') {
217
+ throw new Error('prefer 需要 root:明确指定哪一份同名副本胜出(dsh / hub / agents / codex / claude 或项目键)');
218
+ }
219
+ // 两个动作一个都没给就拒:`args.enabled ? enable : disable` 会把"忘了给"静默当成"停用"。
220
+ if (args.enabled === undefined && !preferGiven) {
221
+ throw new Error('缺少参数:enabled 必须是 true 或 false(只想改同名首选就传 prefer)');
222
+ }
175
223
  if (source) {
176
224
  // 来源级启停:登记表里 `skill-source-enable` / `skill-source-disable` 都是
177
225
  // `{ serviceWrite, frozen, syncsArchive }`,所以守卫已过、档案也要同步
@@ -186,51 +234,103 @@ export function buildSkillTools(deps) {
186
234
  return 'OK: source ' + source + ' now ' + (args.enabled ? 'enabled' : 'disabled') +
187
235
  (syncErr ? '\nWARN: 当前场景档案未同步(' + syncErr + ')' : '');
188
236
  }
189
- const root = String(args.root || 'dsh');
190
- const op = args.enabled ? 'skill-enable' : 'skill-disable';
191
- const r = await deps.skillsOps[op]({ name: args.name, root });
192
- if (!r || r.ok === false)
193
- throw new Error((r && r.error) || 'skill toggle failed');
194
- // 场景未锁定时同步进当前场景档案(与 handlers 层同一套同步)。
195
- const syncErr = await deps.syncSwitchToScene(op, { name: args.name, root, enabled: args.enabled === true });
196
- // 静默无效是这个工具最容易骗人的地方:同名胜负只由「首选 > 来源优先级」决定,
197
- // 与启用状态无关。对影子副本操作会返回 OK,但技能依然不可用。如实说一句。
198
- let shadowedBy = '';
199
- try {
200
- const state = await deps.skillsOps['skill-state']({});
201
- for (const rt of (state && state.data && state.data.roots) || []) {
202
- for (const skill of rt.skills || []) {
203
- if (String(skill.declaredName || skill.name || '') !== String(args.name))
204
- continue;
205
- if (String(rt.key || '') !== root)
206
- continue;
207
- if (skill.shadowedBy && skill.shadowedBy.root)
208
- shadowedBy = String(skill.shadowedBy.root);
209
- }
237
+ // 不给 root 就在全部来源里找(2026-10-15 smoke-test P0-1):`skill_manager_save` 的落点
238
+ // 是 hub(`tool-management/skills/`),而这里的默认值原本是 dsh —— 「建完就启用」这条
239
+ // 最自然的链路必然报「技能不存在」。默认值改哪一边都不对(面板是显式传 root 的,
240
+ // 两边都依赖各自的默认),所以判定放到工具侧:按名字找**胜出的那一份**。
241
+ const resolveRoot = async () => {
242
+ const explicit = String((args && args.root) || '').trim();
243
+ if (explicit !== '')
244
+ return explicit;
245
+ const found = await locateSkillRoot(deps.skillsOps, name);
246
+ if (found.root)
247
+ return found.root;
248
+ // 探得到、就是没这个技能:当场把"查过哪些来源"说清楚。否则模型收到 op 那句不带
249
+ // 来源信息的「技能不存在」后,只能猜该传哪个 root(smoke-test P1-4-old 同一处)。
250
+ if (found.ok) {
251
+ throw new Error('技能不存在: ' + name + (found.roots.length ? '(已查 ' + found.roots.join('、') + ')' : ''));
210
252
  }
253
+ // 探不到(skill-state 失败)就别在这里编结论:退回旧默认,交给 op 报它自己的错。
254
+ return 'dsh';
255
+ };
256
+ const root = await resolveRoot();
257
+ const out = [];
258
+ // 首选来源**先写**(与面板「启用这个」同序:先 prefer 再 enable)。顺序不是随意的 ——
259
+ // 反过来的话,下面那段 `shadowedBy` 是在**旧**首选下算的,紧接着 prefer 又把胜负翻了过来,
260
+ // 回执里那句"它被 X 盖住了"当场就成了过期的假话。
261
+ if (preferGiven) {
262
+ const op = prefer ? 'skill-prefer' : 'skill-unprefer';
263
+ const p = await deps.skillsOps[op]({ name: args.name, root });
264
+ if (!p || p.ok === false)
265
+ throw new Error((p && p.error) || '设置同名首选失败');
266
+ // `skill-prefer` 会在"记下了但不会生效"时回 ok:true + warning(首选指向**停用**的来源,
267
+ // 而同名首选会挡住其他副本 → 两份都不生效)。原样转出去,否则模型以为已经生效。
268
+ out.push((prefer
269
+ ? 'OK: ' + root + '/' + String(p.name || args.name) + ' is now the preferred copy of that name'
270
+ : 'OK: cleared the preferred copy of ' + String(p.name || args.name)) +
271
+ (p.warning ? '\nWARN: ' + String(p.warning) : ''));
211
272
  }
212
- catch {
213
- shadowedBy = '';
273
+ if (args.enabled !== undefined) {
274
+ const op = args.enabled ? 'skill-enable' : 'skill-disable';
275
+ const r = await deps.skillsOps[op]({ name: args.name, root });
276
+ if (!r || r.ok === false)
277
+ throw new Error((r && r.error) || 'skill toggle failed');
278
+ // 场景未锁定时同步进当前场景档案(与 handlers 层同一套同步)。
279
+ const syncErr = await deps.syncSwitchToScene(op, { name: args.name, root, enabled: args.enabled === true });
280
+ // 静默无效是这个工具最容易骗人的地方:同名胜负只由「首选 > 来源优先级」决定,
281
+ // 与启用状态无关。对影子副本操作会返回 OK,但技能依然不可用。如实说一句。
282
+ let shadowedBy = '';
283
+ try {
284
+ const state = await deps.skillsOps['skill-state']({});
285
+ for (const rt of (state && state.data && state.data.roots) || []) {
286
+ for (const skill of rt.skills || []) {
287
+ if (String(skill.declaredName || skill.name || '') !== String(args.name))
288
+ continue;
289
+ if (String(rt.key || '') !== root)
290
+ continue;
291
+ if (skill.shadowedBy && skill.shadowedBy.root)
292
+ shadowedBy = String(skill.shadowedBy.root);
293
+ }
294
+ }
295
+ }
296
+ catch {
297
+ shadowedBy = '';
298
+ }
299
+ // 出路指向**本工具自己的参数**(2026-10-15 注入文本审查):此前写的是
300
+ // "or make this root the preferred copy in the panel" —— 模型读得到这句却做不到,
301
+ // 只能转述给用户去点界面。现在 `prefer` 就在同一张参数表里;而这条工具在出厂默认
302
+ // 名单里是关着的,模型既然调得到它,就一定拿得到这个参数(不会指到一条它没有的工具)。
303
+ const warn = shadowedBy
304
+ ? ' — BUT this copy is shadowed by the same-name skill in "' + shadowedBy + '", so it stays inactive: enable that copy instead, or make it win with prefer=true and root="' + shadowedBy + '"'
305
+ : '';
306
+ const sceneWarn = syncErr ? '\nWARN: 当前场景档案未同步(' + syncErr + ')' : '';
307
+ out.push('OK: ' + args.name + ' now ' + (args.enabled ? 'enabled' : 'disabled') + warn + sceneWarn);
214
308
  }
215
- const warn = shadowedBy
216
- ? ' — BUT this copy is shadowed by the same-name skill in "' + shadowedBy + '", so it stays inactive: enable that copy instead, or make this root the preferred copy in the panel'
217
- : '';
218
- const sceneWarn = syncErr ? '\nWARN: 当前场景档案未同步(' + syncErr + ')' : '';
219
- return 'OK: ' + args.name + ' now ' + (args.enabled ? 'enabled' : 'disabled') + warn + sceneWarn;
309
+ return out.join('\n');
220
310
  },
221
311
  }));
222
312
  register(defineTool({
223
313
  name: 'skill_manager_save',
224
- // 落点必须和 UI「创建技能」一致:两者都走 core 的默认落点(hub 的
225
- // tool-management/skills/,hub 缺失时退回 DSH_HOME/skills)。以前这里硬编码
226
- // root:'dsh',于是同一个「新建技能」动作,人点界面和模型调用会落到两个不同的根。
227
- // 改的分支**只认 hub 里的胜出者**:同名技能在 dsh/agents/claude 各有一份时只有一份
228
- // 生效,改错那一份会返回 OK 而技能毫无变化 —— 那层判定在 `skill-update` op 里。
229
- description: 'Create a DSH skill under DSH_HOME/tool-management/skills, or update the one that already has this name — the receipt says which. Only skills this plugin wrote can be updated. Use only when the user asks to save a reusable skill.',
314
+ // 落点必须和 UI「创建技能」一致 —— 这是本工具与面板之间唯一不能各自默认的地方(否则同一个
315
+ // 「新建技能」动作,人点界面和模型调用会落到两个不同的根)。当前两边都是 **DSH 技能目录**
316
+ // (`~/.dsh/skills/`):UI 是 `43-skills.js` 的 `defaultCreateRoot`,模型侧就是下面这句
317
+ // `skill-create` 的 op 默认值。hub(`tool-management/skills/`)只留给「导入技能」功能
318
+ // (用户裁定 2026-10-15)。历史上两边曾一个 dsh 一个 hub,靠本条纪律对齐。
319
+ //
320
+ // 改的分支**认全部来源里的胜出者**:同名技能在 dsh/hub/项目根各有一份时只有一份生效,
321
+ // 改错那一份会返回 OK 而技能毫无变化 —— 那层判定在 `skill-update` op 里,它同时要求
322
+ // 胜出者落在本插件可写的两个根(hub 导入落点 / dsh 新建落点)。也就是说:本工具建出来
323
+ // 的技能(只落在 dsh)一定还能被本工具改回去,不会建完就改不动。
324
+ //
325
+ // 描述里的落点路径(`under DSH_HOME/tool-management/skills`)删掉(2026-10-15 注入文本
326
+ // 审查):模型不按路径行动,而这段路径还与本轮 `remove.ts` 头注释写的落点口径不一
327
+ // (那里写 hub 的 tool-management/skills/)。真正要它知道的是"建在哪一侧",
328
+ // 而"只认本插件写的技能"那半句已经说清了。
329
+ description: 'Create a DSH skill, or update the one that already has this name — the receipt says which. Only skills this plugin wrote can be updated. Only on the user\'s instruction.',
230
330
  parameters: {
231
- name: { type: 'string', required: true, description: 'Skill name; normalized to kebab-case.' },
232
- description: { type: 'string', description: 'A concise routing description. Required when creating; omit = keep.' },
233
- body: { type: 'string', description: 'Markdown instructions. Required when creating; omit = keep.' },
331
+ name: { type: 'string', required: true, description: 'Skill name; normalized to kebab-case (≤128 chars, [a-z0-9-]).' },
332
+ description: { type: 'string', description: 'A concise routing description (≤500 chars). Required when creating; omit = keep.' },
333
+ body: { type: 'string', description: 'Markdown instructions (≤256 KiB). Required when creating; omit = keep.' },
234
334
  },
235
335
  output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
236
336
  async execute(args) {
@@ -272,7 +372,14 @@ export function buildSkillTools(deps) {
272
372
  if (!r || r.ok === false)
273
373
  throw new Error((r && r.error) || 'skill create failed');
274
374
  const data = r.data || {};
275
- return 'Created DSH skill ' + (data.name || kebab) + ' at ' + (data.path || '(unknown)');
375
+ // 新建的技能默认停用(与 MCP / 人设同口径)。这句必须留 —— 不说的话模型会直接去用,
376
+ // 然后发现技能不生效,而它不知道自己少拨了一个开关(smoke-test 报告 P1-3:三个 save
377
+ // 里只有这一条不报启用状态)。点名的 `skill_manager_switch` 在出厂默认名单里就是关着的,
378
+ // 关着就改说"用户启用后才能用",别指一条模型没有的工具。
379
+ const enableHint = deps.toolVisible('skill_manager_switch')
380
+ ? '(默认未启用,要启用先 skill_manager_switch)'
381
+ : '(默认未启用,用户启用后才能用)';
382
+ return 'Created DSH skill ' + (data.name || kebab) + ' at ' + (data.path || '(unknown)') + enableHint;
276
383
  }
277
384
  // 改的分支:省略 = 保持。两个可改字段一个都没给就拒 —— op 会按"保持原样"重写一次文件
278
385
  // 并刷新时间戳,模型以为改了点什么,其实只是把同一份内容又写了一遍。
@@ -7,6 +7,24 @@
7
7
  // 「是哪一个没注册上」——只打一行日志时,模型侧只会「查无此工具」、界面毫无痕迹。
8
8
  import { defineSubagentManagerListTool, defineSubagentManagerRunTool } from '../subagents/tools.js';
9
9
  import { text } from './deps.js';
10
+ /**
11
+ * 高频高级字段:`subagent_manager_save` 与 op 之间原样透传的那几个(省略 = 不出现 = op 侧保持)。
12
+ *
13
+ * 2026-10-15:从"面板专有"里挑出**配置人设时最常改的三类**交给模型 —— `model`(换模型)、
14
+ * `continuable`(允许追问)、`tools` / `toolsDeny`(工具黑白名单)。此前模型看得见却改不了
15
+ * (`subagent_manager_save` 只读回它们防止冲掉,旧注释把这条写成"留给面板")。
16
+ *
17
+ * **刻意不加** `provider` / `reasoningEffort` / `catalogDepth`:都是低频(用户裁定
18
+ * "使用频率高的才需要,太多功能反而冗余、浪费 token")。`toolsByPreset`(嵌套对象)同理。
19
+ */
20
+ const ADVANCED_FIELDS = ['model', 'continuable', 'tools', 'toolsDeny'];
21
+ function advancedArgs(args) {
22
+ const out = {};
23
+ for (const k of ADVANCED_FIELDS)
24
+ if (args && args[k] !== undefined)
25
+ out[k] = args[k];
26
+ return out;
27
+ }
10
28
  export function buildSubagentTools(deps) {
11
29
  const { defineTool, register } = deps;
12
30
  const recordFailure = (name, e) => {
@@ -51,14 +69,16 @@ export function buildSubagentTools(deps) {
51
69
  // 宿主没挂 `dsh-subagent-*` 也照样该能用(恰恰那时更需要:能把人设建好、开关拨对,
52
70
  // 等 provider 回来就可用)。所以各自单独 try/catch,不与 `_run` 绑在一起失败。
53
71
  //
54
- // 参数表刻意只给 name / description / body / output 四个:工具限制(tools / toolsDeny /
55
- // toolsByPreset)与 provider/model 留给面板。理由不是"怕模型不会填",而是填错的后果
56
- // 不对称 —— 一个错误的 toolsDeny 会静默改变子代理能做什么,而模型看不到自己改对了没有。
72
+ // 参数表(2026-10-15 扩):基础四项(name / description / body / output)之外,把高级字段
73
+ // 也接上 —— 此前工具限制(tools / toolsDeny)与 provider / model / reasoningEffort /
74
+ // catalogDepth / continuable 只读回防止冲掉、模型改不了,于是"给这个人设换个便宜模型"
75
+ // 这类要求模型手里没有出口,描述里也没写"去面板"。现在能设的都在参数里说清,
76
+ // 只留 `toolsByPreset`(嵌套对象,schema 带不动)给面板。
57
77
  const ops = () => deps.subagentService.ops;
58
78
  try {
59
79
  register(deps.defineTool({
60
80
  name: 'subagent_manager_switch',
61
- description: 'Enable or disable one persona. Only enabled personas appear in the「可委派的子智能体」catalog and can be given work. Reversible, and the file is untouched. Only when the user asks or approves.',
81
+ description: 'Enable or disable one persona. Only enabled personas appear in the「当前子智能体」catalog and can be given work. Reversible. Only when the user asks or approves.',
62
82
  parameters: {
63
83
  name: { type: 'string', required: true, description: 'Persona name.' },
64
84
  enabled: { type: 'boolean', required: true, description: 'true = enable, false = disable; omission is refused.' },
@@ -93,13 +113,21 @@ export function buildSubagentTools(deps) {
93
113
  //
94
114
  // 0.14.0:create 与 update 并成一条 upsert。判定也只能在这里做 —— `subagent-create`
95
115
  // 对同名是拒绝的、`subagent-update` 对不存在的名字报错,而模型手里的目录可能已经过期。
96
- description: 'Create a persona at ~/.dsh/tool-management/subagents/<name>.md, or update the one that already has this name — the receipt says which. Omitted fields keep their current values. Write `body` as who this is and how it judges its own work — not the steps or paths, those belong to the task; put hard output requirements in `output` (one per line), not buried in prose. Only on the user\'s explicit request.',
116
+ //
117
+ // 落点路径(`at ~/.dsh/tool-management/subagents/<name>.md`)删掉(2026-10-15 注入文本
118
+ // 审查):模型不按路径行动,这条工具也没有"给我路径"的用法(要读正文有 `list` +
119
+ // 宿主的文件工具,而人设正文本来就是给它自己当系统提示词用的)。
120
+ description: 'Create a persona, or update the one that already has this name — the receipt says which. Omitted fields keep their current values. Write `body` as who this is and how it judges its own work — not the steps or paths, those belong to the task; put hard output requirements in `output` (one per line), not buried in prose. The three settings changed most often are settable here too: `model` (route this persona to another model; \'\' = inherit), `continuable` (allow follow-up), and the tool allow/deny lists `tools`/`toolsDeny` ([] = clear). Other routing/limit fields stay panel-only. Blocked while the active scene is locked. Only on the user\'s instruction.',
97
121
  parameters: {
98
- name: { type: 'string', required: true, description: 'Persona name (= file name); ≤64 chars, no path separators or < > : " | ? *.' },
122
+ name: { type: 'string', required: true, description: 'Persona name (= file name); ≤64 chars, one path segment — no / \\ or < > : " | ? *, no leading dot, no reserved device name, no trailing dot or space.' },
99
123
  description: { type: 'string', description: 'One-line routing description — this is what shows up in the delegation catalog. Required when creating; omit = keep.' },
100
124
  body: { type: 'string', description: 'Markdown role definition. Required when creating; omit = keep.' },
101
125
  output: { type: 'string', description: 'Output contract, one requirement per line. Empty string clears it; omit = keep.' },
102
126
  nextName: { type: 'string', description: 'New name (rename); re-points scene-profile bindings. Omit = keep. Refused if a persona already has that name.' },
127
+ model: { type: 'string', description: "Model id for this persona's runs. '' = inherit the main session's model; omit = keep." },
128
+ continuable: { type: 'boolean', description: 'true = this persona may be followed up (runs in the background and returns an agent id); false = one-shot. Omit = keep.' },
129
+ tools: { type: 'array', items: { type: 'string' }, description: 'Tool allowlist — the subagent keeps only these tools. [] = clear; omit = keep.' },
130
+ toolsDeny: { type: 'array', items: { type: 'string' }, description: 'Tool denylist — removed from the subagent; wins over the allowlist. [] = clear; omit = keep.' },
103
131
  },
104
132
  output: { schema: { type: 'string' }, render: (_a, v) => text(String(v)) },
105
133
  async execute(args) {
@@ -107,6 +135,16 @@ export function buildSubagentTools(deps) {
107
135
  if (blocked)
108
136
  throw new Error(blocked);
109
137
  const name = String((args && args.name) || '').trim();
138
+ // 高级字段的形状校验:填错会在 op 侧**静默**(`serializePersona` 对非数组 `tools`
139
+ // 是"当空处理",等于把用户原有的名单清掉)。在这里先挡一句。
140
+ if (args && args.continuable !== undefined && typeof args.continuable !== 'boolean') {
141
+ throw new Error('continuable 必须是 true 或 false');
142
+ }
143
+ for (const k of ['tools', 'toolsDeny']) {
144
+ if (args && args[k] !== undefined && !Array.isArray(args[k])) {
145
+ throw new Error(k + ' 必须是字符串数组([] = 清空)');
146
+ }
147
+ }
110
148
  const current = await ops()['subagent-get']({ name });
111
149
  if (!current || current.ok === false) {
112
150
  // 建的分支:description 与 body 都必填。`subagent-create` 自己会拒(人设名不合法 /
@@ -119,7 +157,7 @@ export function buildSubagentTools(deps) {
119
157
  ].filter((x) => x !== '');
120
158
  if (missing.length)
121
159
  throw new Error('新建人设必须给 ' + missing.join(' 与 ') + '(' + name + ' 还不存在)');
122
- const r = await ops()['subagent-create']({ name, description: args.description, body: args.body, output: args.output });
160
+ const r = await ops()['subagent-create']({ name, description: args.description, body: args.body, output: args.output, ...advancedArgs(args) });
123
161
  if (!r || r.ok === false)
124
162
  throw new Error((r && r.error) || '创建人设失败');
125
163
  // 新建的人设默认停用(与 MCP / 技能同口径)。这句必须留:不说的话模型会直接去委派,
@@ -132,35 +170,31 @@ export function buildSubagentTools(deps) {
132
170
  return 'OK: persona ' + String(r.name || name) + ' created' + enableHint;
133
171
  }
134
172
  const keep = current.persona || {};
173
+ // 省略 = 保持(读回现值),给了 = 覆盖。高级字段走同一套 —— 不在工具侧合并,
174
+ // `subagent-update` 的整份重写就会把没给的那些冲掉。
175
+ const pick = (k, fallback) => (args && args[k] !== undefined ? args[k] : fallback);
135
176
  const touched = [];
136
- if (args.nextName !== undefined)
137
- touched.push('nextName');
138
- if (args.description !== undefined)
139
- touched.push('description');
140
- if (args.body !== undefined)
141
- touched.push('body');
142
- if (args.output !== undefined)
143
- touched.push('output');
177
+ for (const k of ['nextName', 'description', 'body', 'output', ...ADVANCED_FIELDS]) {
178
+ if (args && args[k] !== undefined)
179
+ touched.push(k);
180
+ }
144
181
  if (!touched.length)
145
- throw new Error('没有要改的东西:nextName / description / body / output 至少给一个');
182
+ throw new Error('没有要改的东西:description / body / output / nextName 或任一高级字段至少给一个');
146
183
  const merged = {
147
184
  name,
148
185
  nextName: args.nextName,
149
- description: args.description !== undefined ? String(args.description) : String(keep.description ?? ''),
150
- body: args.body !== undefined ? String(args.body) : String(keep.body ?? ''),
151
- output: args.output !== undefined ? String(args.output) : (Array.isArray(keep.output) ? keep.output.join('\n') : String(keep.output ?? '')),
152
- provider: keep.provider ?? '',
153
- model: keep.model ?? '',
154
- // ⚠️ 必须带回来:`subagent-update` 走 `serializePersona` **整份重写**,少带一个字段
155
- // 就是静默清掉它。`reasoningEffort` 是 0.14.0 新增的,最容易在这里被漏掉 ——
156
- // 模型只改一句 body,用户配好的思考强度就没了,而且没有任何提示。
157
- reasoningEffort: keep.reasoningEffort ?? '',
158
- // 0.19.0 同理:`continuable` 不在本工具的参数表里(与工具限制、provider/model 同一档 ——
159
- // 它在面板上配),所以这里**只负责别把它冲掉**。漏了它的后果是:模型改一次描述,
160
- // 用户设好的"允许追问"就静默变回一次性,而回执照样说成功。
161
- continuable: keep.continuable === true,
162
- tools: keep.tools ?? [],
163
- toolsDeny: keep.toolsDeny ?? [],
186
+ description: pick('description', String(keep.description ?? '')),
187
+ body: pick('body', String(keep.body ?? '')),
188
+ output: pick('output', Array.isArray(keep.output) ? keep.output.join('\n') : String(keep.output ?? '')),
189
+ // ⚠️ 不加参数的三项(provider / reasoningEffort / catalogDepth)**只负责别冲掉**:
190
+ // `subagent-update` 走 `serializePersona` 整份重写,少带一个就是静默清掉它。
191
+ provider: String(keep.provider ?? ''),
192
+ reasoningEffort: String(keep.reasoningEffort ?? ''),
193
+ model: pick('model', String(keep.model ?? '')),
194
+ continuable: pick('continuable', keep.continuable === true) === true,
195
+ tools: pick('tools', keep.tools ?? []),
196
+ toolsDeny: pick('toolsDeny', keep.toolsDeny ?? []),
197
+ // `toolsByPreset` 也留给面板(嵌套对象,schema 带不动):这里只负责别把它冲掉。
164
198
  toolsByPreset: keep.toolsByPreset ?? {},
165
199
  catalogDepth: keep.catalogDepth,
166
200
  };
@@ -5,7 +5,7 @@
5
5
  // 发出,哪怕这一轮根本用不上。关掉某几个工具,它们整份不进请求(不是"把描述写短点"那种省);
6
6
  // 面板(118 个 op)与注入通道完全不受影响。
7
7
  //
8
- // 出厂**默认就关着十五条**(20 条里实发只剩 5 条)—— 名单、判据与代价见下面
8
+ // 出厂**默认就关着十四条**(20 条里实发 6 条)—— 名单、判据与代价见下面
9
9
  // `DEFAULT_HIDDEN_TOOLS`;用户自己额外关的与出厂关的在合计那句里分开报。
10
10
  //
11
11
  // ⚠️ **那个口径不是 token 数,别拿它跟别的数直接比**(2026-09-23 用真 BPE 词表实测):
@@ -30,19 +30,29 @@
30
30
  // · 注入通道与两个目录:目录里「用 `skill_manager_list` 查」这类句子在工具关掉后就是假的。
31
31
  import { INJECT_DOMAIN_KEYS, domainOfTool } from '../context-inject.js';
32
32
  /**
33
- * 出厂默认关掉的十五条(0.14.0):整张表 20 条 ≈3,769 tok,关掉后**每轮实发 5 条 ≈1,090**(省 ≈2,679)。
33
+ * 出厂默认关掉的**十九条**:整张表 25 条,关掉后**每轮实发 6 条**。
34
34
  *
35
35
  * 判据是用户 2026-09-23 的三轮裁定,同一条:**这些信息或动作常态下由注入段与面板承担,
36
36
  * 平时也用不到模型自己伸手**。三组:
37
37
  * · **写侧五条** —— 三个 `*_save`(MCP / 技能 / 人设:配置本体在面板上改)+ 提示词两条
38
- * (这一族只有两条,而当前生效的那份本来就在「本机提示词」注入段里)。
39
- * · **读侧四条** —— 三个 `*_list` + `memory_manager_read`。会话开头注入的那几段目录已经写了
40
- * 当前有什么,模型不必每轮再带一份查询工具。
38
+ * (这一族只有两条,而当前生效的那份本来就在「当前提示词」注入段里)。
39
+ * · **读侧三条** —— 两个 `*_list`(MCP / 技能)+ `memory_manager_read`。会话开头注入的那几段
40
+ * 目录已经写了当前有什么,模型不必每轮再带一份查询工具。
41
41
  * · **场景三条 + 开关三条** —— 场景一族(建 / 勾档案 / 进入退出)与 `skill` / `memory` /
42
42
  * `subagent` 三个 `_switch`(用户:「平时使用也很少」)。启停在面板上都有同款,注入段也
43
43
  * 写着当前开着哪些。**`mcp_manager_switch` 刻意留着**:它是"这台先给我关了"这条最常用的
44
44
  * 临时动作的唯一入口,也是 MCP 域留下来的一条腿。
45
45
  *
46
+ * 2026-10-14(对抗性审查 P0-2):**`subagent_manager_list` 从这份名单里拿了出来**(十四条)。
47
+ * 判据与上面那条"注入段承担"不同 —— 人设域**没有官方载体兜底**:MCP / 技能 / 记忆三个域的
48
+ * 目录即便被压制型预设压掉,也各自还有一条常驻发现路径,而人设目录一旦不注入,模型手里就
49
+ * 只剩 `subagent_manager_run` 一条可见工具、**无从知道任何人设名**(死路:有工具、没输入)。
50
+ * 它只有 42 tok,买的是把"注入段是唯一发现路径"这个单点消掉。
51
+ *
52
+ * 2026-10-15:**删除 / 回收站族五条**(`*_manager_remove`)出厂默认关。判据是"删一条记忆 /
53
+ * 技能 / 人设"是常问的,但它**低频于"读现状 + 拨开关 + 建改定义"**那批,而且面板每个域都有
54
+ * 删除入口 —— 默认关、要用时在「兼容」页勾上(与另外十四条同一条纪律,不额外付 token)。
55
+ *
46
56
  * ⚠️ 关掉的是**能力**,不是"冗余",代价如实记在这里(要拿回来就在「兼容」页勾,或整批换:
47
57
  * 「恢复方案」):注入的目录是**筛过的视图**(MCP 段只列可用/曾可用的、人设目录只列本场景
48
58
  * 绑定且深度够的、技能目录只列可调用的),所以"没开的那几台 / 被同名覆盖的那份技能 / 没进
@@ -59,20 +69,24 @@ export const DEFAULT_HIDDEN_TOOLS = Object.freeze([
59
69
  'scene_manager_list',
60
70
  'scene_manager_switch',
61
71
  'scene_manager_save',
72
+ 'scene_manager_remove',
62
73
  'memory_manager_read',
63
74
  'memory_manager_switch',
75
+ 'memory_manager_remove',
64
76
  'mcp_manager_list',
65
77
  'mcp_manager_save',
66
78
  'skill_manager_list',
67
79
  'skill_manager_switch',
68
80
  'skill_manager_save',
69
- 'subagent_manager_list',
81
+ 'skill_manager_remove',
70
82
  'subagent_manager_switch',
71
83
  'subagent_manager_save',
84
+ 'subagent_manager_remove',
72
85
  'prompt_manager_list',
73
86
  'prompt_manager_apply',
87
+ 'prompt_manager_remove',
74
88
  ]);
75
- /** 出厂默认那份设置:十五条不发(名单与理由见上面),方案清单是空的。 */
89
+ /** 出厂默认那份设置:十四条不发(名单与理由见上面),方案清单是空的。 */
76
90
  export const DEFAULT_TOOL_TABLE_SETTINGS = { hidden: [...DEFAULT_HIDDEN_TOOLS], presets: [] };
77
91
  /** 方案名上限(与 MCP 备注那类输入同风格:短到能在一行里读,长了也没人看)。 */
78
92
  export const PRESET_NAME_MAX_LENGTH = 40;
@@ -232,13 +246,19 @@ export function estimateTok(bytes) {
232
246
  return Math.ceil(value / CHARS_PER_TOKEN);
233
247
  }
234
248
  /**
235
- * 纯函数:把注册时量到的体积按域分组,标出哪些被关掉,给出三档合计。
249
+ * 纯函数:把注册时量到的体积按域分组,标出哪些被关掉,给出四档合计。
236
250
  *
237
251
  * 顺序:域按 `INJECT_DOMAIN_KEYS`(界面与注入通道同一顺序),域内按**注册顺序**
238
252
  * —— 改顺序等于改显示顺序,不额外定义"重要性",免得出现第二份排序真相。
253
+ *
254
+ * `carrierHidden` 是"官方载体在场时我们主动让位"的名字(当前只有 `skill_manager_read`)。
255
+ * 它是 **per-agent** 的(取决于那个 agent 的 scope 解析得到解析不到官方 `skill`),而报表是
256
+ * 全局的 —— 所以由调用方给一份口径明确的名单(`tool-table-switch.ts` 传的是"标准预设下会
257
+ * 让位"的保守估计),本函数只负责把它从**可见**里扣掉、单独计一档。
239
258
  */
240
- export function buildToolTableReport(rows, hidden) {
259
+ export function buildToolTableReport(rows, hidden, carrierHidden = []) {
241
260
  const off = new Set(hidden.map((name) => String(name)));
261
+ const carried = new Set(carrierHidden.map((name) => String(name)));
242
262
  const buckets = new Map();
243
263
  for (const key of INJECT_DOMAIN_KEYS)
244
264
  buckets.set(key, []);
@@ -263,6 +283,8 @@ export function buildToolTableReport(rows, hidden) {
263
283
  let totalCount = 0;
264
284
  let defaultHiddenCount = 0;
265
285
  let defaultHiddenTok = 0;
286
+ let carrierHiddenCount = 0;
287
+ let carrierHiddenTok = 0;
266
288
  for (const [key, tools] of buckets) {
267
289
  if (!tools.length)
268
290
  continue;
@@ -278,6 +300,11 @@ export function buildToolTableReport(rows, hidden) {
278
300
  defaultHiddenTok += tool.tok;
279
301
  }
280
302
  }
303
+ else if (carried.has(tool.name)) {
304
+ // 让位那一档:**不发,但不是被谁关掉的** —— 单独计,绝不并进 visible。
305
+ carrierHiddenCount += 1;
306
+ carrierHiddenTok += tool.tok;
307
+ }
281
308
  else {
282
309
  visibleTok += tool.tok;
283
310
  visibleCount += 1;
@@ -287,8 +314,8 @@ export function buildToolTableReport(rows, hidden) {
287
314
  groups.push({ key, tools, tok, hiddenTok: offTok });
288
315
  }
289
316
  return {
290
- groups, visibleTok, hiddenTok, totalTok: visibleTok + hiddenTok,
291
- visibleCount, hiddenCount: totalCount - visibleCount, totalCount,
292
- defaultHiddenCount, defaultHiddenTok,
317
+ groups, visibleTok, hiddenTok, totalTok: visibleTok + hiddenTok + carrierHiddenTok,
318
+ visibleCount, hiddenCount: totalCount - visibleCount - carrierHiddenCount, totalCount,
319
+ defaultHiddenCount, defaultHiddenTok, carrierHiddenCount, carrierHiddenTok,
293
320
  };
294
321
  }