dsh-plugin-tool-management 0.14.0 → 0.15.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 +560 -519
- 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 +391 -365
- package/lib/audit-log.js +157 -0
- package/lib/client.js +3800 -2504
- package/lib/compat/patch-dialect.js +3 -1
- package/lib/compat/preset-reach.js +37 -6
- package/lib/compat/probe.js +149 -7
- package/lib/context-inject.js +5 -3
- package/lib/index.js +236 -30
- package/lib/mcp/loader-token.js +53 -28
- package/lib/mcp/manager.js +146 -7
- package/lib/memories/archive-engine.js +70 -2
- package/lib/memories/archive.js +8 -1
- package/lib/memories/index-io.js +6 -0
- package/lib/op-registry.js +18 -4
- package/lib/ops/compat.js +109 -35
- package/lib/ops/scene-records.js +34 -0
- package/lib/ops/scene-sync.js +12 -1
- package/lib/ops/sessions.js +23 -8
- package/lib/ops/snapshot.js +596 -0
- package/lib/scenes/candidates.js +10 -5
- package/lib/sessions/bridge.js +5 -0
- package/lib/sessions/workspace.js +17 -5
- package/lib/skills/core.js +12 -3
- package/lib/tools/deps.js +1 -1
- package/lib/tools/table.js +5 -1
- package/package.json +112 -108
package/lib/mcp/manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// MCP 管理域 —— 侧车读写(备注 / 设置 / 停用表)+ 补丁文件行编辑 +
|
|
1
|
+
// MCP 管理域 —— 侧车读写(备注 / 设置 / 停用表)+ 补丁文件行编辑 + 17 个 op 的实现。
|
|
2
2
|
//
|
|
3
3
|
// 2026-09-19 从 index.ts 的 apply 闭包原样抽出(块 A 1795-1913 + 块 B 1920-3100)。
|
|
4
4
|
// 为什么单独一个文件:这 1300 行只做一件事 —— 把「MCP 配置」翻译成补丁文件的改动与
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
// 其余 14 项(ensurePaths / withWriteLock / readPatch / pluginInventory / memoriesService 等)
|
|
19
19
|
// 由 deps 显式传入。
|
|
20
20
|
import { clearRuntimeNote, noteRuntime } from '../compat/runtime-notes.js';
|
|
21
|
+
// 配置体检查 PATH 用(见 mcpmInspect):execFile 不走 shell,参数是数组 —— 体检只查询
|
|
22
|
+
// 命令名,**绝不执行**配置里的命令本身。
|
|
23
|
+
import { execFile } from 'node:child_process';
|
|
21
24
|
import { MCP_CLIENT_MODULE } from '../host-names.js';
|
|
22
25
|
import { hubPath } from '../hub.js';
|
|
23
26
|
import { planOverrideCompaction } from './override-blocks.js';
|
|
@@ -524,6 +527,7 @@ export function createMcpManager(deps) {
|
|
|
524
527
|
kind: 'write',
|
|
525
528
|
fallback: 'inform-only',
|
|
526
529
|
detail: 'agent scope 上没有可用的 tools.restrict(官方接口变了或该 scope 未暴露 tools):停用的 MCP 工具与兼容页关掉的本插件工具仍会出现在模型可见的工具表里,执行侧拦截仍然生效。',
|
|
530
|
+
detailKey: 'mcp-tool-visibility.no-restrict',
|
|
527
531
|
});
|
|
528
532
|
return;
|
|
529
533
|
}
|
|
@@ -550,6 +554,8 @@ export function createMcpManager(deps) {
|
|
|
550
554
|
kind: 'write',
|
|
551
555
|
fallback: 'inform-only',
|
|
552
556
|
detail: '有工具没能从模型可见的工具表里摘掉(' + message(e) + '):执行侧拦截仍然生效,模型仍能看到该工具的名字。',
|
|
557
|
+
detailKey: 'mcp-tool-visibility.partial',
|
|
558
|
+
params: { reason: message(e) },
|
|
553
559
|
});
|
|
554
560
|
}
|
|
555
561
|
}
|
|
@@ -1543,6 +1549,132 @@ export function createMcpManager(deps) {
|
|
|
1543
1549
|
return { ok: true, removed: total, ids: removed, files };
|
|
1544
1550
|
});
|
|
1545
1551
|
}
|
|
1552
|
+
// ---------- 配置体检(0.15.0 B1)--------------------------------------------------
|
|
1553
|
+
// 只读、零副作用:不拉起子进程、不连网、一个字节都不写盘。唯一的对外调用是 `where` /
|
|
1554
|
+
// `which` 查询 PATH —— 它查的是「这个命令名能不能找到」,绝不执行配置里的命令本身。
|
|
1555
|
+
//
|
|
1556
|
+
// 为什么单独读补丁而不复用 `mcpmList()`:那个函数顺带回写「已知工具」缓存、还要问一次
|
|
1557
|
+
// schema 注册表。体检的承诺是「点了什么都不变」,所以这里只走 readPatch + parseRows。
|
|
1558
|
+
const INSPECT_PATH_TIMEOUT_MS = 3000;
|
|
1559
|
+
/** 命令是否在 PATH 上:`unknown`(查不了)与 `missing`(确认没有)必须分开 —— 前者不是问题。 */
|
|
1560
|
+
async function probeCommandOnPath(command) {
|
|
1561
|
+
if (!command)
|
|
1562
|
+
return 'unknown';
|
|
1563
|
+
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
1564
|
+
return await new Promise((resolve) => {
|
|
1565
|
+
execFile(finder, [command], { timeout: INSPECT_PATH_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {
|
|
1566
|
+
if (!error) {
|
|
1567
|
+
resolve(String(stdout || '').trim() ? 'ok' : 'missing');
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
// ENOENT = 连 where/which 自己都没跑到;killed = 超时。两种都是「无法确认」,
|
|
1571
|
+
// 剩下的(退出码非 0)才是「PATH 上没有这条命令」。
|
|
1572
|
+
const code = error.code;
|
|
1573
|
+
if (code === 'ENOENT' || error.killed === true)
|
|
1574
|
+
resolve('unknown');
|
|
1575
|
+
else
|
|
1576
|
+
resolve('missing');
|
|
1577
|
+
});
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
/** 一行配置 → 检查项。`level: 'warn'` 才点黄;`info` 是「查不了 / 不算问题」那类说明。 */
|
|
1581
|
+
async function inspectChecks(row, duplicateIds) {
|
|
1582
|
+
const checks = [];
|
|
1583
|
+
const declared = String(row.transport || '');
|
|
1584
|
+
const url = String(row.url || '');
|
|
1585
|
+
const command = String(row.command || '');
|
|
1586
|
+
// transport 缺失/不认识时按字段形状推:历史上手工改过补丁文件的行会绕过 normalize,
|
|
1587
|
+
// 直接判「没有 transport」会把一条其实能用的配置说成坏的。
|
|
1588
|
+
const transport = declared === 'stdio' || (!declared && command && !url) ? 'stdio'
|
|
1589
|
+
: declared === 'streamable-http' || (!declared && url) ? 'streamable-http'
|
|
1590
|
+
: declared;
|
|
1591
|
+
if (!declared && transport)
|
|
1592
|
+
checks.push({ id: 'transportMissing', level: 'info' });
|
|
1593
|
+
/** 值必须是字符串映射:非对象整片算坏,对象里逐个挑出非字符串的键。 */
|
|
1594
|
+
const badKv = (map) => {
|
|
1595
|
+
if (map == null)
|
|
1596
|
+
return [];
|
|
1597
|
+
if (typeof map !== 'object' || Array.isArray(map))
|
|
1598
|
+
return ['*'];
|
|
1599
|
+
return Object.keys(map).filter((k) => typeof map[k] !== 'string');
|
|
1600
|
+
};
|
|
1601
|
+
if (transport === 'stdio') {
|
|
1602
|
+
if (!command)
|
|
1603
|
+
checks.push({ id: 'noCommand', level: 'warn' });
|
|
1604
|
+
else {
|
|
1605
|
+
// 唯一一次外部调用。npx / uvx 这类 shim 找不到时界面给的下半句是「shim 属正常可忽略」,
|
|
1606
|
+
// 不让用户以为命令坏了。
|
|
1607
|
+
const found = await probeCommandOnPath(command);
|
|
1608
|
+
if (found === 'missing')
|
|
1609
|
+
checks.push({ id: 'cmdMissing', level: 'warn', params: { command } });
|
|
1610
|
+
else if (found === 'unknown')
|
|
1611
|
+
checks.push({ id: 'cmdUnknown', level: 'info', params: { command } });
|
|
1612
|
+
}
|
|
1613
|
+
if (row.args != null && !Array.isArray(row.args))
|
|
1614
|
+
checks.push({ id: 'argsNotArray', level: 'warn' });
|
|
1615
|
+
const badEnv = badKv(row.env);
|
|
1616
|
+
if (badEnv.length)
|
|
1617
|
+
checks.push({ id: 'kvNotString', level: 'warn', params: { where: 'env', keys: badEnv.join('、') } });
|
|
1618
|
+
}
|
|
1619
|
+
else if (transport === 'streamable-http') {
|
|
1620
|
+
if (!url)
|
|
1621
|
+
checks.push({ id: 'noUrl', level: 'warn' });
|
|
1622
|
+
else if (!/^https?:\/\//.test(url))
|
|
1623
|
+
checks.push({ id: 'badUrl', level: 'warn', params: { url } });
|
|
1624
|
+
const badHeaders = badKv(row.headers);
|
|
1625
|
+
if (badHeaders.length)
|
|
1626
|
+
checks.push({ id: 'kvNotString', level: 'warn', params: { where: 'headers', keys: badHeaders.join('、') } });
|
|
1627
|
+
}
|
|
1628
|
+
else if (!command && !url) {
|
|
1629
|
+
checks.push({ id: 'noEndpoint', level: 'warn' });
|
|
1630
|
+
}
|
|
1631
|
+
else {
|
|
1632
|
+
checks.push({ id: 'badTransport', level: 'warn', params: { transport } });
|
|
1633
|
+
}
|
|
1634
|
+
if (duplicateIds > 1)
|
|
1635
|
+
checks.push({ id: 'nameDup', level: 'warn', params: { name: String(row.serverName || ''), count: duplicateIds } });
|
|
1636
|
+
return checks;
|
|
1637
|
+
}
|
|
1638
|
+
async function mcpmInspect(args) {
|
|
1639
|
+
const p = await ensurePaths();
|
|
1640
|
+
const rows = [];
|
|
1641
|
+
const errors = [];
|
|
1642
|
+
for (const level of ['project', 'global']) {
|
|
1643
|
+
const abs = level === 'project' ? p.projectPatch : p.globalPatch;
|
|
1644
|
+
let content = '';
|
|
1645
|
+
try {
|
|
1646
|
+
content = await readPatch(abs);
|
|
1647
|
+
}
|
|
1648
|
+
catch (e) {
|
|
1649
|
+
errors.push(level + ': ' + message(e));
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const { rows: fileRows } = parseRows(content);
|
|
1653
|
+
for (const r of fileRows)
|
|
1654
|
+
rows.push(normalizeRow(r, level, abs));
|
|
1655
|
+
}
|
|
1656
|
+
// 重名判据:同一个 serverName 挂在**不同 id** 下。同 id 跨层是正常遮蔽(界面已有「重复 id」标签),
|
|
1657
|
+
// 而按名字注册的 `mcp__<serverName>__*` 会撞车 —— 那才是配置问题。
|
|
1658
|
+
const idsByName = {};
|
|
1659
|
+
for (const r of rows) {
|
|
1660
|
+
const n = String(r.serverName || '');
|
|
1661
|
+
if (!idsByName[n])
|
|
1662
|
+
idsByName[n] = new Set();
|
|
1663
|
+
idsByName[n].add(String(r.id));
|
|
1664
|
+
}
|
|
1665
|
+
const onlyId = args && String(args.id || '') ? String(args.id) : '';
|
|
1666
|
+
const onlyLevel = args && String(args.level || '') ? String(args.level) : '';
|
|
1667
|
+
const results = [];
|
|
1668
|
+
for (const r of rows) {
|
|
1669
|
+
if (onlyId && String(r.id) !== onlyId)
|
|
1670
|
+
continue;
|
|
1671
|
+
if (onlyLevel && String(r.level) !== onlyLevel)
|
|
1672
|
+
continue;
|
|
1673
|
+
const checks = await inspectChecks(r, (idsByName[String(r.serverName || '')] || new Set()).size);
|
|
1674
|
+
results.push({ id: r.id, serverName: r.serverName, level: r.level, transport: r.transport || null, checks });
|
|
1675
|
+
}
|
|
1676
|
+
return { ok: true, results, errors };
|
|
1677
|
+
}
|
|
1546
1678
|
async function mcpmExport() {
|
|
1547
1679
|
const p = await ensurePaths();
|
|
1548
1680
|
const list = await mcpmList();
|
|
@@ -1789,6 +1921,7 @@ export function createMcpManager(deps) {
|
|
|
1789
1921
|
'mcpm-compact': mcpmCompact,
|
|
1790
1922
|
'mcpm-export': mcpmExport,
|
|
1791
1923
|
'mcpm-import': mcpmImport,
|
|
1924
|
+
'mcpm-inspect': mcpmInspect,
|
|
1792
1925
|
},
|
|
1793
1926
|
readDisabledTools,
|
|
1794
1927
|
readKnownMcpTools,
|
|
@@ -1799,13 +1932,19 @@ export function createMcpManager(deps) {
|
|
|
1799
1932
|
mcpmRowsWithNotes,
|
|
1800
1933
|
readPluginSettings,
|
|
1801
1934
|
isToolDisabled: (name) => isToolDisabledIn(disabledToolsCache ? disabledToolsCache.value : {}, name),
|
|
1802
|
-
/**
|
|
1803
|
-
|
|
1935
|
+
/** 停用表的两个口径(读 TTL 缓存;功能总览用):`*` 通配的台数 + 单独停用的工具数。 */
|
|
1936
|
+
disabledToolSummary: () => {
|
|
1804
1937
|
const map = disabledToolsCache ? disabledToolsCache.value : {};
|
|
1805
|
-
let
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1938
|
+
let servers = 0;
|
|
1939
|
+
let tools = 0;
|
|
1940
|
+
for (const list of Object.values(map)) {
|
|
1941
|
+
if (!Array.isArray(list))
|
|
1942
|
+
continue;
|
|
1943
|
+
if (list.indexOf('*') >= 0)
|
|
1944
|
+
servers += 1;
|
|
1945
|
+
tools += list.filter((name) => name !== '*').length;
|
|
1946
|
+
}
|
|
1947
|
+
return { servers, tools };
|
|
1809
1948
|
},
|
|
1810
1949
|
warmUp,
|
|
1811
1950
|
scheduleToolRestrictions,
|
|
@@ -108,6 +108,11 @@ export function createArchiveEngine(deps) {
|
|
|
108
108
|
await deps.applySubagentSwitches(personasOn.map((n) => ({ name: n, enabled: true })));
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
// 模型工具表(C1):按快照原值整体回写。老快照没有这一栏 → 一个字都不动(与进场景前一致)。
|
|
112
|
+
if (snapshot.toolTableHidden) {
|
|
113
|
+
await deps.applyToolTableHidden(snapshot.toolTableHidden);
|
|
114
|
+
}
|
|
115
|
+
deps.audit?.('scene-restore', '');
|
|
111
116
|
}
|
|
112
117
|
/**
|
|
113
118
|
* 失败回滚:运行时还原 + 切片写回;每步失败都记下来,绝不谎报「已回滚」。
|
|
@@ -172,6 +177,15 @@ export function createArchiveEngine(deps) {
|
|
|
172
177
|
if (entries.length)
|
|
173
178
|
await deps.applyMcpNotes(entries);
|
|
174
179
|
}
|
|
180
|
+
// 工具表方案(C1):改档案 = 立刻生效,这一域也得跟上。快照里的原值**不动** ——
|
|
181
|
+
// 退出场景仍然回到「进场景前」那份名单,而不是回到上一次档案的方案。
|
|
182
|
+
const reapplyPreset = (archive && archive.toolTablePreset) || '';
|
|
183
|
+
if (reapplyPreset) {
|
|
184
|
+
const target = await deps.toolTablePresetHidden(reapplyPreset);
|
|
185
|
+
if (target)
|
|
186
|
+
await deps.applyToolTableHidden(target);
|
|
187
|
+
}
|
|
188
|
+
deps.audit?.('scene-apply', '');
|
|
175
189
|
return { mcpServers: mcpPlan.serverSwitches, skillSources: skillsPlan.sourceSwitches };
|
|
176
190
|
}
|
|
177
191
|
const ops = {
|
|
@@ -343,6 +357,21 @@ export function createArchiveEngine(deps) {
|
|
|
343
357
|
for (const s of skillsPlan.sourceSwitches)
|
|
344
358
|
(s.enabled ? sourcesOn : sourcesOff).push(s.root);
|
|
345
359
|
const stale = [...mcpPlan.stale, ...skillsPlan.stale];
|
|
360
|
+
// 工具表方案(C1):绑了就说清「会切成哪一份、关几条」。方案在别处被删掉 / 换机器之后
|
|
361
|
+
// 查不到时 hiddenCount 给 null,界面那句会改成"方案已不存在,这次不会切换"——
|
|
362
|
+
// 比让预览卡继续显示一条看不懂的数字诚实。
|
|
363
|
+
const previewPreset = (archive && archive.toolTablePreset) || '';
|
|
364
|
+
let toolPreset = null;
|
|
365
|
+
if (previewPreset) {
|
|
366
|
+
let list = null;
|
|
367
|
+
try {
|
|
368
|
+
list = await deps.toolTablePresetHidden(previewPreset);
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
return { ok: false, error: `读取模型工具表失败:${msg(e)}` };
|
|
372
|
+
}
|
|
373
|
+
toolPreset = { name: previewPreset, hiddenCount: list ? list.length : null };
|
|
374
|
+
}
|
|
346
375
|
const notesDefined = !!(archive && archive.mcpNotes);
|
|
347
376
|
const noteCount = notesDefined
|
|
348
377
|
? Object.keys(archive.mcpNotes).filter((server) => archive.mcp && server in archive.mcp).length
|
|
@@ -375,6 +404,7 @@ export function createArchiveEngine(deps) {
|
|
|
375
404
|
sourcesOn: clip(sourcesOn), sourcesOff: clip(sourcesOff),
|
|
376
405
|
},
|
|
377
406
|
subagents: { on: clip(personas.on), off: clip(personas.off) },
|
|
407
|
+
toolPreset,
|
|
378
408
|
stale: clip(stale),
|
|
379
409
|
truncated,
|
|
380
410
|
};
|
|
@@ -433,12 +463,29 @@ export function createArchiveEngine(deps) {
|
|
|
433
463
|
if (hadKeys && archive.subagents.length === 0)
|
|
434
464
|
delete archive.subagents;
|
|
435
465
|
}
|
|
466
|
+
if (archive.toolTablePreset) {
|
|
467
|
+
// 方案名查不到(被删了 / 档案是从别的机器带来的)→ 丢弃这一栏并报告:
|
|
468
|
+
// 绑一个不存在的方案不该在进场景时变成一句"方案不存在"的硬失败。
|
|
469
|
+
const found = await deps.toolTablePresetHidden(archive.toolTablePreset);
|
|
470
|
+
if (!found) {
|
|
471
|
+
stale.push('toolTable/' + archive.toolTablePreset);
|
|
472
|
+
delete archive.toolTablePreset;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
436
475
|
// 记忆段(P5 已废弃):不再写入、不再校验。记忆的开关是 `rules[*].enabled` 单一真相源,
|
|
437
476
|
// 老档案里残留的 `memories` 字段被忽略(不迁移、不删除,回滚代码时仍可读)。
|
|
438
477
|
// 保存档案时顺手把残留字段清掉,避免新旧语义并存造成误读。
|
|
439
478
|
if (archive.memories !== undefined)
|
|
440
479
|
delete archive.memories;
|
|
441
480
|
const slice = await deps.loadSlice();
|
|
481
|
+
// 工具表方案的绑定入口已改到「修改场景」表单(0.15.0 用户裁定):档案保存的载荷
|
|
482
|
+
// 不再携带这一栏,**不带 ≠ 解绑** —— 保留现值(解绑走 rules-update-scene 的空串;
|
|
483
|
+
// 带上来的值仍走上面的 stale 校验,保留的现值由进出场景时的 stale 处理兜底)。
|
|
484
|
+
if (archive.toolTablePreset === undefined) {
|
|
485
|
+
const prev = slice.archives[scene];
|
|
486
|
+
if (prev && typeof prev.toolTablePreset === 'string' && prev.toolTablePreset)
|
|
487
|
+
archive.toolTablePreset = prev.toolTablePreset;
|
|
488
|
+
}
|
|
442
489
|
if (Object.keys(archive).length === 0)
|
|
443
490
|
delete slice.archives[scene];
|
|
444
491
|
else
|
|
@@ -590,11 +637,28 @@ export function createArchiveEngine(deps) {
|
|
|
590
637
|
return { ok: false, error: `读取备注状态失败(未改动任何东西):${msg(e)}` };
|
|
591
638
|
}
|
|
592
639
|
}
|
|
640
|
+
// 工具表方案(C1):**改任何东西之前**先把现状读出来 —— 读失败就等于什么都没动。
|
|
641
|
+
// 没绑定方案就完全不碰这一域(也不往快照里加栏),老场景行为逐字节不变。
|
|
642
|
+
const toolTablePreset = (archive && archive.toolTablePreset) || '';
|
|
643
|
+
let toolTableBefore = null;
|
|
644
|
+
let toolTableTarget = null;
|
|
645
|
+
if (toolTablePreset) {
|
|
646
|
+
try {
|
|
647
|
+
toolTableBefore = await deps.currentToolTableHidden();
|
|
648
|
+
toolTableTarget = await deps.toolTablePresetHidden(toolTablePreset);
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
return { ok: false, error: `读取模型工具表现状失败(未改动任何东西):${msg(e)}` };
|
|
652
|
+
}
|
|
653
|
+
if (!toolTableTarget) {
|
|
654
|
+
return { ok: false, error: `场景绑定的工具表方案「${toolTablePreset}」已不存在,未进入场景(先在场景档案里换一份或取消绑定)` };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
593
657
|
// 快照**恒拍**:三个域都按「与勾选集完全一致」应用(未定义 = 全关),所以任何场景
|
|
594
658
|
// 进入都可能改动运行时(哪怕只是停掉几台服务器 / 几个技能),退出都得能精确还原。
|
|
595
659
|
// 上层两行传**现状全量**(上面的 mcpServerStates / skillSources 就是进场景前的值),
|
|
596
660
|
// 不是计划里那几行 —— 退出按「现状 ≠ 记录值」回写(见 mcpServerRowsToRestore)。
|
|
597
|
-
const snapshot = snapshotRuntime(await deps.currentMcpRaw(), await deps.currentSkills(), mcpServerStates.map((s) => ({ id: s.id, level: s.level, disabled: s.disabled })), skillSources.map((s) => ({ root: s.root, enabled: s.enabled })), personaRestore, personaRestoreOn, noteBefore, personaStates);
|
|
661
|
+
const snapshot = snapshotRuntime(await deps.currentMcpRaw(), await deps.currentSkills(), mcpServerStates.map((s) => ({ id: s.id, level: s.level, disabled: s.disabled })), skillSources.map((s) => ({ root: s.root, enabled: s.enabled })), personaRestore, personaRestoreOn, noteBefore, personaStates, toolTableBefore);
|
|
598
662
|
const entered = { ...slice, mode: { scene: target, snapshot }, active: [target] };
|
|
599
663
|
try {
|
|
600
664
|
await deps.saveSlice({ ...slice, mode: entered.mode });
|
|
@@ -629,6 +693,10 @@ export function createArchiveEngine(deps) {
|
|
|
629
693
|
if (noteTargets && noteTargets.length) {
|
|
630
694
|
await deps.applyMcpNotes(noteTargets.map((x) => ({ id: x.id, note: x.note })));
|
|
631
695
|
}
|
|
696
|
+
// ⑤ 模型工具表:档案绑了方案就把关停名单**整体换成**那份(省下的 token 按轮算,
|
|
697
|
+
// 所以它是"进入场景"这一动作里唯一直接改每请求内容的域)。
|
|
698
|
+
if (toolTableTarget)
|
|
699
|
+
await deps.applyToolTableHidden(toolTableTarget);
|
|
632
700
|
}
|
|
633
701
|
catch (e) {
|
|
634
702
|
return await rollback(slice, snapshot, '应用档案', e);
|
|
@@ -645,7 +713,7 @@ export function createArchiveEngine(deps) {
|
|
|
645
713
|
mode: entered.mode,
|
|
646
714
|
// 三个域都是「按勾选集全量应用」(未定义 = 全关),所以都算已应用;
|
|
647
715
|
// 备注段未定义 = 不覆盖任何备注,如实报它是否定义。
|
|
648
|
-
applied: { mcp: true, skills: true, subagents: true, mcpNotes: !!(archive && archive.mcpNotes) },
|
|
716
|
+
applied: { mcp: true, skills: true, subagents: true, mcpNotes: !!(archive && archive.mcpNotes), toolTable: !!toolTableTarget },
|
|
649
717
|
// 上层实际切换了几个(0 = 本来就已经是目标状态,界面不必提示"已停用 N 台")。
|
|
650
718
|
switched: {
|
|
651
719
|
mcpServers: mcpPlan.serverSwitches.length,
|
package/lib/memories/archive.js
CHANGED
|
@@ -66,6 +66,10 @@ export function normalizeArchive(raw) {
|
|
|
66
66
|
out.subagents = normalizeStringList(obj.subagents);
|
|
67
67
|
if (obj.memories != null)
|
|
68
68
|
out.memories = normalizeStringList(obj.memories);
|
|
69
|
+
// 工具表方案是**标量**不是集合:空串 / 非字符串一律视为未绑定(段不存在)。
|
|
70
|
+
const toolTablePreset = typeof obj.toolTablePreset === 'string' ? obj.toolTablePreset.trim() : '';
|
|
71
|
+
if (toolTablePreset)
|
|
72
|
+
out.toolTablePreset = toolTablePreset;
|
|
69
73
|
return out;
|
|
70
74
|
}
|
|
71
75
|
export function hasSection(archive, section) {
|
|
@@ -202,7 +206,9 @@ mcpNotes = [],
|
|
|
202
206
|
* 进入模式时**全部**人设的开关状态(名字 → 是否开着)。给了就写进 `subagentsAll`:
|
|
203
207
|
* 退出时按它精确还原(场景里手动开过的人设也会被还原回进场景前的状态)。
|
|
204
208
|
*/
|
|
205
|
-
subagentStates = null
|
|
209
|
+
subagentStates = null,
|
|
210
|
+
/** 进场景前的模型工具表关停名单(null = 这次不进快照,退出也就不动工具表)。 */
|
|
211
|
+
toolTableHidden = null) {
|
|
206
212
|
return {
|
|
207
213
|
mcp: Object.fromEntries(Object.entries(mcpRaw).map(([k, v]) => [k, v.slice()])),
|
|
208
214
|
skills: { ...skills },
|
|
@@ -212,6 +218,7 @@ subagentStates = null) {
|
|
|
212
218
|
...(subagentsOn.length ? { subagentsOn: subagentsOn.slice() } : {}),
|
|
213
219
|
...(subagentStates ? { subagentsAll: { ...subagentStates } } : {}),
|
|
214
220
|
...(mcpNotes.length ? { mcpNotes: mcpNotes.map((x) => ({ id: x.id, note: x.note })) } : {}),
|
|
221
|
+
...(toolTableHidden ? { toolTableHidden: toolTableHidden.slice() } : {}),
|
|
215
222
|
};
|
|
216
223
|
}
|
|
217
224
|
// ── 记忆勾选(v3)──────────────────────────────────────────────────────────
|
package/lib/memories/index-io.js
CHANGED
|
@@ -157,6 +157,11 @@ export function parseModeState(raw) {
|
|
|
157
157
|
.filter((x) => !!x && typeof x === 'object')
|
|
158
158
|
.map((x) => ({ id: String(x.id || ''), note: typeof x.note === 'string' ? x.note : null }))
|
|
159
159
|
.filter((x) => x.id !== '');
|
|
160
|
+
// v0.15 的模型工具表原值名单:**同样必须原样透传**(剥掉它 = 退出场景后工具表停在方案那份,
|
|
161
|
+
// 与历史上 mcpServers / mcpNotes 被剥掉是同一类 bug)。老快照没有这一栏 → null = 不还原。
|
|
162
|
+
const toolTableHidden = Array.isArray(snapshotRaw.toolTableHidden)
|
|
163
|
+
? snapshotRaw.toolTableHidden.map((x) => String(x))
|
|
164
|
+
: null;
|
|
160
165
|
return {
|
|
161
166
|
scene,
|
|
162
167
|
snapshot: scene
|
|
@@ -169,6 +174,7 @@ export function parseModeState(raw) {
|
|
|
169
174
|
...(subagentsOn.length ? { subagentsOn } : {}),
|
|
170
175
|
...(Object.keys(subagentsAll).length ? { subagentsAll } : {}),
|
|
171
176
|
...(mcpNotes.length ? { mcpNotes } : {}),
|
|
177
|
+
...(toolTableHidden ? { toolTableHidden } : {}),
|
|
172
178
|
}
|
|
173
179
|
: null,
|
|
174
180
|
};
|
package/lib/op-registry.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// "会不会改宿主或外部系统的状态",而上面那些写的是插件自己的侧车(丢了可重建)。
|
|
32
32
|
// 想要它们也带令牌就显式改成 write,别靠"读操作带副作用"这句话推。
|
|
33
33
|
export const OP_REGISTRY = Object.freeze({
|
|
34
|
-
// ── MCP 域(
|
|
34
|
+
// ── MCP 域(17)────────────────────────────────────────────────────────────
|
|
35
35
|
'mcpm-list': { annotatesLock: true },
|
|
36
36
|
// reveal 返回**未打码**的凭据。它是读操作,但按写门禁 —— 局域网暴露的端口上,令牌对
|
|
37
37
|
// 明文凭据必须是最后一道防线,而不只对着写操作。
|
|
@@ -55,6 +55,9 @@ export const OP_REGISTRY = Object.freeze({
|
|
|
55
55
|
// 会停在启用态 —— 是写不是读。(0.6.0 / 0.7.0 各有漏列前科,见文件头。)
|
|
56
56
|
'mcpm-tools-refresh': { write: true },
|
|
57
57
|
'mcpm-tools': { readonly: true },
|
|
58
|
+
// 配置体检:只读补丁文件 + 一次 PATH 查询(`where`/`which`,不执行配置里的命令)。
|
|
59
|
+
// 返回的是检查项 id,不回任何字段值 —— 与 `mcpm-list` 同档,不带令牌。
|
|
60
|
+
'mcpm-inspect': { readonly: true },
|
|
58
61
|
// ── 技能域(22,含内联的 skill-open)───────────────────────────────────────
|
|
59
62
|
'skill-state': { annotatesLock: true },
|
|
60
63
|
'skill-detail': { readonly: true },
|
|
@@ -164,15 +167,26 @@ export const OP_REGISTRY = Object.freeze({
|
|
|
164
167
|
'bundle-export': { write: true },
|
|
165
168
|
// 新增一条宿主工作区登记 —— 改的是宿主侧的登记,不是插件自己的侧车。
|
|
166
169
|
'history-workspace-register': { write: true },
|
|
167
|
-
// ── 兼容 / 注入 /
|
|
170
|
+
// ── 兼容 / 注入 / 备份 / 流水 / 迁移快照(11)────────────────────────────────
|
|
168
171
|
'compat-status': { readonly: true },
|
|
172
|
+
// 「最近改动」流水的读侧:只读 hub 里的 `audit.jsonl`(内容只有对象名,见 audit-log.ts 第 ① 条)。
|
|
173
|
+
'audit-list': { readonly: true },
|
|
174
|
+
// 清空流水:删的是插件自己的文件,但它是**一次改动**(清完还会留下一条 `audit-clear`),按写门禁。
|
|
175
|
+
'audit-clear': { write: true },
|
|
176
|
+
// 整机迁移快照(0.15.0 C2)。export 标 sensitive 与 `mcpm-export` 同档:`includeSecrets` 时
|
|
177
|
+
// 它交出的就是明文凭据;不配令牌时不设防(判据见 http-fence.ts 的 secretOpRejection)。
|
|
178
|
+
'snapshot-export': { write: true, sensitive: true },
|
|
179
|
+
'snapshot-preview': { readonly: true },
|
|
180
|
+
// 逐域调各域自己的 import op —— 那些 op 本来就被场景冻结挡着,这里不重复标 frozen,
|
|
181
|
+
// 免得两道锁口径分家(预览与执行都走同一套门禁)。
|
|
182
|
+
'snapshot-import': { write: true },
|
|
169
183
|
'feature-overview': { readonly: true },
|
|
170
184
|
'preset-reach': { readonly: true },
|
|
171
|
-
//
|
|
185
|
+
// 全部注入域的当前正文**全文**都从这里出去。今天不带令牌就能读,是这批只读里披露面最大的一条;
|
|
172
186
|
// 要不要上门禁是产品决定,登记在这里是为了让它可数。
|
|
173
187
|
'injection-live': { readonly: true },
|
|
174
188
|
'backups-list': { readonly: true },
|
|
175
|
-
//
|
|
189
|
+
// 注入设置(各域开关 / 压制型预设口径)写侧车。它是"配置"不是"宿主状态",但改的是
|
|
176
190
|
// 投递语义,按写操作门禁。
|
|
177
191
|
'inject-settings': { write: true },
|
|
178
192
|
// 模型工具表(哪些工具根本不发给模型)也写侧车,改的是每轮请求的内容 —— 同上按写门禁。
|
package/lib/ops/compat.js
CHANGED
|
@@ -129,15 +129,35 @@ export function buildCompatOps(deps) {
|
|
|
129
129
|
const domainsOn = INJECT_DOMAIN_KEYS.filter((key) => settings.domains[key] !== false);
|
|
130
130
|
const token = await deps.readTokenState();
|
|
131
131
|
const lockedScenes = await deps.lockedSceneNames();
|
|
132
|
-
const disabledTools = deps.
|
|
132
|
+
const disabledTools = deps.disabledToolSummary();
|
|
133
133
|
const routeOf = (operation) => assessment === undefined ? undefined : routeFor(assessment, operation);
|
|
134
|
-
/** 一行一个功能点:状态三层合成,`detail` 写明依据。 */
|
|
135
134
|
const rows = [];
|
|
136
|
-
const push = (
|
|
137
|
-
|
|
135
|
+
const push = (row) => rows.push(row);
|
|
136
|
+
/** note 降级行的公共字段:透传 note 的 detailKey / params,没有结构化键就只带原文。 */
|
|
137
|
+
const noteFields = (note) => note === undefined ? {}
|
|
138
|
+
: { ...(note.detailKey === undefined ? {} : { detailKey: note.detailKey }), ...(note.params === undefined ? {} : { params: note.params }) };
|
|
139
|
+
/** 路由判定行的公共字段:可用带 via;不可用带缺失能力的 id + 原始标签(客户端出各语言标签)。 */
|
|
140
|
+
const routeFields = (decision) => {
|
|
141
|
+
if (decision === undefined)
|
|
142
|
+
return {};
|
|
143
|
+
return decision.via === 'none'
|
|
144
|
+
? { refusals: decision.refusals.map((item) => ({ id: item.id, label: item.label })) }
|
|
145
|
+
: { via: decision.via };
|
|
146
|
+
};
|
|
147
|
+
const routeDetail = (decision) => decision === undefined ? '能力探测不可用(归档服务未挂载)'
|
|
148
|
+
: decision.via === 'none' ? '宿主缺少该路径所需能力:' + decision.refusals.map((item) => item.label).join('、')
|
|
149
|
+
: `可用(路径:${decision.via})`;
|
|
150
|
+
// 前置项:插件挂载(这一行本身说明 inject 都解析了 —— 否则 apply 根本不会跑);
|
|
138
151
|
// 启动期那段 `!!js` 表达式若抛错,插件同样不会挂上(它的 try/catch 就是为了不抛)。
|
|
139
152
|
const mountNote = notes.get('cordis-original-symbol');
|
|
140
|
-
push(
|
|
153
|
+
push({
|
|
154
|
+
key: 'mount', label: '插件挂载', tab: 'compat',
|
|
155
|
+
state: mountNote === undefined ? 'ok' : 'degraded',
|
|
156
|
+
detail: mountNote === undefined
|
|
157
|
+
? `${deps.injectServiceCount} 个 inject 服务全部解析,插件已挂载(详见 doctor 的挂载心跳)`
|
|
158
|
+
: mountNote.detail,
|
|
159
|
+
...(mountNote === undefined ? { params: { count: deps.injectServiceCount } } : noteFields(mountNote)),
|
|
160
|
+
});
|
|
141
161
|
// 装配层:有上报就是降级,附上报里的原因;没有就是正常。
|
|
142
162
|
const assemblyRows = [
|
|
143
163
|
['patch-write-guard', '宿主配置写入(补丁校验)', 'mcp'],
|
|
@@ -149,52 +169,106 @@ export function buildCompatOps(deps) {
|
|
|
149
169
|
];
|
|
150
170
|
for (const [noteId, label, tab] of assemblyRows) {
|
|
151
171
|
const note = notes.get(noteId);
|
|
152
|
-
push(
|
|
172
|
+
push({
|
|
173
|
+
key: noteId, label, tab, state: note === undefined ? 'ok' : 'degraded',
|
|
174
|
+
detail: note === undefined ? '装配正常,无降级上报' : note.detail,
|
|
175
|
+
...noteFields(note),
|
|
176
|
+
});
|
|
153
177
|
}
|
|
154
178
|
// 宿主能力层:删除 / 归档 / 恢复 / 批量各自由路由判定回答(与服务端执行同源)。
|
|
155
179
|
const deleteRoute = routeOf('delete');
|
|
156
|
-
push(
|
|
157
|
-
:
|
|
158
|
-
|
|
180
|
+
push({
|
|
181
|
+
key: 'session-delete', label: '会话删除', tab: 'sessions',
|
|
182
|
+
state: deleteRoute === undefined ? 'unknown' : deleteRoute.via === 'none' ? 'unavailable' : 'ok',
|
|
183
|
+
detail: routeDetail(deleteRoute), ...routeFields(deleteRoute),
|
|
184
|
+
});
|
|
159
185
|
for (const [operation, label] of [['archive', '归档'], ['unarchive', '恢复'], ['batch', '批量操作'], ['list', '历史列表']]) {
|
|
160
186
|
const decision = routeOf(operation);
|
|
161
|
-
push(
|
|
162
|
-
:
|
|
163
|
-
|
|
187
|
+
push({
|
|
188
|
+
key: 'session-' + operation, label: `会话${label}`, tab: 'sessions',
|
|
189
|
+
state: decision === undefined ? 'unknown' : decision.via === 'none' ? 'unavailable' : 'ok',
|
|
190
|
+
detail: routeDetail(decision), ...routeFields(decision),
|
|
191
|
+
});
|
|
164
192
|
}
|
|
165
193
|
// 用户配置层:注入域 / 令牌 / 场景锁定 / 停用工具数。
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
push(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
// 分母必须取自 INJECT_DOMAIN_KEYS:0.14.0 场景与记忆分家后域从 5 个变 6 个,
|
|
195
|
+
// 写死的「/5」把 6/6 报成 6/5(2026-09-28 用户实测)。
|
|
196
|
+
const domainTotal = INJECT_DOMAIN_KEYS.length;
|
|
197
|
+
push({
|
|
198
|
+
key: 'injection-domains', label: '注入域开关', tab: 'compat',
|
|
199
|
+
state: domainsOn.length === 0 ? 'disabled' : 'ok',
|
|
200
|
+
detail: domainsOn.length === 0 ? `${domainTotal} 个注入域全部关闭(用户设置)` : `${domainsOn.length}/${domainTotal} 个域开启:${domainsOn.join('、')}`,
|
|
201
|
+
domains: domainsOn, count: domainsOn.length, total: domainTotal,
|
|
202
|
+
});
|
|
203
|
+
push({
|
|
204
|
+
key: 'token', label: '访问令牌', tab: 'compat',
|
|
205
|
+
state: !token.active ? 'disabled' : token.accepted ? 'ok' : 'locked',
|
|
206
|
+
detail: !token.active ? '未启用(宿主没配令牌,或令牌功能被关掉)'
|
|
207
|
+
: token.accepted ? '已生效且本次启动已通过验证' : '已生效,本次启动尚未验证:写操作与对话会被拦住,到本页下方填写令牌',
|
|
208
|
+
});
|
|
209
|
+
push({
|
|
210
|
+
key: 'scene-lock', label: '场景锁定', tab: 'scenes',
|
|
211
|
+
state: lockedScenes.length === 0 ? 'ok' : 'locked-scene',
|
|
212
|
+
detail: lockedScenes.length === 0 ? '无锁定场景' : `锁定中:${lockedScenes.join('、')}(写门禁按锁定场景生效)`,
|
|
213
|
+
scenes: lockedScenes,
|
|
214
|
+
});
|
|
215
|
+
// 停用是用户配置或场景收窄的**事实**,不是故障:功能本身的好坏由上面「工具表可见性」
|
|
216
|
+
// 装配行报(`mcp-tool-visibility` 降级 = 拦截/摘除没装上),这一行只如实陈述停用构成,
|
|
217
|
+
// 状态恒为 ok —— 否则"只开了几台 MCP"的用户会一直看到琥珀,以为出了问题
|
|
218
|
+
// (2026-09-28 用户裁定:琥珀只留给功能出问题)。
|
|
219
|
+
// 「N 个工具」也说不清整台停用:一条 `*` 通配背后是那台服务器的全部工具,
|
|
220
|
+
// 按条目数报会把 6 台整台停用读成"6 个工具"。两个口径分开说:台数只数 `*`,工具数只数单独停用的条目。
|
|
221
|
+
const disabledPart = disabledTools.servers > 0 && disabledTools.tools > 0
|
|
222
|
+
? `${disabledTools.servers} 台服务器整台停用 + ${disabledTools.tools} 个工具单独停用`
|
|
223
|
+
: disabledTools.servers > 0
|
|
224
|
+
? `${disabledTools.servers} 台服务器整台停用`
|
|
225
|
+
: disabledTools.tools > 0
|
|
226
|
+
? `${disabledTools.tools} 个工具单独停用`
|
|
227
|
+
: '没有停用的工具';
|
|
228
|
+
push({
|
|
229
|
+
key: 'mcp-tools', label: 'MCP 工具停用', tab: 'mcp', state: 'ok',
|
|
230
|
+
detail: disabledPart + (disabledTools.servers + disabledTools.tools === 0 ? '' : '(执行拦截 + 可见性摘除)'),
|
|
231
|
+
servers: disabledTools.servers, tools: disabledTools.tools,
|
|
232
|
+
});
|
|
233
|
+
// 工具表按每个请求付钱:这一行回答"这一轮实际发出去多少"。关掉(无论出厂默认还是
|
|
234
|
+
// 用户自己关)都是**配置事实**而非故障 —— 状态恒为 ok,琥珀只留给功能出问题
|
|
235
|
+
// (口径与 MCP 工具停用一致,2026-09-28 用户裁定)。出厂默认关掉的那几条单说:
|
|
236
|
+
// 那是插件替用户做的一个可逆选择,与用户自己关的分开计数。
|
|
237
|
+
// 说明只保留一句关了多少:省多少 tok、去哪开关、面板不受影响、skill_manager_read
|
|
238
|
+
// 让位这些次级信息都不上功能总览(正常行一行读完,2026-09-28 用户反馈);
|
|
239
|
+
// 逐会话的让位差额(index.ts 的 CARRIER_DUPLICATES)在别处自有交代。
|
|
178
240
|
const table = deps.toolTableReport();
|
|
179
|
-
const carrierNote = ';官方 `skill` 工具在场的会话,`skill_manager_read` 还会自动让位一份';
|
|
180
241
|
const userOff = table.hiddenCount - table.defaultHiddenCount;
|
|
181
242
|
const offPart = table.hiddenCount === 0
|
|
182
|
-
? `${table.totalCount}
|
|
243
|
+
? `${table.totalCount} 个工具全部下发`
|
|
183
244
|
: (userOff === 0
|
|
184
245
|
? `出厂默认关掉 ${table.defaultHiddenCount}/${table.totalCount} 个`
|
|
185
|
-
: `关掉 ${table.hiddenCount}/${table.totalCount} 个(含出厂默认 ${table.defaultHiddenCount} 个)`)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
246
|
+
: `关掉 ${table.hiddenCount}/${table.totalCount} 个(含出厂默认 ${table.defaultHiddenCount} 个)`);
|
|
247
|
+
push({
|
|
248
|
+
key: 'tool-table', label: '模型工具表', tab: 'compat', state: 'ok', detail: offPart,
|
|
249
|
+
hidden: table.hiddenCount, total: table.totalCount, defaults: table.defaultHiddenCount,
|
|
250
|
+
});
|
|
251
|
+
const nativeNote = notes.get('workspace.delete-native');
|
|
252
|
+
push({
|
|
253
|
+
key: 'native-delete', label: '宿主原生删除入口', tab: 'compat',
|
|
254
|
+
state: nativeNote ? 'partial' : 'ok',
|
|
255
|
+
detail: nativeNote?.detail ?? '宿主未提供原生删除入口(本插件自有完整序列)',
|
|
256
|
+
...noteFields(nativeNote),
|
|
257
|
+
});
|
|
189
258
|
// 身份 / 版本:与 compat-status 同一份数据。
|
|
190
259
|
const identity = assessment?.identity;
|
|
191
260
|
const identityIssue = identity !== undefined && (identity.blockers.length > 0 || Object.values(identity.sameAsHost).some((same) => same === false));
|
|
192
|
-
push(
|
|
193
|
-
:
|
|
194
|
-
|
|
261
|
+
push({
|
|
262
|
+
key: 'host-identity', label: '宿主身份与模块', tab: 'compat',
|
|
263
|
+
state: assessment === undefined ? 'unknown' : identityIssue ? 'degraded' : 'ok',
|
|
264
|
+
detail: assessment === undefined ? '能力探测不可用(归档服务未挂载)'
|
|
265
|
+
: identityIssue ? '存在阻塞项:' + (identity?.blockers ?? []).join(';')
|
|
266
|
+
: `模块与宿主同源(宿主 DSH ${identity?.version ?? '?'})`,
|
|
267
|
+
...(assessment === undefined ? {} : { version: identity?.version ?? '?', blockers: identity?.blockers ?? [] }),
|
|
268
|
+
});
|
|
195
269
|
// 无独立装配点的域:没有上报就是正常(如实说明依据是"没有降级上报")。
|
|
196
270
|
for (const [key, label, tab] of [['memory', '记忆', 'memory'], ['prompts', '提示词', 'prompts'], ['subagents', '子智能体', 'subagents'], ['scenes', '场景档案', 'scenes']]) {
|
|
197
|
-
push(key, label, tab, 'ok', '无降级上报(装配失败会出现在这里)');
|
|
271
|
+
push({ key, label, tab, state: 'ok', detail: '无降级上报(装配失败会出现在这里)' });
|
|
198
272
|
}
|
|
199
273
|
return { ok: true, rows, generatedAt: Date.now() };
|
|
200
274
|
}
|
|
@@ -230,7 +304,7 @@ export function buildCompatOps(deps) {
|
|
|
230
304
|
...(r.failed.length ? { failedCount: r.failed.length, failedNames: r.failed } : {}),
|
|
231
305
|
};
|
|
232
306
|
},
|
|
233
|
-
//
|
|
307
|
+
// 注入实况(只读):最近活跃会话里模型**真正看到**的各域文本 + 那一段对话的投递统计。
|
|
234
308
|
// 回答"勾了开关到底送没送到"——界面配置与实际注入不一致时,这里一眼可见。
|
|
235
309
|
'injection-live': async () => {
|
|
236
310
|
const live = deps.getContextInjectorLive();
|