dsh-plugin-tool-management 0.9.1 → 0.11.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 +119 -1
- package/README.md +227 -201
- package/README_EN.md +227 -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 +144 -12
- package/lib/client.js +8437 -5929
- package/lib/compat/preset-reach.js +1 -10
- package/lib/compat/probe.js +158 -25
- package/lib/context-inject.js +396 -53
- package/lib/host-names.js +12 -0
- package/lib/http-fence.js +35 -15
- package/lib/hub.js +31 -3
- package/lib/imports/parsers.js +15 -9
- package/lib/imports/upload.js +43 -4
- package/lib/index.js +940 -3765
- package/lib/mcp/loader-token.js +238 -0
- package/lib/mcp/manager.js +1681 -0
- package/lib/mcp/override-blocks.js +20 -9
- package/lib/mcp/patch-yaml.js +351 -0
- package/lib/mcp/secret-guard.js +145 -0
- package/lib/mcp/state-section.js +64 -21
- package/lib/{rules → memories}/archive-engine.js +65 -10
- package/lib/{rules → memories}/archive.js +6 -7
- package/lib/memories/constants.js +128 -0
- package/lib/memories/index-io.js +330 -0
- package/lib/memories/projection.js +280 -0
- package/lib/memories/service.js +686 -0
- package/lib/memories/snapshot.js +672 -0
- package/lib/ops/candidates.js +64 -0
- package/lib/ops/compat.js +136 -0
- package/lib/ops/ctx.js +9 -0
- package/lib/ops/memory.js +678 -0
- package/lib/ops/prompts.js +107 -0
- package/lib/ops/scene-records.js +460 -0
- package/lib/ops/scene-sync.js +17 -0
- package/lib/ops/sessions.js +603 -0
- package/lib/ops/trash.js +140 -0
- package/lib/paths.js +103 -0
- package/lib/prompts/preset-id.js +49 -0
- package/lib/{agents-md → prompts}/service.js +1 -1
- package/lib/request-gate.js +320 -0
- package/lib/scene-prompt-sync.js +4 -4
- package/lib/scenes/candidates.js +344 -0
- package/lib/{history → sessions}/bridge.js +22 -5
- package/lib/sessions/history.js +323 -0
- package/lib/{history → sessions}/tombstone.js +1 -2
- package/lib/{history → sessions}/workspace.js +92 -24
- package/lib/skills/catalog.js +6 -9
- package/lib/skills/core.js +79 -43
- package/lib/skills/readonly-discovery.js +4 -1
- package/lib/skills/service.js +98 -13
- package/lib/subagents/catalog.js +31 -15
- package/lib/subagents/service.js +506 -89
- package/lib/subagents/tools.js +29 -4
- package/lib/tools/deps.js +8 -0
- package/lib/tools/mcp.js +110 -0
- package/lib/tools/memory.js +87 -0
- package/lib/tools/prompt.js +70 -0
- package/lib/tools/skills.js +139 -0
- package/lib/tools/subagent.js +40 -0
- package/package.json +105 -102
- package/lib/agents-md/preset-id.js +0 -49
- package/lib/history/projcache.js +0 -335
- package/lib/rules/service.js +0 -2971
package/lib/mcp/state-section.js
CHANGED
|
@@ -9,18 +9,33 @@
|
|
|
9
9
|
// 这类信息在任何工具 schema 里都拿不到:工具只暴露 `mcp__<server>__<tool>` 的名字与
|
|
10
10
|
// 参数,永远不会说「A 挂了改用 B」。所以「列出 server」只是载体。
|
|
11
11
|
//
|
|
12
|
-
// 范围(2026-09-15
|
|
13
|
-
// -
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// 范围(2026-09-15 用户裁定;2026-09-17 按实测修正"可用"的判据):
|
|
13
|
+
// - 分两档,判据全部可验证:
|
|
14
|
+
// ① **可用**:配置上已启用 **且** 当前真实注册了未被停用的工具
|
|
15
|
+
// (`liveEnabledToolCount > 0`;场景档案收窄与手动逐工具开关都会写停用表,这里如实扣减)。
|
|
16
|
+
// ② **曾经连上、现在不可用**:已启用、当前一个工具都没注册,但「已知工具」缓存非空。
|
|
17
|
+
// 这是**状态变化**,要报 —— 模型据此给"检查网络/命令/凭据"的建议,而不是把
|
|
18
|
+
// "配了但连不上"读成"本机没配"(后者会让它建议用户去装一个)。同时,用户写在那台
|
|
19
|
+
// server 上的备注(placeholder 就是「A 不可用时改用 B 兜底」)恰恰只在此时才有用,
|
|
20
|
+
// 不列就永远送不到。
|
|
21
|
+
// 不列的三类:**没开**(用户明确要求)、**工具全被停用**(模型调不到,留着只会谎报数量)、
|
|
22
|
+
// **从未连上过**(缓存也空 —— 一直没成功过,不该反复打扰;这条抄的是 Claude Code 的通知
|
|
23
|
+
// 策略:一直没连上的不提示,昨天还好今天挂的才提示)。
|
|
16
24
|
// - 只列 **server 名 + 工具数 + 备注**;**具体工具名绝不注入** ——
|
|
17
25
|
// 已启用 server 的工具本来就在模型自己的工具 schema 里(`mcp__<server>__<tool>`),
|
|
18
26
|
// 段里再列一遍是重复;模型按前缀就能对上号。
|
|
19
27
|
// - 工具数只算**当前可用**的:被停用的那一部分不显示(它就是模型 schema 里没有的),
|
|
20
|
-
//
|
|
28
|
+
// 数字必须与模型能调到的工具对得上。②档没有当前数字,所以写的是「上次连上时 N 个工具」——
|
|
29
|
+
// 那是历史事实,不是现状,措辞上必须区分开。
|
|
21
30
|
// - 无备注 → 只有名字 + 工具数(正是「不写就只知道名字」)。
|
|
22
|
-
// -
|
|
23
|
-
// -
|
|
31
|
+
// - 不写「已启用」字样:能进段的都是可用的,标状态是废话;②档则必须标,否则就是谎报。
|
|
32
|
+
// - 两档都空 → 返回 `''`(renderPrompt 删空段,零 token 成本)。
|
|
33
|
+
//
|
|
34
|
+
// 一条**测不出来**的边界(写在这里免得后来者以为它是 bug):宿主没有暴露"MCP 已连接"这个信号
|
|
35
|
+
// (插件清单只有 entryId / moduleName / enabled / fiberPhase),所以本模块只能读"工具有没有注册"。
|
|
36
|
+
// 而官方 dsh-mcp-client 在**掉线后不会立刻摘掉工具**:`maxAttempts: 10`、退避 500ms 翻倍到 30s,
|
|
37
|
+
// 约 2.5 分钟后才 `tools unregistered`;若把 `reconnect.enabled` 设为 false 则**永久保留**。
|
|
38
|
+
// 也就是说那个窗口内本段仍会把它算作①档。这不是判据写错,是插件身份决定的观测上限。
|
|
24
39
|
//
|
|
25
40
|
// 同步性:`text()` 必须同步返回(renderPrompt 不接受 Promise),而 MCP 清单是异步读盘 →
|
|
26
41
|
// 与子智能体目录同构的 stale-while-revalidate(见 src/subagents/catalog.ts)。
|
|
@@ -53,32 +68,60 @@ export function normalizeMcpNote(value, maxLength) {
|
|
|
53
68
|
/**
|
|
54
69
|
* 渲染 MCP 状态段(纯函数,便于单独推理)。
|
|
55
70
|
*
|
|
71
|
+
* 分两档:**可用**(当前真注册了可用工具)与**曾经连上、现在不可用**(当前没注册,
|
|
72
|
+
* 但「已知工具」缓存证明它曾经成功过)。判据与不列的三类见文件头。
|
|
73
|
+
*
|
|
56
74
|
* 返回 `''` 表示不注入 —— 没有可用 server 的用户零 token 成本。
|
|
57
75
|
*/
|
|
58
76
|
export function renderMcpStateSection(rows, opts = {}) {
|
|
59
77
|
const maxEntries = opts.maxEntries ?? DEFAULT_MCP_MAX_ENTRIES;
|
|
60
78
|
const noteMaxLength = opts.noteMaxLength ?? DEFAULT_MCP_NOTE_MAX_LENGTH;
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
79
|
+
// 当前真实可用的工具数(停用表扣减后)。**优先读 live 那个数** —— 旧调用方只给
|
|
80
|
+
// enabledToolCount / toolCount 时逐级退回,行为与拆分之前一致(那个数在"从未连上过"的
|
|
81
|
+
// server 上恰好也是 0,所以退回不会造成谎报,只会少报)。
|
|
82
|
+
const liveEnabledOf = (r) => {
|
|
83
|
+
if (Number.isFinite(Number(r.liveEnabledToolCount)))
|
|
84
|
+
return Number(r.liveEnabledToolCount);
|
|
85
|
+
return Number.isFinite(Number(r.enabledToolCount)) ? Number(r.enabledToolCount) : (Number(r.toolCount) || 0);
|
|
86
|
+
};
|
|
87
|
+
// 当前注册了**几个**工具(不扣停用)。用来把"什么都没注册"与"注册了但都被停用"分开:
|
|
88
|
+
// 后者是用户的选择(该不列),前者才是"连不上"(该列并标注)。
|
|
89
|
+
const liveOf = (r) => (Number.isFinite(Number(r.liveToolCount)) ? Number(r.liveToolCount) : liveEnabledOf(r));
|
|
90
|
+
// 曾经连上过的证据。缺省 0 = 从未连上过(旧调用方给不出这个信号,按"不列"处理 —— 少说不错)。
|
|
91
|
+
const knownOf = (r) => Number(r.knownToolCount) || 0;
|
|
92
|
+
const usable = rows.filter((r) => !r.disabled && liveEnabledOf(r) > 0);
|
|
93
|
+
const offline = rows.filter((r) => !r.disabled && liveOf(r) === 0 && knownOf(r) > 0);
|
|
94
|
+
if (!usable.length && !offline.length)
|
|
66
95
|
return '';
|
|
67
96
|
// 按展示名排序:即使补丁文件里的行序变化,段文本也保持逐字节稳定(前缀缓存契约)。
|
|
68
|
-
|
|
97
|
+
// 两档各自排序,可用的一律排在前面(它才是模型当下能用的东西)。
|
|
98
|
+
const byName = (a, b) => {
|
|
69
99
|
const x = String(a.serverName ?? '');
|
|
70
100
|
const y = String(b.serverName ?? '');
|
|
71
101
|
return x < y ? -1 : x > y ? 1 : 0;
|
|
72
|
-
}
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
};
|
|
103
|
+
const cap = Math.max(0, maxEntries);
|
|
104
|
+
const shownUsable = [...usable].sort(byName).slice(0, cap);
|
|
105
|
+
const shownOffline = [...offline].sort(byName).slice(0, cap);
|
|
106
|
+
const lineOf = (r, count, isOffline) => {
|
|
107
|
+
const name = String(r.serverName ?? r.id);
|
|
108
|
+
// ②档必须把状态写出来:不写就等于谎报"可用"。「上次连上时」四个字不能省 ——
|
|
109
|
+
// 那个数字是历史事实,写成"(N 个工具)"会被读成现状。
|
|
110
|
+
const head = isOffline
|
|
111
|
+
? '- **' + name + '**(当前未连上;上次连上时 ' + count + ' 个工具)'
|
|
112
|
+
: '- **' + name + '**(' + count + ' 个工具)';
|
|
77
113
|
const note = normalizeMcpNote(r.notes, noteMaxLength);
|
|
78
114
|
return note ? head + ' — ' + MCP_NOTE_PREFIX + note : head;
|
|
79
|
-
}
|
|
80
|
-
const
|
|
81
|
-
|
|
115
|
+
};
|
|
116
|
+
const lines = [
|
|
117
|
+
...shownUsable.map((r) => lineOf(r, liveEnabledOf(r), false)),
|
|
118
|
+
...shownOffline.map((r) => lineOf(r, knownOf(r), true)),
|
|
119
|
+
];
|
|
120
|
+
// 只有清单:标题(`## 本机 MCP 服务器的当前状态`)与"该拿它做什么"由注入通道的框架承担
|
|
121
|
+
// (2026-09-18 起,见 context-inject.ts 的 DOMAIN_FRAME)—— 同一件事只有一个出处,
|
|
122
|
+
// 框架已经写了标题,正文再写一遍就是每步多付一行 token。
|
|
123
|
+
const out = [...lines];
|
|
124
|
+
const hidden = (usable.length + offline.length) - (shownUsable.length + shownOffline.length);
|
|
82
125
|
if (hidden > 0)
|
|
83
126
|
out.push('', `(另有 ${hidden} 台未列出。)`);
|
|
84
127
|
return out.join('\n');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/memories/archive-engine.ts —— 场景档案引擎:sidecar 读写 + 运行时应用,全部经 deps 注入(无直接 I/O)。
|
|
2
2
|
// 状态机(设计 §2.2):进入 = 快照 → 先落盘 mode(留可退路径)→ 应用已定义段 → 记忆收窄为 {S};
|
|
3
3
|
// 退出 = 恢复快照 → 落盘自由模式;切换 = 先退后进。
|
|
4
4
|
// 失败语义(fail-closed):任一步失败即反向恢复运行时并写回旧切片;回滚不全会如实写进错误文本。
|
|
@@ -12,22 +12,70 @@ export function createArchiveEngine(deps) {
|
|
|
12
12
|
chain = queued.catch(() => undefined);
|
|
13
13
|
return queued;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* 快照里的上层两行 → 退出时**真正要回写**的那些:现状 ≠ 快照值、且这一行现在还存在。
|
|
17
|
+
*
|
|
18
|
+
* 为什么必须过滤而不是逐行无条件回写:
|
|
19
|
+
* - 快照是全量记录(进场景前每一行的原值),场景没碰过的行占绝大多数,逐行回写会让
|
|
20
|
+
* 补丁文件 / 状态文件白写一遍(MCP 那一侧还会连带触发宿主热重载与备份噪音);
|
|
21
|
+
* - 现状已经等于原值的行本来就无需还原。真正要回写的只有「场景期间被改过的行」——
|
|
22
|
+
* 档案改的,或用户自己在场景页面上改的(未锁定时可用,改动同步进档案)。
|
|
23
|
+
* 后者正是用户报的「场景里关掉 A 目录,退出后 A 与它下面的技能都没开回来」:
|
|
24
|
+
* 旧口径只记「进场景时被档案改动过的行」,而 A 在进场景时没被改动(档案里勾着
|
|
25
|
+
* A 下面的技能),快照里根本没有 A 这一行。
|
|
26
|
+
*
|
|
27
|
+
* 快照里记过、但现在已不存在的行直接跳过:这类行在场景期间被删掉了,没有可还原的状态,
|
|
28
|
+
* 硬写还会让退出失败(MCP 启停按 id 定位,`未找到条目` 直接报错)。
|
|
29
|
+
* 读不到现状(列表读取抛错)→ 退回旧行为全量回写:宁可多写几行,不可少还原。
|
|
30
|
+
*/
|
|
31
|
+
async function mcpServerRowsToRestore(rows) {
|
|
32
|
+
if (!rows.length)
|
|
33
|
+
return [];
|
|
34
|
+
let current;
|
|
35
|
+
try {
|
|
36
|
+
current = await deps.mcpServerStates();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return rows;
|
|
40
|
+
}
|
|
41
|
+
const byKey = new Map(current.map((s) => [s.id + '\u0000' + s.level, s.disabled]));
|
|
42
|
+
return rows.filter((x) => {
|
|
43
|
+
const key = x.id + '\u0000' + x.level;
|
|
44
|
+
return byKey.has(key) && byKey.get(key) !== x.disabled;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** 同 mcpServerRowsToRestore:来源级的还原行。 */
|
|
48
|
+
async function skillSourceRowsToRestore(rows) {
|
|
49
|
+
if (!rows.length)
|
|
50
|
+
return [];
|
|
51
|
+
let current;
|
|
52
|
+
try {
|
|
53
|
+
current = await deps.skillSourceStates();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
const byRoot = new Map(current.map((s) => [s.root, s.enabled]));
|
|
59
|
+
return rows.filter((x) => byRoot.has(x.root) && byRoot.get(x.root) !== x.enabled);
|
|
60
|
+
}
|
|
15
61
|
/**
|
|
16
62
|
* 还原到快照(退出模式与失败回滚共用同一条路径):
|
|
17
63
|
* MCP 停用表**按快照原文整体回写**——模式自己写进去的键(未勾服务器的 ['*'])必须随之消失,
|
|
18
64
|
* 否则退出后用户环境仍被静默停用(比"多留一个键"严重得多);模式期间的手动改动按设计 §2.2
|
|
19
65
|
* 不保留(「退出 = 恢复 mode.snapshot」,手动改动只在「保存到场景」时回写)。
|
|
20
66
|
* 技能只写快照列出的键(这是既有批量通道的语义):模式期间新增的技能保持现状。
|
|
67
|
+
* 上层两行(服务器级 / 来源级)按**全量**快照还原 —— 只回写与现状不同的行,见
|
|
68
|
+
* mcpServerRowsToRestore / skillSourceRowsToRestore。
|
|
21
69
|
*/
|
|
22
70
|
async function restoreSnapshot(snapshot) {
|
|
23
71
|
// 顺序与进入时**相反**:先恢复服务器级 / 来源级,再恢复工具级 / 技能级 ——
|
|
24
72
|
// 来源还关着的时候写技能级策略会被吞掉(`skills/core.js` 的 sourceEnabled 判定)。
|
|
25
73
|
// 老 snapshot 没有这两栏 → `?? []`,按旧行为只恢复下层。
|
|
26
|
-
const servers = snapshot.mcpServers ?? [];
|
|
74
|
+
const servers = await mcpServerRowsToRestore(snapshot.mcpServers ?? []);
|
|
27
75
|
if (servers.length) {
|
|
28
76
|
await deps.applyMcpServerSwitches(servers.map((x) => ({ id: x.id, level: x.level, enabled: !x.disabled })));
|
|
29
77
|
}
|
|
30
|
-
const sources = snapshot.skillSources ?? [];
|
|
78
|
+
const sources = await skillSourceRowsToRestore(snapshot.skillSources ?? []);
|
|
31
79
|
if (sources.length) {
|
|
32
80
|
await deps.applySkillSourceSwitches(sources.map((x) => ({ root: x.root, enabled: x.enabled })));
|
|
33
81
|
}
|
|
@@ -291,17 +339,24 @@ export function createArchiveEngine(deps) {
|
|
|
291
339
|
// 备注段是例外:未定义 = 不覆盖任何备注(没有东西"因为未定义而需要关掉")。
|
|
292
340
|
const mcpSpec = (archive && archive.mcp) || {};
|
|
293
341
|
const skillsSpec = (archive && archive.skills) || [];
|
|
294
|
-
//
|
|
295
|
-
//
|
|
342
|
+
// 先读现状、再算计划:现状既喂给计划(算「哪些行需要改」),也**全量**写进快照。
|
|
343
|
+
// 快照记的是**每一行**服务器 / 来源的进场景前状态,不是只记「本次计划要改的行」——
|
|
344
|
+
// 场景期间用户可以在页面上改开关(未锁定时可用,改动同步进档案),只记计划行的话
|
|
345
|
+
// 这些改动就没有原值可回:用户报的「场景里关掉 A 目录,退出后 A 和它下面的技能都没开回来」,
|
|
346
|
+
// 就是 A 在进场景时没被档案改动(档案里勾着 A 下面的技能)而未进快照。
|
|
296
347
|
let mcpPlan = null;
|
|
297
348
|
let skillsPlan = null;
|
|
349
|
+
let mcpServerStates = [];
|
|
350
|
+
let skillSources = [];
|
|
298
351
|
try {
|
|
352
|
+
mcpServerStates = await deps.mcpServerStates();
|
|
353
|
+
skillSources = await deps.skillSourceStates();
|
|
299
354
|
mcpPlan = computeMcpPlan(mcpSpec, {
|
|
300
355
|
configuredServers: await deps.configuredServers(),
|
|
301
356
|
knownTools: await deps.serverKnownTools(),
|
|
302
|
-
serverStates:
|
|
357
|
+
serverStates: mcpServerStates,
|
|
303
358
|
});
|
|
304
|
-
skillsPlan = computeSkillsPlan(skillsSpec, await deps.knownSkillKeys(),
|
|
359
|
+
skillsPlan = computeSkillsPlan(skillsSpec, await deps.knownSkillKeys(), skillSources);
|
|
305
360
|
}
|
|
306
361
|
catch (e) {
|
|
307
362
|
return { ok: false, error: `读取运行时状态失败(未改动任何东西):${msg(e)}` };
|
|
@@ -338,9 +393,9 @@ export function createArchiveEngine(deps) {
|
|
|
338
393
|
}
|
|
339
394
|
// 快照**恒拍**:三个域都按「与勾选集完全一致」应用(未定义 = 全关),所以任何场景
|
|
340
395
|
// 进入都可能改动运行时(哪怕只是停掉几台服务器 / 几个技能),退出都得能精确还原。
|
|
341
|
-
|
|
342
|
-
//
|
|
343
|
-
|
|
396
|
+
// 上层两行传**现状全量**(上面的 mcpServerStates / skillSources 就是进场景前的值),
|
|
397
|
+
// 不是计划里那几行 —— 退出按「现状 ≠ 记录值」回写(见 mcpServerRowsToRestore)。
|
|
398
|
+
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);
|
|
344
399
|
const entered = { ...slice, mode: { scene: target, snapshot }, active: [target] };
|
|
345
400
|
try {
|
|
346
401
|
await deps.saveSlice({ ...slice, mode: entered.mode });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/memories/archive.ts —— 场景档案数据模型与模式切换纯逻辑(无 I/O;I/O 由 archive-engine 注入)。
|
|
2
2
|
// 勾选集语义(设计 §2.1):段内存储「勾选的启用集合」;应用时该域置为与勾选集完全一致;
|
|
3
3
|
// 段未定义 = 该域不碰;段已定义但全不勾 = 合法(全部停用)。清单只对"实时发现"的条目生效。
|
|
4
4
|
// v2:MCP 段为两级——服务器勾选(mcp 键存在)+ 可选工具明细('*' = 整台,string[] = 指定工具)。
|
|
@@ -160,8 +160,9 @@ export function computeSkillsPlan(skills, knownSkillKeys, sourceStates) {
|
|
|
160
160
|
/**
|
|
161
161
|
* 把「这次要改的上层行」并进快照 —— **已记录的行保持原值**(先记的才是进场景前的状态)。
|
|
162
162
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
163
|
+
* 为什么需要:新快照已是**全量**(进入时就记了每一行),这里是兜底 —— 旧快照只记了
|
|
164
|
+
* 当时将要改动的行,而模式进行中用户改档案(改档案 = 立即生效)又可能新改到别的行;
|
|
165
|
+
* 退出必须回到「进场景前」,所以这些新改的行也得有记录。
|
|
165
166
|
* 反之,若某行在进入时就记过,它的 `*Before` 才是进场景前的值 —— 这次的中间态值必须丢弃。
|
|
166
167
|
*/
|
|
167
168
|
export function mergeSnapshotSwitches(snapshot, mcpServers = [], skillSources = []) {
|
|
@@ -189,10 +190,8 @@ export function mergeSnapshotSwitches(snapshot, mcpServers = [], skillSources =
|
|
|
189
190
|
};
|
|
190
191
|
}
|
|
191
192
|
export function snapshotRuntime(mcpRaw, skills,
|
|
192
|
-
/**
|
|
193
|
-
mcpServers = [],
|
|
194
|
-
/** **将被档案改动**的来源,带改动前的 `enabled`。 */
|
|
195
|
-
skillSources = [],
|
|
193
|
+
/** 上层两行:进场景前**每一行**的原值(全量;退出按「现状 ≠ 原值」回写)。 */
|
|
194
|
+
mcpServers = [], skillSources = [],
|
|
196
195
|
/** **将被档案启用**的人设名(改动前停用的子集;退出时按此停回)。 */
|
|
197
196
|
subagents = [],
|
|
198
197
|
/** **将被档案停用**的人设名(改动前启用的子集;退出时按此重新打开)。 */
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// 规则/记忆域的模块级常量与无依赖小工具(2026-09-19 从 memories/service.ts 抽出)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独一个文件:service.ts 里这些常量夹在类型与函数之间,而投影函数(projection.ts)、
|
|
4
|
+
// 索引 IO(index-io.ts)、发现与快照(snapshot.ts)与 service 本体都要用同一份 `global` /
|
|
5
|
+
// 预算上限 / 引导语 / 路径段校验。留在 service.ts 会让它们反过来 import service.ts —— 那是
|
|
6
|
+
// 运行时循环依赖。抽到这个无依赖的一层,几边都 import 它即可。
|
|
7
|
+
//
|
|
8
|
+
// 这里只放**不依赖本域状态**的东西:常量、纯字符串函数、字节计算。
|
|
9
|
+
// ── 预算与默认 ─────────────────────────────────────────────────────────────
|
|
10
|
+
import { isValidSegment } from '../paths.js';
|
|
11
|
+
export const MAX_SOURCE_DEPTH = 64; // 与 core.js 一致
|
|
12
|
+
export const MAX_DIRECTORIES = 2000; // 目录预算
|
|
13
|
+
export const MAX_ENTRIES = 20000; // 条目预算
|
|
14
|
+
export const MAX_GROUP_SEGMENT_LENGTH = 64; // 场景/子目录段名长度上限
|
|
15
|
+
export const MAX_DESCRIPTION_LENGTH = 500; // 派生/显式描述上限(派生超长截断,显式超长拒绝)
|
|
16
|
+
export const MAX_RULE_BYTES = 1 << 18; // 正文上限 256 KiB
|
|
17
|
+
export const DEFAULT_ORDER = 1000; // 默认投影 order(索引无记录时)
|
|
18
|
+
export const DEFAULT_GROUP_ORDER = 1000; // 新场景默认 order
|
|
19
|
+
export const SNAPSHOT_TTL_MS = 1000; // 读路径短 TTL 缓存,吸收 UI 密集轮询
|
|
20
|
+
export const DEFAULT_MAX_BYTES = 65536; // 场景记忆段预算上限(字节)
|
|
21
|
+
// ── 保留场景名 ─────────────────────────────────────────────────────────────
|
|
22
|
+
/** 保留场景名:界面显示「全局」,恒定存在、不可删除,其记忆注入任何对话。 */
|
|
23
|
+
export const GLOBAL_SCENE = 'global';
|
|
24
|
+
/** 保留场景在界面上的显示名(磁盘上仍用 ASCII 目录/文件名)。 */
|
|
25
|
+
export const GLOBAL_SCENE_LABEL = '全局';
|
|
26
|
+
export const SHARED_GROUP = '_shared'; // 保留场景名:公共基线(历史语义,仍可使用)
|
|
27
|
+
// ── 段渲染 ─────────────────────────────────────────────────────────────────
|
|
28
|
+
export const TRUNCATION_MARKER = '<!-- truncated -->';
|
|
29
|
+
// 段尾清单:让模型知道自己漏了什么。去掉伞标题后这里也不再挂「场景记忆」前缀 ——
|
|
30
|
+
// 它紧跟在场景块之后,`参考信息` 与段首引导语同一说法。
|
|
31
|
+
export const DROPPED_HEADING = '## 未注入的参考信息(超出预算)';
|
|
32
|
+
/** 单行正文的最大长度:超过就退回「标题 + 正文块」,避免出现一条几千字符的列表行。 */
|
|
33
|
+
export const INLINE_BODY_MAX = 120;
|
|
34
|
+
/** 附件行里最多列几个文件名;多的只报总数(路径已经给了,缺的名字模型自己列目录即可)。 */
|
|
35
|
+
export const ATTACHMENT_LIST_MAX = 10;
|
|
36
|
+
/**
|
|
37
|
+
* 段首的引导语:让模型知道下面是**用户为本机写的参考信息**,并且**以它为准**
|
|
38
|
+
* —— 涉及本机的事一律照它办,确实无关时才放下。
|
|
39
|
+
*
|
|
40
|
+
* 写法(2026-09-16 用户裁定,基于真实注入结果的三次修正):
|
|
41
|
+
* - **单行、加粗**,不再用括号分两行 —— 括号跨行在真实提示词里读起来像被截断,
|
|
42
|
+
* 而加粗是 Markdown 里最省字符的强调手段(用户要求「加强模型对此的重视程度」)。
|
|
43
|
+
* - **提到段首、整段只出现一次**:原来它挂在每个场景的段头里,多场景时会重复注入。
|
|
44
|
+
* - **不点名任何工具**:模型从工具 schema 就知道 `memory_manager_list` 存在,点名反而
|
|
45
|
+
* 像在提示它去调;用户裁定「没启用的信息就是不想在当前用」,所以工具指引整句删除。
|
|
46
|
+
* 真正防探测的是**完整性声明**(「以下就是全部信息」),那半句必须留。
|
|
47
|
+
* - **完整性声明按截断状态自适应**:真有条目因预算没注入时,段尾会有未注入清单,
|
|
48
|
+
* 此时不能再声称「全部」,否则和清单自相矛盾 —— 也正因为那时确实有东西没给到,
|
|
49
|
+
* 模型去查工具是**合理**的,不该再拦。
|
|
50
|
+
*
|
|
51
|
+
* 用词:不用「常驻」「注入」这类内部行话(模型没有先验);用户裁定用「信息」而不是
|
|
52
|
+
* 「记忆」——「记忆」在系统提示词里指代不明,而这段的实质就是用户写的参考信息。
|
|
53
|
+
*
|
|
54
|
+
* 2026-09-17 重写(用户:「当前模式会不重视这些提示词」)。上一版的三个毛病都在**授权**
|
|
55
|
+
* 上,而不在措辞好不好看上:
|
|
56
|
+
* 1. 「与当前任务相关时直接采用」——**没有给"相关"的判据**。最省力的解读永远是"无关",
|
|
57
|
+
* 因为判成无关不需要任何工作;
|
|
58
|
+
* 2. 通篇没有优先级规则。与本机实际情况冲突时,模型会默默按自己的默认假设走,
|
|
59
|
+
* 而用户完全不知道发生了什么;
|
|
60
|
+
* 3. 「无关时忽略」——「忽略」是这句话里**最后一个动词**,也是记得最牢的那个。它把
|
|
61
|
+
* 一个免打扰出口写成了对内容的态度许可。
|
|
62
|
+
* 现在:给出**判据**(凡涉及本机路径 / 配置 / 工具 / 习惯)、给出**裁决规则**、把出口降级为
|
|
63
|
+
* 「不必提及」(关于**要不要声明**,不是关于**要不要采用**)。出口保留是必要的 —— 去掉它
|
|
64
|
+
* 会让模型对无关条目强行攀附,那是另一种失真。
|
|
65
|
+
*
|
|
66
|
+
* 2026-09-17 第二版:**按条目类型分级授权**(用户采纳的四条里的第 1、4 条)。Claude Code
|
|
67
|
+
* 把两类内容分进两个系统、用**相反**的授权:用户指令(`claudemd.ts:89`)是
|
|
68
|
+
* "These instructions OVERRIDE any default behavior and you MUST follow them exactly as
|
|
69
|
+
* written",而记忆(`memdir/memoryTypes.ts:202`)是
|
|
70
|
+
* "If a recalled memory conflicts with current information, trust what you observe now"
|
|
71
|
+
* —— 书里(ch11:25)说记忆是 "working notes, not gospel"。上一版把两类塞进一句授权,
|
|
72
|
+
* 对**场景说明**(用户写的约定)是对的,对**记忆条目**(可能是几个月前记下的事实)是错的。
|
|
73
|
+
* 好在这两类在渲染时就分处不同位置:场景说明在 `sceneHeader` 的 `**场景说明:…**` 里,
|
|
74
|
+
* 记忆条目在 `memoryBlock` 里 —— 所以一句话就能分级,不必改数据结构。
|
|
75
|
+
*
|
|
76
|
+
* 冲突阶梯(第 4 条)来自 Codex `base_instructions/default.md:22-27` 与 Claude Code 的
|
|
77
|
+
* `caller override > agent definition > parent model > default`:把"谁高于谁"写明,
|
|
78
|
+
* 模型才不会在「用户当场说的 ≠ 本机记录」时悬空。写明它还有一个反直觉的好处 ——
|
|
79
|
+
* 它让授权更可信:这说明本条不是要让记忆压过用户,只是要压过模型的默认假设。
|
|
80
|
+
*/
|
|
81
|
+
export const SCENE_MEMORY_NOTE = '**用户为本机写的参考信息:「场景说明」是用户的约定,一律照办,覆盖你的默认做法;其余条目是记录,可能已过期 —— 与当前实际情况冲突时以你看到的为准,与用户当场说的冲突时以用户为准。无关时不必提及。以下就是全部信息。**';
|
|
82
|
+
/** 有未注入条目时的版本:去掉完整性声明(见上)。 */
|
|
83
|
+
export const SCENE_MEMORY_NOTE_PARTIAL = '**用户为本机写的参考信息:「场景说明」是用户的约定,一律照办,覆盖你的默认做法;其余条目是记录,可能已过期 —— 与当前实际情况冲突时以你看到的为准,与用户当场说的冲突时以用户为准。无关时不必提及。**';
|
|
84
|
+
// ── bundle 与索引 ──────────────────────────────────────────────────────────
|
|
85
|
+
// bundle 附件限制(body 走 HTTP JSON + base64,故比技能上传收紧一档)。
|
|
86
|
+
export const MAX_ATTACH_ENTRY_BYTES = 8 << 20; // 单个附件 8 MiB
|
|
87
|
+
export const MAX_ATTACH_TOTAL_BYTES = 16 << 20; // 单次总大小 16 MiB
|
|
88
|
+
export const MAX_ATTACH_ENTRIES = 32; // 单次最多 32 个
|
|
89
|
+
export const LEGACY_BUNDLE_DOC = 'SKILL.md'; // 旧版 bundle 的正文文件名;仅在发现/附件排除时作只读兼容,新建一律用 bundleDocName()
|
|
90
|
+
/** bundle 的正文文件名 = `<记忆名>.md`(与目录名一致,不再是固定的 SKILL.md)。 */
|
|
91
|
+
export const bundleDocName = (name) => `${name}.md`;
|
|
92
|
+
export const INDEX_VERSION = 1;
|
|
93
|
+
// ── 段名约束提示 ───────────────────────────────────────────────────────────
|
|
94
|
+
// 场景/子目录段名约束(§4 对照表 + §5.3):任意 Unicode,但必须对文件系统安全。
|
|
95
|
+
// 谓词收敛到 `../paths.ts` —— 此前这里与 subagents / imports / skills 各写一套,
|
|
96
|
+
// 口径不一(这里漏了 Windows 保留设备名与控制字符,而那两样恰好是创建即失败/路径被截断)。
|
|
97
|
+
/**
|
|
98
|
+
* 段名非法时的统一说明。与 `paths.ts` 的谓词保持同步 —— 此前六处报错文案各写一份,
|
|
99
|
+
* 谓词收紧后它们会集体变成过时描述(用户按提示改了还是被拒)。
|
|
100
|
+
*/
|
|
101
|
+
export const SEGMENT_RULE_HINT = `非空、≤${MAX_GROUP_SEGMENT_LENGTH} 字符、不含路径分隔符与 < > : " | ? *、不以 . 开头、首尾无空白、不是 Windows 保留设备名(CON/NUL 等)、不含控制字符`;
|
|
102
|
+
// ── 小工具 ─────────────────────────────────────────────────────────────────
|
|
103
|
+
/** UTF-8 字节长度(预算按字节算,不能按 UTF-16 码元)。 */
|
|
104
|
+
export const byteLen = (s) => Buffer.byteLength(s, 'utf8');
|
|
105
|
+
// ── 与索引层共用的路径常量与无依赖小工具(2026-09-19 从 memories/service.ts 下沉)──
|
|
106
|
+
// 放在这里而非 service.ts:index-io / snapshot 都要用,留在 service.ts 会让它们反向引值,
|
|
107
|
+
// 形成运行时循环依赖。
|
|
108
|
+
/** 单个路径段(场景名或子目录名)是否合法。放宽后 `办公` / `日常` 均通过。 */
|
|
109
|
+
export function isValidGroupSegment(segment) {
|
|
110
|
+
return isValidSegment(segment, MAX_GROUP_SEGMENT_LENGTH);
|
|
111
|
+
}
|
|
112
|
+
/** group/场景路径可多层(a/b/c),每段必须合法;_shared 作为保留场景名放行。 */
|
|
113
|
+
export function isValidGroupPath(group) {
|
|
114
|
+
if (typeof group !== 'string' || group === '' || group.startsWith('/') || group.endsWith('/'))
|
|
115
|
+
return false;
|
|
116
|
+
return group.split('/').every(isValidGroupSegment);
|
|
117
|
+
}
|
|
118
|
+
export const message = (e) => String((e && e.message) || e);
|
|
119
|
+
/** 业务校验失败的统一返回形态(与 skills core 一致)。ops 分域后各域都要用,故放在这一层。 */
|
|
120
|
+
export const fail = (code, error, params) => (params ? { ok: false, error, code, params } : { ok: false, error, code });
|
|
121
|
+
/**
|
|
122
|
+
* 记忆索引文件名 / 记忆回收站目录名(hub 根下)。
|
|
123
|
+
* 域叫「记忆」(工具 `memory_manager_*`、界面「记忆」页),所以按域命名 ——
|
|
124
|
+
* 旧名 `rules-index.json` / `rules-trash/` 由 hub 的启动迁移搬过来(见 hub.ts)。
|
|
125
|
+
*/
|
|
126
|
+
export const MEMORIES_INDEX_FILE = 'memories-index.json';
|
|
127
|
+
/** 记忆回收站目录名(hub 根下)。ops 分域后 trash 域要用,故与索引文件名一起放在这一层。 */
|
|
128
|
+
export const MEMORIES_TRASH_DIR = 'memories-trash';
|