dsh-plugin-tool-management 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +212 -201
- package/README_EN.md +206 -199
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +40 -1
- package/lib/client.js +6220 -5929
- package/lib/compat/probe.js +0 -5
- package/lib/context-inject.js +385 -53
- package/lib/history/bridge.js +7 -0
- package/lib/history/tombstone.js +1 -2
- package/lib/hub.js +3 -1
- package/lib/index.js +306 -48
- package/lib/mcp/override-blocks.js +10 -6
- package/lib/mcp/state-section.js +64 -21
- package/lib/rules/archive-engine.js +64 -9
- package/lib/rules/archive.js +5 -6
- package/lib/rules/service.js +147 -40
- package/lib/skills/catalog.js +6 -9
- package/lib/skills/core.js +5 -4
- package/lib/subagents/catalog.js +31 -15
- package/lib/subagents/service.js +307 -34
- package/lib/subagents/tools.js +29 -4
- package/package.json +105 -102
- package/lib/history/projcache.js +0 -335
package/lib/index.js
CHANGED
|
@@ -10,18 +10,19 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Build: `tsc -p tsconfig.json` compiles this to lib/index.js (the shipped
|
|
12
12
|
// artifact — same convention as DSH's own packages, which ship compiled JS).
|
|
13
|
-
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
|
+
import { defineTool as hostDefineTool } from '@deepseek-ai/dsh-tools';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
|
-
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
16
16
|
import { ArchiveWorkspaceRegistry as HistoryService, workspaceBaseName, workspacePathKey } from './history/workspace.js';
|
|
17
17
|
import { createSkillsService, pluginLog } from './skills/service.js';
|
|
18
|
+
import { renameWithRetry } from './skills/core.js';
|
|
18
19
|
import { createAgentsMdService } from './agents-md/service.js';
|
|
19
20
|
import { createRulesService, planMemoryExport } from './rules/service.js';
|
|
20
21
|
import { createArchiveEngine } from './rules/archive-engine.js';
|
|
21
22
|
import { createSubagentService, decideToolFilter, defaultPersonasDir } from './subagents/service.js';
|
|
22
23
|
import { createSubagentCatalog } from './subagents/catalog.js';
|
|
23
24
|
import { createSkillCatalog } from './skills/catalog.js';
|
|
24
|
-
import { DEFAULT_INJECT_SETTINGS, createContextInjector, normalizeInjectSettings, } from './context-inject.js';
|
|
25
|
+
import { DEFAULT_INJECT_SETTINGS, createContextInjector, normalizeInjectSettings, subagentDepthOf, } from './context-inject.js';
|
|
25
26
|
import { isApprovalNever } from './approval-policy.js';
|
|
26
27
|
import { EXPECTED_MIN_HOST_VERSION, EXPECTED_PEER_RANGE, VERIFIED_HOST_VERSION, summarize } from './compat/probe.js';
|
|
27
28
|
import { assessPresetReach, injectionFactsOf, presetRosterOf, readCompositionFacts, reachNoticeForAgent } from './compat/preset-reach.js';
|
|
@@ -34,7 +35,7 @@ import { defineSubagentManagerListTool, defineSubagentManagerRunTool } from './s
|
|
|
34
35
|
import { detectFormat, extractText, parseGenericText, parseJsonlTranscript, parseMarkdownTranscript } from './imports/parsers.js';
|
|
35
36
|
import { homedir } from 'node:os';
|
|
36
37
|
import { spawn } from 'node:child_process';
|
|
37
|
-
import { mkdir, readdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';
|
|
38
|
+
import { mkdir, readdir, readFile, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
|
38
39
|
import { basename, dirname, join } from 'node:path';
|
|
39
40
|
import { zipSync } from 'fflate';
|
|
40
41
|
/**
|
|
@@ -154,6 +155,10 @@ export default {
|
|
|
154
155
|
// is not an injected service and throws "cannot get property without
|
|
155
156
|
// inject" at boot.
|
|
156
157
|
const TOKEN = String(config?.token || process.env.DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN || '').trim();
|
|
158
|
+
// 令牌比较走「双侧 sha256 → timingSafeEqual」:摘要定长 32 字节,无需长度分支
|
|
159
|
+
// (直接比较不等长 Buffer 会抛),逐字节的耗时不随匹配前缀长度变化 —— JS 的 ===
|
|
160
|
+
// 逐字符短路,理论上可被计时侧信道逐位猜测。
|
|
161
|
+
const tokenMatches = (presented) => timingSafeEqual(createHash('sha256').update(presented).digest(), createHash('sha256').update(TOKEN).digest());
|
|
157
162
|
const wait = (ms) => ctx.timeout(ms);
|
|
158
163
|
const message = (e) => String((e && e.message) || e);
|
|
159
164
|
let writeChain = Promise.resolve();
|
|
@@ -572,7 +577,7 @@ export default {
|
|
|
572
577
|
await subagentService.enabledStore.setEnabled(on, true);
|
|
573
578
|
void subagentCatalog.refresh();
|
|
574
579
|
},
|
|
575
|
-
// 记忆 id
|
|
580
|
+
// 记忆 id 全集。引擎已不消费(memories 段 P5 起废弃),实现保留以维持接口形状。
|
|
576
581
|
knownMemoryIds: async () => {
|
|
577
582
|
const r = await rulesService.ops['rules-list']({});
|
|
578
583
|
return new Set(((r && r.rules) || []).filter((x) => !x.shadowed).map((x) => String(x.id)));
|
|
@@ -700,6 +705,9 @@ export default {
|
|
|
700
705
|
// 注入实况(只读诊断):注入器的 live() 读回"最近活跃会话"可见表面上的五域文本。
|
|
701
706
|
// 挂在 apply 作用域:`injection-live` op 与注入器不在同一层(effect 内部)。
|
|
702
707
|
let contextInjectorLive = null;
|
|
708
|
+
// 采纳遥测的入口(同样是 apply 作用域的中转):工具注册在 effect 之外,
|
|
709
|
+
// 拿不到 effect 内部的 `injector`;经这个 `let` 中转,热更新重建注入器也能跟上。
|
|
710
|
+
let contextInjectorNote = null;
|
|
703
711
|
try {
|
|
704
712
|
ctx.effect(() => {
|
|
705
713
|
const injector = createContextInjector({
|
|
@@ -707,18 +715,43 @@ export default {
|
|
|
707
715
|
// 顺序即界面勾选与消息顺序(用户裁定 2026-09-16):场景和记忆 → MCP → 技能 →
|
|
708
716
|
// 子智能体 → 提示词。form 是宿主语义轴:记忆是「当前状态」(snapshot,后发取代先发),
|
|
709
717
|
// 三个目录是 catalog,提示词是 instructions(与官方 AGENTS.md 那条行同一形态)。
|
|
718
|
+
//
|
|
719
|
+
// `applicableTo`(2026-09-17 方案 A):子智能体域按"**目录该不该注入到这么深的
|
|
720
|
+
// 会话**"(人设的 `catalogDepth`,默认 1 = 只在顶层注入)判断,**不是**"能不能
|
|
721
|
+
// 委派"。委派可行性由官方决定:`dsh-tool-subagent` 默认 `maxDepth: 3`,provider
|
|
722
|
+
// 只在传了该值时才校验,所以子代理本来就能继续嵌套(用户实测确认)。此前判据叫
|
|
723
|
+
// "还有没有委派预算"、语义是"还能不能委派",那是错的 —— 详见 service.ts 里
|
|
724
|
+
// `catalogDepth` 字段的注释。目录正文也按同一个判据过滤(`text(agent)`),两处同源
|
|
725
|
+
// 所以不会分叉。
|
|
726
|
+
//
|
|
727
|
+
// 记忆域(2026-09-17 用户裁定):**只在顶层注入**。记忆是"父会话的现场",不是子代理
|
|
728
|
+
// 完成任务所需的事实 —— 而且它带着「一律照办,覆盖你的默认做法」这种强主张,塞进
|
|
729
|
+
// 一次性子会话只会与角色定义争注意力(实测:子代理跑审查时,上下文里同时躺着人设与
|
|
730
|
+
// 整份场景记忆)。子代理手里有 `memory_manager_list/read`,需要什么自己取;父代理
|
|
731
|
+
// 上下文里也有记忆,相关事实应当由它写进 `task`(子代理的上下文 = 角色 + 任务)。
|
|
732
|
+
// 其余三域对任何深度都成立:提示词是用户规则(本插件的立身之本就是"覆盖到子代理")、
|
|
733
|
+
// 技能目录与 MCP 状态是"操作这台机器所需的事实"(子代理手里就有 `skill` / `mcp__*`
|
|
734
|
+
// 工具,不知道清单就只能瞎调)。
|
|
710
735
|
domains: () => [
|
|
711
|
-
{ key: 'memory', name: 'tool-management:scene-memory', label: '场景和记忆', form: 'snapshot', text: () => rulesService.memoryText() },
|
|
736
|
+
{ key: 'memory', name: 'tool-management:scene-memory', label: '场景和记忆', form: 'snapshot', text: () => rulesService.memoryText(), applicableTo: (agent) => subagentDepthOf(agent) === 0 },
|
|
712
737
|
{ key: 'mcp', name: 'tool-management:mcp-state', label: 'MCP 服务器', form: 'catalog', text: () => mcpStateCatalog.text() },
|
|
713
738
|
{ key: 'skills', name: 'tool-management:skill-catalog', label: '技能目录', form: 'catalog', text: () => skillCatalog.text() },
|
|
714
|
-
{
|
|
715
|
-
|
|
739
|
+
{
|
|
740
|
+
key: 'subagents',
|
|
741
|
+
name: 'tool-management:subagents',
|
|
742
|
+
label: '子智能体',
|
|
743
|
+
form: 'catalog',
|
|
744
|
+
text: (agent) => subagentCatalog.text(agent),
|
|
745
|
+
applicableTo: (agent) => subagentCatalog.catalogVisibleAt(subagentDepthOf(agent)),
|
|
746
|
+
},
|
|
747
|
+
{ key: 'prompt', name: 'tool-management:prompt', label: '提示词', form: 'instructions', text: () => rulesService.promptText(), files: () => rulesService.promptFiles() },
|
|
716
748
|
],
|
|
717
749
|
settings: () => injectSettingsSync(),
|
|
718
750
|
factsFor: (agent) => presetFactsForAgent(agent),
|
|
719
751
|
});
|
|
720
752
|
contextInjectorLive = () => injector.live();
|
|
721
|
-
|
|
753
|
+
contextInjectorNote = (toolName, agent) => injector.noteToolUse(toolName, agent);
|
|
754
|
+
return () => { contextInjectorLive = null; contextInjectorNote = null; injector.dispose(); };
|
|
722
755
|
}, 'dsh-plugin-tool-management: context injection');
|
|
723
756
|
}
|
|
724
757
|
catch (e) {
|
|
@@ -783,12 +816,51 @@ export default {
|
|
|
783
816
|
return 0;
|
|
784
817
|
}
|
|
785
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* 人设改名后同步运行时快照(F-025):退出还原按 `subagentsAll` 逐名走,快照里
|
|
821
|
+
* 还留着旧名的话,退出会「旧名静默跳过、新名不还原」—— 被改名的人设停留在
|
|
822
|
+
* 场景期间状态。快照在进入场景时拍、改名发生在进入后,只能在改名时跟着改。
|
|
823
|
+
*/
|
|
824
|
+
async function renameSubagentInSnapshot(from, to) {
|
|
825
|
+
try {
|
|
826
|
+
const slice = await rulesService.readArchiveSlice();
|
|
827
|
+
const mode = slice && slice.mode;
|
|
828
|
+
const snapshot = mode && mode.snapshot;
|
|
829
|
+
if (!snapshot)
|
|
830
|
+
return;
|
|
831
|
+
let changed = false;
|
|
832
|
+
const next = { ...snapshot };
|
|
833
|
+
if (snapshot.subagentsAll && Object.prototype.hasOwnProperty.call(snapshot.subagentsAll, from)) {
|
|
834
|
+
const all = {};
|
|
835
|
+
for (const [k, v] of Object.entries(snapshot.subagentsAll)) {
|
|
836
|
+
all[k === from ? to : k] = v;
|
|
837
|
+
if (k === from)
|
|
838
|
+
changed = true;
|
|
839
|
+
}
|
|
840
|
+
next.subagentsAll = all;
|
|
841
|
+
}
|
|
842
|
+
for (const field of ['subagents', 'subagentsOn']) {
|
|
843
|
+
const list = snapshot[field];
|
|
844
|
+
if (Array.isArray(list) && list.includes(from)) {
|
|
845
|
+
next[field] = list.map((n) => (n === from ? to : n));
|
|
846
|
+
changed = true;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (changed)
|
|
850
|
+
await rulesService.patchIndex({ mode: { ...mode, snapshot: next } });
|
|
851
|
+
}
|
|
852
|
+
catch { /* 快照同步失败不阻断改名本身;残留与修复前一致 */ }
|
|
853
|
+
}
|
|
786
854
|
const baseSubagentUpdate = subagentService.ops['subagent-update'];
|
|
787
855
|
if (typeof baseSubagentUpdate === 'function') {
|
|
788
856
|
subagentService.ops['subagent-update'] = async (args) => {
|
|
789
857
|
const res = await baseSubagentUpdate(args);
|
|
790
|
-
if (res && res.ok !== false && res.renamedFrom)
|
|
791
|
-
|
|
858
|
+
if (res && res.ok !== false && res.renamedFrom) {
|
|
859
|
+
const from = String(res.renamedFrom);
|
|
860
|
+
const to = String(res.name);
|
|
861
|
+
void rebindSubagentInArchives(from, to);
|
|
862
|
+
void renameSubagentInSnapshot(from, to);
|
|
863
|
+
}
|
|
792
864
|
return res;
|
|
793
865
|
};
|
|
794
866
|
}
|
|
@@ -883,6 +955,10 @@ export default {
|
|
|
883
955
|
'mcpm-add', 'mcpm-edit', 'mcpm-remove', 'mcpm-set-enabled', 'mcpm-set-all', 'mcpm-restart',
|
|
884
956
|
'mcpm-compact',
|
|
885
957
|
'mcpm-export', 'mcpm-import', 'mcpm-note', 'mcpm-settings', 'mcpm-tool-enabled',
|
|
958
|
+
// mcpm-tools-refresh 为了拿实时工具表会临时启用目标服务器、结束后恢复原状(两次
|
|
959
|
+
// writePatch),恢复失败还会停留在启用态 —— 是写不是读,按写门禁(与 mcpm-restart
|
|
960
|
+
// 已在清单同理;0.6.0/0.7.0 已有两次写 op 漏列前科)。
|
|
961
|
+
'mcpm-tools-refresh',
|
|
886
962
|
// mcpm-reveal returns UNMASKED secrets; even though it is a read, it is
|
|
887
963
|
// token-gated like a write — on a LAN-exposed port the token must be the
|
|
888
964
|
// last line of defense for plaintext credentials too, not just writes.
|
|
@@ -983,7 +1059,8 @@ export default {
|
|
|
983
1059
|
* - 应用**别的份** = 换掉这个场景的绑定(用户裁定 2026-09-17:未锁定时切换要可用,
|
|
984
1060
|
* 并且同步写进场景档案)。绑定改完再走一次场景同步,于是「场景页显示什么 / 实际注入
|
|
985
1061
|
* 什么 / 谁是引用方」三处立刻重新对齐(此前的做法是直接拒绝,用户得先退出场景)。
|
|
986
|
-
*
|
|
1062
|
+
* 锁定由 `guardLockedOps` 挡住(agentsmd-apply 在冻结清单里);模型工具
|
|
1063
|
+
* `prompt_manager_apply` 不经过 handlers,execute 里自带 `lockedSceneGuard`(F-001)。
|
|
987
1064
|
*/
|
|
988
1065
|
async function applyPresetGuarded(id) {
|
|
989
1066
|
const driver = await scenePromptSync.driver();
|
|
@@ -996,6 +1073,7 @@ export default {
|
|
|
996
1073
|
ok: false,
|
|
997
1074
|
code: 'error.agentsMd.sceneRebindFailed',
|
|
998
1075
|
error: `无法把场景「${driver.label}」的提示词绑定改成「${target}」:${(rebound && rebound.error) || '未知原因'}`,
|
|
1076
|
+
params: { scene: driver.label, target, reason: String((rebound && rebound.error) || '未知原因') },
|
|
999
1077
|
};
|
|
1000
1078
|
}
|
|
1001
1079
|
}
|
|
@@ -1213,7 +1291,6 @@ export default {
|
|
|
1213
1291
|
catch (e) { /* pruning is best-effort */ }
|
|
1214
1292
|
}
|
|
1215
1293
|
async function writePatch(abs, content) {
|
|
1216
|
-
const t = await fs.resolve(abs);
|
|
1217
1294
|
const policy = await sandboxPolicy.resolve({ mode: 'danger-full-access' });
|
|
1218
1295
|
try {
|
|
1219
1296
|
const previous = await readPatch(abs);
|
|
@@ -1221,7 +1298,20 @@ export default {
|
|
|
1221
1298
|
await backupPatchFile(abs, previous);
|
|
1222
1299
|
}
|
|
1223
1300
|
catch (e) { /* a failed backup must never block the write */ }
|
|
1224
|
-
|
|
1301
|
+
// 同目录临时文件 + rename 原子替换:进程中断/磁盘满最坏留下一个 .tmp 兄弟,
|
|
1302
|
+
// 不会截断补丁本体。官方 app-boot 写这两个补丁文件用的是同一套模式
|
|
1303
|
+
// (`filename + '.tmp'` → rename + 瞬时错误重试,lib/index.js `_writeFile`),
|
|
1304
|
+
// 所以 HMR 对 rename 的容忍不需要额外验证。内容写仍走宿主 fs 服务
|
|
1305
|
+
// (沙箱策略与审批语义不变),rename 只是元数据动作。
|
|
1306
|
+
const temp = abs + '.' + randomUUID().slice(0, 8) + '.dsh-tmp';
|
|
1307
|
+
try {
|
|
1308
|
+
await fs.writeText(await fs.resolve(temp), content, undefined, undefined, policy);
|
|
1309
|
+
await renameWithRetry(temp, abs);
|
|
1310
|
+
}
|
|
1311
|
+
catch (e) {
|
|
1312
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
1313
|
+
throw e;
|
|
1314
|
+
}
|
|
1225
1315
|
}
|
|
1226
1316
|
// ---------- duplicate loader-id guard ----------
|
|
1227
1317
|
// Two rows with the same loader id make the plugin composition fail to
|
|
@@ -1401,7 +1491,6 @@ export default {
|
|
|
1401
1491
|
managedIds.add(m[1].trim());
|
|
1402
1492
|
}
|
|
1403
1493
|
const rows = [];
|
|
1404
|
-
const overrides = [];
|
|
1405
1494
|
const blocks = [];
|
|
1406
1495
|
let current = null;
|
|
1407
1496
|
for (const line of lines) {
|
|
@@ -1440,16 +1529,18 @@ export default {
|
|
|
1440
1529
|
}
|
|
1441
1530
|
}
|
|
1442
1531
|
else {
|
|
1532
|
+
// 覆盖块按**文件顺序**生效,不是"收集后统一回填":官方
|
|
1533
|
+
// `@deepseek-ai/dsh-app-boot` 的 applyEntryPatches 只给此刻已入索引的 id 打补丁
|
|
1534
|
+
// (insert 是插入时立即入索引),命中不到就 warn + skip。所以写在 insert 之前的
|
|
1535
|
+
// 覆盖块在宿主侧是 no-op —— 这里同样丢弃,界面才和生效值一致。
|
|
1443
1536
|
const entry = parseEntry(block.text.split('\n'));
|
|
1444
|
-
if (entry && entry.name === '@deepseek-ai/dsh-mcp-client')
|
|
1445
|
-
|
|
1537
|
+
if (entry && entry.name === '@deepseek-ai/dsh-mcp-client' && entry.disabled !== undefined) {
|
|
1538
|
+
const row = rows.find((r) => r.id === entry.id);
|
|
1539
|
+
if (row)
|
|
1540
|
+
row.disabled = entry.disabled;
|
|
1541
|
+
}
|
|
1446
1542
|
}
|
|
1447
1543
|
}
|
|
1448
|
-
for (const o of overrides) {
|
|
1449
|
-
const row = rows.find((r) => r.id === o.id);
|
|
1450
|
-
if (row && o.disabled !== undefined)
|
|
1451
|
-
row.disabled = o.disabled;
|
|
1452
|
-
}
|
|
1453
1544
|
return { rows };
|
|
1454
1545
|
}
|
|
1455
1546
|
// ---------- line-based block editing ----------
|
|
@@ -2109,7 +2200,9 @@ export default {
|
|
|
2109
2200
|
toolsList.push({
|
|
2110
2201
|
name: known.name,
|
|
2111
2202
|
description: known.description || '(服务器未运行;这个名字来自上次运行记录)',
|
|
2112
|
-
|
|
2203
|
+
// 与上方 live 行同一口径:整台停用(`*`)时 stale 行同样报停用,
|
|
2204
|
+
// 否则同页「live 行停用、stale 行启用」自相矛盾。
|
|
2205
|
+
enabled: wildcardHere ? false : !disabledHere.has(known.name),
|
|
2113
2206
|
parameters: [],
|
|
2114
2207
|
stale: true,
|
|
2115
2208
|
});
|
|
@@ -2204,6 +2297,24 @@ export default {
|
|
|
2204
2297
|
}
|
|
2205
2298
|
const toolCounts = {};
|
|
2206
2299
|
const enabledToolCounts = {};
|
|
2300
|
+
// 只来自**真实 schema** 的两个数,以及只来自**「已知工具」缓存**的一个数。
|
|
2301
|
+
//
|
|
2302
|
+
// 为什么必须与上面那两个分开(2026-09-17 用户实测发现):`toolCounts` 是三处并集
|
|
2303
|
+
// (live ∪ 停用表 ∪ 缓存),把"这台 server 曾经有什么工具"与"它现在有什么工具"混成了
|
|
2304
|
+
// 一个数。界面状态胶囊与注入段此前读的就是它,于是**一台已经连不上的 server 会被报成
|
|
2305
|
+
// 「已运行」并注入上下文**,模型照着去调 `mcp__X__*`,而那个工具根本没注册 —— 白烧一轮。
|
|
2306
|
+
//
|
|
2307
|
+
// 分开之后三个数各有明确含义:
|
|
2308
|
+
// liveToolCounts 当前真实注册的工具数(0 = 一个都没注册)
|
|
2309
|
+
// liveEnabledToolCounts 上面这个数里未被停用表扣减的(真正能调到的)
|
|
2310
|
+
// knownToolCounts 缓存里的工具数 —— 它是**曾经真的连上过**的证据(缓存只在真实
|
|
2311
|
+
// schema 里见到工具时才写入),0 = 从未连上过
|
|
2312
|
+
//
|
|
2313
|
+
// 前两个给状态胶囊与注入段用;`toolCounts` / `enabledToolCounts` 语义不变,继续给场景
|
|
2314
|
+
// 档案挑工具、详情页清单用(那里"上次见过哪些工具"正是需要的,且已标 `stale`)。
|
|
2315
|
+
const liveToolCounts = {};
|
|
2316
|
+
const liveEnabledToolCounts = {};
|
|
2317
|
+
const knownToolCounts = {};
|
|
2207
2318
|
try {
|
|
2208
2319
|
const schemas = await tools.schemas();
|
|
2209
2320
|
const knownCache = await readKnownMcpTools();
|
|
@@ -2254,6 +2365,17 @@ export default {
|
|
|
2254
2365
|
// 否则收窄后模型看到的数字比它能调的工具多,页面与详情页也会互相矛盾。
|
|
2255
2366
|
enabledToolCounts[server] = countEnabledTools(set, disabledMap[server] || []);
|
|
2256
2367
|
}
|
|
2368
|
+
// 真实来源的两个数:只数 `liveNames`(schema 里真正注册了的),再按停用表扣减。
|
|
2369
|
+
for (const [server, list] of Object.entries(liveNames)) {
|
|
2370
|
+
const set = new Set(list);
|
|
2371
|
+
liveToolCounts[server] = set.size;
|
|
2372
|
+
liveEnabledToolCounts[server] = countEnabledTools(set, disabledMap[server] || []);
|
|
2373
|
+
}
|
|
2374
|
+
// 「曾经连上过」的证据:缓存里这台 server 见过几个工具。注意读的是**回写之前**的
|
|
2375
|
+
// `knownCache` —— 回写会把这次 live 见到的并进去,而这次 live 见到的本来就已经算在
|
|
2376
|
+
// liveToolCounts 里了,不需要它来充当"曾经"。
|
|
2377
|
+
for (const [server, list] of Object.entries(knownCache))
|
|
2378
|
+
knownToolCounts[server] = list.length;
|
|
2257
2379
|
// 回写「已知工具」:live 见到的名字与描述并入缓存(有变化才写盘,读路径上的 best-effort)。
|
|
2258
2380
|
// 名字集合变化,或某个已知工具这次拿到了描述而缓存里没有 → 都算变化。
|
|
2259
2381
|
let cacheChanged = false;
|
|
@@ -2309,7 +2431,21 @@ export default {
|
|
|
2309
2431
|
row.toolCount = toolCounts[row.serverName] || 0;
|
|
2310
2432
|
if (row.enabledToolCount === undefined)
|
|
2311
2433
|
row.enabledToolCount = enabledToolCounts[row.serverName] || 0;
|
|
2312
|
-
|
|
2434
|
+
// 这三个**总是**覆盖:它们描述的是"此刻的真实状态",而缓存合并出来的那两个数
|
|
2435
|
+
// 描述的是"这台 server 有过哪些工具"。两者混淆过一次,代价是注入段谎报可用。
|
|
2436
|
+
row.liveToolCount = liveToolCounts[row.serverName] || 0;
|
|
2437
|
+
row.liveEnabledToolCount = liveEnabledToolCounts[row.serverName] || 0;
|
|
2438
|
+
row.knownToolCount = knownToolCounts[row.serverName] || 0;
|
|
2439
|
+
}
|
|
2440
|
+
// 列表顺序(用户要求 2026-09-17):**全局在前、应用级在后**,各级按服务器名排,
|
|
2441
|
+
// 当前在跑的 loader 行垫底。放在服务端而不是页面里:同一个顺序也是场景档案勾选器
|
|
2442
|
+
// (`scene-inventory`)与模型侧 `mcp_manager_list` 的顺序 —— 三处说同一件事。
|
|
2443
|
+
// 名字比较与技能页同一套(localeCompare),大小写混排时不会把大写全顶到前面。
|
|
2444
|
+
const levelRank = { global: 0, project: 1, loader: 2 };
|
|
2445
|
+
const serverNameOf = (row) => String(row.serverName || row.id || '');
|
|
2446
|
+
rows.sort((a, b) => (levelRank[a.level] ?? 3) - (levelRank[b.level] ?? 3) ||
|
|
2447
|
+
serverNameOf(a).localeCompare(serverNameOf(b)) ||
|
|
2448
|
+
String(a.id).localeCompare(String(b.id)));
|
|
2313
2449
|
// A loader id that appears twice (same id in both patch files, or twice in
|
|
2314
2450
|
// one) makes the composition fail to boot. Surface it instead of hiding it.
|
|
2315
2451
|
const idCounts = new Map();
|
|
@@ -2424,6 +2560,17 @@ export default {
|
|
|
2424
2560
|
}
|
|
2425
2561
|
const oldAbs = cur.level === 'global' ? p.globalPatch : p.projectPatch;
|
|
2426
2562
|
const newAbs = level === 'global' ? p.globalPatch : p.projectPatch;
|
|
2563
|
+
// 编辑表单不建模 config 里的全部键:手写补丁可能带 toolCallTimeoutMs 这类字段,
|
|
2564
|
+
// 重建前从旧条目原样带回,否则一次「编辑」就把它静默删掉。只透传 buildInsertBlock
|
|
2565
|
+
// 已有发射路径的键 —— 其余未知键的保真需要值保持式序列化(裸布尔/数字经
|
|
2566
|
+
// unquote/yq 往返会漂成字符串),不在此处理,宁可如实丢弃也不写错类型。
|
|
2567
|
+
try {
|
|
2568
|
+
const curRaw = parseRows(await readPatch(oldAbs)).rows.find((r) => r.id === id);
|
|
2569
|
+
if (curRaw && curRaw.config && curRaw.config.toolCallTimeoutMs != null) {
|
|
2570
|
+
row.toolCallTimeoutMs = curRaw.config.toolCallTimeoutMs;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
catch { /* 旧文件读不了时按建模字段重建,与原行为一致 */ }
|
|
2427
2574
|
const block = buildInsertBlock(row);
|
|
2428
2575
|
return withWriteLock(async () => {
|
|
2429
2576
|
// Per-tool disable state is keyed by the serverName namespace: migrate
|
|
@@ -2439,6 +2586,20 @@ export default {
|
|
|
2439
2586
|
catch (e) { /* non-fatal */ }
|
|
2440
2587
|
disabledToolsCache = { at: Date.now(), value: toolMap };
|
|
2441
2588
|
}
|
|
2589
|
+
// F-025:运行时快照的 mcp 停用表同样按 serverName 键 —— 改名不跟着改,
|
|
2590
|
+
// 退出场景整体回写时旧键成死键、新名工具的停用状态丢失(回到全启用)。
|
|
2591
|
+
try {
|
|
2592
|
+
const slice = await rulesService.readArchiveSlice();
|
|
2593
|
+
const mode = slice && slice.mode;
|
|
2594
|
+
const mcpMap = mode && mode.snapshot && mode.snapshot.mcp;
|
|
2595
|
+
if (mcpMap && Object.prototype.hasOwnProperty.call(mcpMap, cur.serverName)) {
|
|
2596
|
+
const next = {};
|
|
2597
|
+
for (const [k, v] of Object.entries(mcpMap))
|
|
2598
|
+
next[k === cur.serverName ? serverName : k] = v;
|
|
2599
|
+
await rulesService.patchIndex({ mode: { ...mode, snapshot: { ...mode.snapshot, mcp: next } } });
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
catch { /* 快照同步失败不阻断改名本身;残留与修复前一致 */ }
|
|
2442
2603
|
}
|
|
2443
2604
|
if (oldAbs !== newAbs) {
|
|
2444
2605
|
// Level migration: remove from the old file, insert into the new one.
|
|
@@ -2621,6 +2782,16 @@ export default {
|
|
|
2621
2782
|
// waits deliberately run OUTSIDE the write lock — holding the global
|
|
2622
2783
|
// write lock for up to ~11s stalled every other write op.
|
|
2623
2784
|
await withWriteLock(async () => {
|
|
2785
|
+
// 并发护栏:阶段 1 的强制写自身必然把 effective 状态置为停用,所以走到这里时
|
|
2786
|
+
// 若读到「已启用」,只可能是等待窗口内别的写操作重新启用了它 —— 以现状为准并
|
|
2787
|
+
// 警告,绝不按重启前快照把用户的显式选择改回去。其余情形按 wasDisabled 原样恢复。
|
|
2788
|
+
// (已知残留:窗口内被「停用」与阶段 1 的写不可区分,仍按快照恢复 —— 重启语义
|
|
2789
|
+
// 本就是「状态与重启前一致」,方向性无害。)
|
|
2790
|
+
const currentDisabled = await isRowDisabled(id, level);
|
|
2791
|
+
if (!currentDisabled && wasDisabled) {
|
|
2792
|
+
warnings.push('重启等待窗口内该服务被重新启用:已保留启用状态,未按重启前状态恢复');
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2624
2795
|
let c = await readPatch(abs);
|
|
2625
2796
|
c = removeMarked(c, id, 'disable');
|
|
2626
2797
|
if (wasDisabled)
|
|
@@ -2764,9 +2935,13 @@ export default {
|
|
|
2764
2935
|
return { skipped: true, reason: 'serverName 已存在' };
|
|
2765
2936
|
if (idTaken && !overwrite)
|
|
2766
2937
|
return { skipped: true, reason: 'id 已存在' };
|
|
2938
|
+
// Overwrite: purge every trace of the id from BOTH patch files first,
|
|
2939
|
+
// then insert the imported row at its own level. The purge is not
|
|
2940
|
+
// atomic with the insert, so keep the pre-purge content of both files
|
|
2941
|
+
// and restore it when the insert fails — same trade-off as mcpmEdit's
|
|
2942
|
+
// level migration: losing the old entries is worse than a transient dup.
|
|
2943
|
+
const origs = [];
|
|
2767
2944
|
if (idTaken && overwrite) {
|
|
2768
|
-
// Purge every trace of the id from BOTH patch files first, then
|
|
2769
|
-
// insert the imported row at its own level.
|
|
2770
2945
|
for (const lvl of ['project', 'global']) {
|
|
2771
2946
|
const lAbs = lvl === 'global' ? p.globalPatch : p.projectPatch;
|
|
2772
2947
|
let lContent = '';
|
|
@@ -2776,24 +2951,50 @@ export default {
|
|
|
2776
2951
|
catch (e) {
|
|
2777
2952
|
continue;
|
|
2778
2953
|
}
|
|
2779
|
-
|
|
2954
|
+
origs.push({ abs: lAbs, content: lContent });
|
|
2780
2955
|
try {
|
|
2781
|
-
await writePatch(lAbs, lContent);
|
|
2956
|
+
await writePatch(lAbs, removeEntryAll(lContent, row.id));
|
|
2782
2957
|
}
|
|
2783
2958
|
catch (e) {
|
|
2784
|
-
|
|
2959
|
+
// 前面已清掉的文件也要恢复:purge 中途失败同样不能留下「旧条目没了」的状态。
|
|
2960
|
+
for (const o of origs) {
|
|
2961
|
+
try {
|
|
2962
|
+
await writePatch(o.abs, o.content);
|
|
2963
|
+
}
|
|
2964
|
+
catch { /* best effort */ }
|
|
2965
|
+
}
|
|
2966
|
+
return { skipped: true, reason: '覆盖旧条目失败(已回滚): ' + message(e) };
|
|
2785
2967
|
}
|
|
2786
2968
|
}
|
|
2787
2969
|
}
|
|
2788
2970
|
const abs = row.level === 'global' ? p.globalPatch : p.projectPatch;
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2971
|
+
try {
|
|
2972
|
+
let c = await readPatch(abs);
|
|
2973
|
+
const before = c;
|
|
2974
|
+
c = appendBlock(c, buildInsertBlock(row));
|
|
2975
|
+
if (row.disabled)
|
|
2976
|
+
c = appendBlock(c, buildDisableBlock(row.id, true));
|
|
2977
|
+
const guard = duplicateGuard(before, c);
|
|
2978
|
+
if (guard) {
|
|
2979
|
+
for (const o of origs) {
|
|
2980
|
+
try {
|
|
2981
|
+
await writePatch(o.abs, o.content);
|
|
2982
|
+
}
|
|
2983
|
+
catch { /* best effort */ }
|
|
2984
|
+
}
|
|
2985
|
+
return { skipped: true, reason: guard.error };
|
|
2986
|
+
}
|
|
2987
|
+
await writePatch(abs, c);
|
|
2988
|
+
}
|
|
2989
|
+
catch (e) {
|
|
2990
|
+
for (const o of origs) {
|
|
2991
|
+
try {
|
|
2992
|
+
await writePatch(o.abs, o.content);
|
|
2993
|
+
}
|
|
2994
|
+
catch { /* best effort */ }
|
|
2995
|
+
}
|
|
2996
|
+
return { skipped: true, reason: '写入失败(已回滚): ' + message(e) };
|
|
2997
|
+
}
|
|
2797
2998
|
return { added: true, wasOverwrite: idTaken && overwrite };
|
|
2798
2999
|
});
|
|
2799
3000
|
if (res.added) {
|
|
@@ -3425,10 +3626,12 @@ export default {
|
|
|
3425
3626
|
ctx.logger?.warn?.('agents-md: reference probe failed; remove allowed');
|
|
3426
3627
|
const why = refs ? refs.get(id) || [] : [];
|
|
3427
3628
|
if (why.length) {
|
|
3629
|
+
const refsText = why.map(promptRefReason).join(';');
|
|
3428
3630
|
return {
|
|
3429
3631
|
ok: false,
|
|
3430
3632
|
code: 'error.agentsMd.referenced',
|
|
3431
|
-
error: `「${id}」仍被引用,不能删除:${
|
|
3633
|
+
error: `「${id}」仍被引用,不能删除:${refsText}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。`,
|
|
3634
|
+
params: { id, refs: refsText },
|
|
3432
3635
|
};
|
|
3433
3636
|
}
|
|
3434
3637
|
return agentsMdService.remove(id);
|
|
@@ -4483,9 +4686,50 @@ export default {
|
|
|
4483
4686
|
}
|
|
4484
4687
|
// ---------- agent-facing tools (standard ctx.tools.register + defineTool) ----------
|
|
4485
4688
|
const text = (value) => [{ type: 'text', text: value }];
|
|
4689
|
+
/**
|
|
4690
|
+
* 采纳遥测:把每个工具包一层,模型一调就记一笔"哪个域被伸手了"(见 context-inject 的
|
|
4691
|
+
* `noteToolUse`)。计数在宿主工具对象之外,所以**参数校验失败也算**——我们要测的是
|
|
4692
|
+
* "模型知不知道有这个域、会不会去用",不是"调用写得对不对"。
|
|
4693
|
+
*
|
|
4694
|
+
* 只统计本插件自己的工具,不走 `tools/result` 这类宿主事件:事件的作用域语义一旦变化,
|
|
4695
|
+
* 遥测会静默变成 0,而 0 恰好会被读成"模型从来不用"——一个假结论比没有结论更糟。
|
|
4696
|
+
* 包自己的工具是确定性的:工具在,观测就在。
|
|
4697
|
+
*/
|
|
4698
|
+
const trackAdoption = (def) => {
|
|
4699
|
+
const name = String(def.name ?? '');
|
|
4700
|
+
const run = def.execute;
|
|
4701
|
+
if (name === '' || typeof run !== 'function')
|
|
4702
|
+
return def;
|
|
4703
|
+
return {
|
|
4704
|
+
...def,
|
|
4705
|
+
async execute(args, exec) {
|
|
4706
|
+
try {
|
|
4707
|
+
if (contextInjectorNote !== null)
|
|
4708
|
+
contextInjectorNote(name, exec && exec.agent);
|
|
4709
|
+
}
|
|
4710
|
+
catch { /* 遥测绝不能影响工具 */ }
|
|
4711
|
+
return run(args, exec);
|
|
4712
|
+
},
|
|
4713
|
+
};
|
|
4714
|
+
};
|
|
4715
|
+
/**
|
|
4716
|
+
* 与宿主 `defineTool` **同签名的**本地包装。
|
|
4717
|
+
*
|
|
4718
|
+
* 为什么用同名遮蔽而不是逐个改 14 个注册点:采纳遥测必须覆盖**每一个**域工具,
|
|
4719
|
+
* 漏一个就得到"这个域从来没用过"的假结论。遮蔽 import 让"新加工具自动被统计"成为
|
|
4720
|
+
* 默认,而不是靠后来者记得手动加。
|
|
4721
|
+
*
|
|
4722
|
+
* 类型上刻意**不重写签名**,而是把箭头函数断言成 `typeof hostDefineTool`:宿主签名带
|
|
4723
|
+
* `const S/O` 类型参数,照着写一遍会让 TS 在每个调用点上多展开一层 `InferObject<S, ?>`,
|
|
4724
|
+
* 直接撞 "Excessive stack depth comparing types"(实测)。断言成宿主自己的类型之后,
|
|
4725
|
+
* 14 个调用点的参数表推断与改造前**完全一致** —— 宿主改签名时该报错的地方照旧报错;
|
|
4726
|
+
* 包装体本身只是转发(参数收 `unknown`、原样交给宿主),不参与推断,也就没有可失效的类型。
|
|
4727
|
+
*/
|
|
4728
|
+
const hostDefineToolAny = hostDefineTool;
|
|
4729
|
+
const defineTool = ((options) => trackAdoption(hostDefineToolAny(options)));
|
|
4486
4730
|
tools.register(defineTool({
|
|
4487
4731
|
name: 'mcp_manager_list',
|
|
4488
|
-
description: 'List configured MCP servers (level, enabled state, live loader status, tool count excluding switched-off tools, note). Read a server\'s note before choosing it. Defaults to enabled servers; pass all=true for every configured server.',
|
|
4732
|
+
description: 'List configured MCP servers (level, enabled state, live loader status, tool count excluding switched-off tools, note). Read a server\'s note before choosing it. The same list is injected into your context each turn (the「本机 MCP 服务器的当前状态」system-reminder); this tool is the raw view — all=true includes disabled servers. Defaults to enabled servers; pass all=true for every configured server.',
|
|
4489
4733
|
parameters: {
|
|
4490
4734
|
all: { type: 'boolean', description: 'Include disabled servers (default false).' },
|
|
4491
4735
|
},
|
|
@@ -4577,6 +4821,9 @@ export default {
|
|
|
4577
4821
|
},
|
|
4578
4822
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
4579
4823
|
async execute(args) {
|
|
4824
|
+
const blocked = await lockedSceneGuard();
|
|
4825
|
+
if (blocked)
|
|
4826
|
+
throw new Error(blocked);
|
|
4580
4827
|
const r = await mcpmAdd(args);
|
|
4581
4828
|
if (!r.ok)
|
|
4582
4829
|
throw new Error(r.error);
|
|
@@ -4587,7 +4834,7 @@ export default {
|
|
|
4587
4834
|
// tools/pre-execute hook below (the model must ask before writing files).
|
|
4588
4835
|
tools.register(defineTool({
|
|
4589
4836
|
name: 'skill_manager_list',
|
|
4590
|
-
description: 'List DSH skills with enabled state, effective/shadowed status and source file path. Defaults to enabled skills only; pass all=true for every entry. A copy marked "shadowed by <root>" stays inactive even if enabled.',
|
|
4837
|
+
description: 'List DSH skills with enabled state, effective/shadowed status and source file path. The injected「本机技能目录」system-reminder carries callable skills and summaries only; use this tool to get a source file path (read the file for the full body) and to see entries that are off. Defaults to enabled skills only; pass all=true for every entry. A copy marked "shadowed by <root>" stays inactive even if enabled.',
|
|
4591
4838
|
parameters: {
|
|
4592
4839
|
all: { type: 'boolean', description: 'Include disabled and shadowed entries (default false).' },
|
|
4593
4840
|
},
|
|
@@ -4706,6 +4953,9 @@ export default {
|
|
|
4706
4953
|
},
|
|
4707
4954
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
4708
4955
|
async execute(args) {
|
|
4956
|
+
const blocked = await lockedSceneGuard();
|
|
4957
|
+
if (blocked)
|
|
4958
|
+
throw new Error(blocked);
|
|
4709
4959
|
const r = await skillsService.ops['skill-create']({ name: args.name, description: args.description, body: args.body });
|
|
4710
4960
|
if (!r || r.ok === false)
|
|
4711
4961
|
throw new Error((r && r.error) || 'skill create failed');
|
|
@@ -4716,7 +4966,7 @@ export default {
|
|
|
4716
4966
|
// AGENTS.md 预设库:模型可查/切,不能造/删(避免模型乱删用户预设)。
|
|
4717
4967
|
tools.register(defineTool({
|
|
4718
4968
|
name: 'prompt_manager_list',
|
|
4719
|
-
description: 'List AGENTS.md presets (id, active state, file path). Read that file to see a preset body. Defaults to the preset currently in effect; pass all=true for the whole library.',
|
|
4969
|
+
description: 'List AGENTS.md presets (id, active state, file path). Read that file to see a preset body. The preset in effect right now is injected into your context each turn (the「本机提示词」system-reminder, whose「来源:」line names the file). Defaults to the preset currently in effect; pass all=true for the whole library.',
|
|
4720
4970
|
parameters: {
|
|
4721
4971
|
all: { type: 'boolean', description: 'Include inactive presets (default false).' },
|
|
4722
4972
|
},
|
|
@@ -4761,6 +5011,9 @@ export default {
|
|
|
4761
5011
|
},
|
|
4762
5012
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
4763
5013
|
async execute(args) {
|
|
5014
|
+
const blocked = await lockedSceneGuard();
|
|
5015
|
+
if (blocked)
|
|
5016
|
+
throw new Error(blocked);
|
|
4764
5017
|
const r = await applyPresetGuarded(args.id);
|
|
4765
5018
|
if (!r.ok)
|
|
4766
5019
|
throw new Error(r.error);
|
|
@@ -4773,7 +5026,7 @@ export default {
|
|
|
4773
5026
|
// 路径锚点:$DSH_HOME/tool-management/memories/<场景>/…(场景 `global` = 界面「全局」)。
|
|
4774
5027
|
tools.register(defineTool({
|
|
4775
5028
|
name: 'memory_manager_list',
|
|
4776
|
-
description: 'List memories under ~/.dsh/tool-management/memories (id, scene, enabled, description). Defaults to the memories that will actually be injected; pass all=true for every entry.',
|
|
5029
|
+
description: 'List memories under ~/.dsh/tool-management/memories (id, scene, enabled, description). The ones actually injected are carried in your context each turn (the「本机当前的场景和记忆」system-reminder); use this tool to find ids/paths or to see entries that are off. Defaults to the memories that will actually be injected; pass all=true for every entry.',
|
|
4777
5030
|
parameters: {
|
|
4778
5031
|
group: { type: 'string', description: 'Optional scene filter.' },
|
|
4779
5032
|
all: { type: 'boolean', description: 'Include memories that are off, in an inactive scene, or shadowed (default false).' },
|
|
@@ -4839,6 +5092,9 @@ export default {
|
|
|
4839
5092
|
},
|
|
4840
5093
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
4841
5094
|
async execute(args) {
|
|
5095
|
+
const blocked = await lockedSceneGuard();
|
|
5096
|
+
if (blocked)
|
|
5097
|
+
throw new Error(blocked);
|
|
4842
5098
|
const r = await rulesService.ops['rules-create'](args);
|
|
4843
5099
|
if (!r || r.ok === false)
|
|
4844
5100
|
throw new Error((r && r.error) || '创建规则失败');
|
|
@@ -4919,11 +5175,13 @@ export default {
|
|
|
4919
5175
|
.catch(() => ({ kind: 'ask', reason: 'Write a memory under ~/.dsh/tool-management/memories' }));
|
|
4920
5176
|
}
|
|
4921
5177
|
// subagent_manager_run:子代理运行花真 token:默认确认(requireConfirmForModelSubagentRun !== false),可关。
|
|
5178
|
+
// `inherit: true` 时子代理会读到本次会话已完成的对话 —— 卡里如实说明(用户批准的是
|
|
5179
|
+
// "把这段对话交给它",不只是"跑个子代理")。文案只在与 fork 有关的部分分叉。
|
|
5180
|
+
const inherits = !!(exec && exec.arguments && exec.arguments.inherit);
|
|
5181
|
+
const ask = { kind: 'ask', reason: inherits ? 'Run a subagent on this conversation (it inherits the conversation; consumes tokens)' : 'Run a subagent (consumes tokens)' };
|
|
4922
5182
|
return readPluginSettings()
|
|
4923
|
-
.then((s) => (s.requireConfirmForModelSubagentRun !== false
|
|
4924
|
-
|
|
4925
|
-
: next()))
|
|
4926
|
-
.catch(() => ({ kind: 'ask', reason: 'Run a subagent (consumes tokens)' }));
|
|
5183
|
+
.then((s) => (s.requireConfirmForModelSubagentRun !== false ? ask : next()))
|
|
5184
|
+
.catch(() => ask);
|
|
4927
5185
|
});
|
|
4928
5186
|
}
|
|
4929
5187
|
// ---------- per-tool MCP switches: execution guard + visibility ----------
|
|
@@ -5040,7 +5298,7 @@ export default {
|
|
|
5040
5298
|
res.end(JSON.stringify({ ok: false, error: 'missing plugin gate header' }));
|
|
5041
5299
|
return;
|
|
5042
5300
|
}
|
|
5043
|
-
const tokenAccepted = TOKEN !== '' && hdr('x-dsh-token')
|
|
5301
|
+
const tokenAccepted = TOKEN !== '' && tokenMatches(hdr('x-dsh-token'));
|
|
5044
5302
|
if (!tokenAccepted) {
|
|
5045
5303
|
const fence = fenceRejection(req, connection);
|
|
5046
5304
|
if (fence) {
|
|
@@ -5074,7 +5332,7 @@ export default {
|
|
|
5074
5332
|
return;
|
|
5075
5333
|
}
|
|
5076
5334
|
}
|
|
5077
|
-
if (TOKEN && WRITE_OPS.has(op) && hdr('x-dsh-token')
|
|
5335
|
+
if (TOKEN && WRITE_OPS.has(op) && !tokenMatches(hdr('x-dsh-token'))) {
|
|
5078
5336
|
res.end(JSON.stringify({ ok: false, error: '缺少或错误的访问令牌(x-dsh-token)' }));
|
|
5079
5337
|
return;
|
|
5080
5338
|
}
|