dsh-plugin-tool-management 0.2.0 → 0.5.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/README.md +236 -226
- package/README_EN.md +290 -231
- package/cordis.patch.yml +11 -0
- package/docs/Changelog.md +471 -0
- package/docs/images/MCP.png +0 -0
- package/docs/images//344/274/232/350/257/235.png +0 -0
- package/docs/images//345/234/272/346/231/257.png +0 -0
- package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images//346/212/200/350/203/275.png +0 -0
- package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images//350/256/260/345/277/206.png +0 -0
- package/lib/approval-policy.js +46 -0
- package/lib/client.js +3489 -2173
- package/lib/hub.js +94 -0
- package/lib/imports/upload.js +286 -0
- package/lib/index.js +512 -155
- package/lib/rules/archive-engine.js +191 -0
- package/lib/rules/archive.js +140 -0
- package/lib/rules/service.js +609 -100
- package/lib/skills/core.js +356 -22
- package/lib/skills/service.js +31 -5
- package/lib/subagents/service.js +333 -0
- package/lib/subagents/tools.js +62 -0
- package/package.json +12 -3
- package/screenshots.json +7 -5
- package/docs/images/agents-md.png +0 -0
- package/docs/images/history.png +0 -0
- package/docs/images/mcp.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images//345/234/272/346/231/257/350/256/260/345/277/206.png +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/rules/archive-engine.ts —— 场景档案引擎:sidecar 读写 + 运行时应用,全部经 deps 注入(无直接 I/O)。
|
|
2
|
+
// 状态机(设计 §2.2):进入 = 快照 → 先落盘 mode(留可退路径)→ 应用已定义段 → 记忆收窄为 {S};
|
|
3
|
+
// 退出 = 恢复快照 → 落盘自由模式;切换 = 先退后进。
|
|
4
|
+
// 失败语义(fail-closed):任一步失败即反向恢复运行时并写回旧切片;回滚不全会如实写进错误文本。
|
|
5
|
+
// v2:MCP 段两级(服务器勾选 + '*' / 工具明细),停用表存勾选集的补集,通配由 index.ts 的 guard/restrict 原生支持。
|
|
6
|
+
import { computeMcpPlan, computeMemoriesPlan, computeSkillsPlan, normalizeArchive, snapshotRuntime, hasSection, } from './archive.js';
|
|
7
|
+
const msg = (e) => String((e && e.message) || e);
|
|
8
|
+
export function createArchiveEngine(deps) {
|
|
9
|
+
let chain = Promise.resolve();
|
|
10
|
+
const serial = (task) => {
|
|
11
|
+
const queued = chain.then(task, task);
|
|
12
|
+
chain = queued.catch(() => undefined);
|
|
13
|
+
return queued;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* 还原到快照(退出模式与失败回滚共用同一条路径):
|
|
17
|
+
* MCP 停用表**按快照原文整体回写**——模式自己写进去的键(未勾服务器的 ['*'])必须随之消失,
|
|
18
|
+
* 否则退出后用户环境仍被静默停用(比"多留一个键"严重得多);模式期间的手动改动按设计 §2.2
|
|
19
|
+
* 不保留(「退出 = 恢复 mode.snapshot」,手动改动只在「保存到场景」时回写)。
|
|
20
|
+
* 技能只写快照列出的键(这是既有批量通道的语义):模式期间新增的技能保持现状。
|
|
21
|
+
*/
|
|
22
|
+
async function restoreSnapshot(snapshot) {
|
|
23
|
+
const mcp = Object.fromEntries(Object.entries(snapshot.mcp).map(([k, v]) => [k, v.slice()]));
|
|
24
|
+
await deps.applyMcpEntries(mcp);
|
|
25
|
+
await deps.applySkills({ ...snapshot.skills });
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 失败回滚:运行时还原 + 切片写回;每步失败都记下来,绝不谎报「已回滚」。
|
|
29
|
+
* 返回结构化错误(ok:false),错误文本如实区分「已回滚」与「回滚未完成」。
|
|
30
|
+
*/
|
|
31
|
+
async function rollback(prev, snapshot, stage, err) {
|
|
32
|
+
const problems = [];
|
|
33
|
+
try {
|
|
34
|
+
await restoreSnapshot(snapshot);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
problems.push('运行时还原: ' + msg(e));
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await deps.saveSlice(prev);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
problems.push('模式状态写回: ' + msg(e));
|
|
44
|
+
}
|
|
45
|
+
const base = `${stage}失败:${msg(err)}`;
|
|
46
|
+
return { ok: false, error: problems.length ? `${base};回滚未完成(${problems.join(';')})` : `${base},已回滚` };
|
|
47
|
+
}
|
|
48
|
+
const ops = {
|
|
49
|
+
// 读:页面渲染模式与档案
|
|
50
|
+
'scene-mode-get': async () => {
|
|
51
|
+
const slice = await deps.loadSlice();
|
|
52
|
+
return { ok: true, archives: slice.archives, mode: slice.mode, active: slice.active };
|
|
53
|
+
},
|
|
54
|
+
// 写:保存某场景档案(三段整体替换;键对实时发现全集校验,未知键丢弃并报告)
|
|
55
|
+
'scene-archive-save': (args) => serial(async () => {
|
|
56
|
+
const scene = String((args && args.scene) || '').trim();
|
|
57
|
+
if (!scene)
|
|
58
|
+
return { ok: false, error: '缺少场景名' };
|
|
59
|
+
if (!(await deps.sceneExists(scene)))
|
|
60
|
+
return { ok: false, error: `场景不存在: ${scene}` };
|
|
61
|
+
const archive = normalizeArchive((args && args.archive) || {});
|
|
62
|
+
const stale = [];
|
|
63
|
+
if (archive.mcp) {
|
|
64
|
+
const configured = await deps.configuredServers();
|
|
65
|
+
const hadKeys = Object.keys(archive.mcp).length > 0;
|
|
66
|
+
for (const server of Object.keys(archive.mcp)) {
|
|
67
|
+
if (configured.indexOf(server) < 0) {
|
|
68
|
+
stale.push('mcp/' + server);
|
|
69
|
+
delete archive.mcp[server];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 只剔除「全 stale」段:空段是合法值(全不勾 = 全部停用),必须留下来。
|
|
73
|
+
if (hadKeys && Object.keys(archive.mcp).length === 0)
|
|
74
|
+
delete archive.mcp;
|
|
75
|
+
}
|
|
76
|
+
if (archive.skills) {
|
|
77
|
+
const known = await deps.knownSkillKeys();
|
|
78
|
+
const hadKeys = archive.skills.length > 0;
|
|
79
|
+
stale.push(...archive.skills.filter((k) => !known.has(k)).map((k) => 'skills/' + k));
|
|
80
|
+
archive.skills = archive.skills.filter((k) => known.has(k));
|
|
81
|
+
if (hadKeys && archive.skills.length === 0)
|
|
82
|
+
delete archive.skills;
|
|
83
|
+
}
|
|
84
|
+
if (archive.subagents) {
|
|
85
|
+
const known = await deps.knownPersonas();
|
|
86
|
+
const hadKeys = archive.subagents.length > 0;
|
|
87
|
+
stale.push(...archive.subagents.filter((p) => !known.has(p)).map((p) => 'subagents/' + p));
|
|
88
|
+
archive.subagents = archive.subagents.filter((p) => known.has(p));
|
|
89
|
+
if (hadKeys && archive.subagents.length === 0)
|
|
90
|
+
delete archive.subagents;
|
|
91
|
+
}
|
|
92
|
+
if (archive.memories) {
|
|
93
|
+
// 记忆段只影响投影(正文是否进 system prompt),不写任何运行时状态;
|
|
94
|
+
// 这里只把已不存在的记忆 id 剔掉并上报,避免档案里留下指向空气的勾选。
|
|
95
|
+
const known = await deps.knownMemoryIds();
|
|
96
|
+
const hadKeys = archive.memories.length > 0;
|
|
97
|
+
const { stale: memStale } = computeMemoriesPlan(archive.memories, known);
|
|
98
|
+
stale.push(...memStale);
|
|
99
|
+
archive.memories = archive.memories.filter((id) => known.has(id));
|
|
100
|
+
if (hadKeys && archive.memories.length === 0)
|
|
101
|
+
delete archive.memories;
|
|
102
|
+
}
|
|
103
|
+
const slice = await deps.loadSlice();
|
|
104
|
+
if (Object.keys(archive).length === 0)
|
|
105
|
+
delete slice.archives[scene];
|
|
106
|
+
else
|
|
107
|
+
slice.archives[scene] = archive;
|
|
108
|
+
await deps.saveSlice(slice);
|
|
109
|
+
return { ok: true, scene, archive: slice.archives[scene] ?? null, stale };
|
|
110
|
+
}),
|
|
111
|
+
// 写:进入/退出/切换当前模式
|
|
112
|
+
// scene=null 退出;目标场景没有 mcp/skills 段 = 结构化拒绝(设计 §2.4:此类场景不显示模式按钮)。
|
|
113
|
+
'scene-mode-set': (args) => serial(async () => {
|
|
114
|
+
const hasKey = args && 'scene' in (args || {});
|
|
115
|
+
if (!hasKey)
|
|
116
|
+
return { ok: false, error: '缺少 scene(null = 退出模式)' };
|
|
117
|
+
const raw = (args && args.scene) ?? null;
|
|
118
|
+
const target = raw == null ? null : String(raw).trim() || null;
|
|
119
|
+
let slice = await deps.loadSlice();
|
|
120
|
+
let current = slice.mode ?? { scene: null, snapshot: null };
|
|
121
|
+
if (target === current.scene)
|
|
122
|
+
return { ok: true, mode: current, applied: null, stale: [] };
|
|
123
|
+
// ① 退出当前模式(切换 = 先退后进):先恢复运行时,再落盘自由模式。
|
|
124
|
+
// 恢复失败即中止且**保持原 mode 不变**——宁可留在「可再退出一次」的状态,也不吞错。
|
|
125
|
+
if (current.scene && current.snapshot) {
|
|
126
|
+
try {
|
|
127
|
+
await restoreSnapshot(current.snapshot);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return { ok: false, error: `退出模式「${current.scene}」失败,已保持原模式(运行时可能部分残留):${msg(e)}` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (current.scene) {
|
|
134
|
+
const exited = { ...slice, mode: { scene: null, snapshot: null } };
|
|
135
|
+
try {
|
|
136
|
+
await deps.saveSlice(exited);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
return { ok: false, error: `退出模式「${current.scene}」已恢复运行时,但模式状态落盘失败:${msg(e)}` };
|
|
140
|
+
}
|
|
141
|
+
slice = exited;
|
|
142
|
+
current = exited.mode;
|
|
143
|
+
}
|
|
144
|
+
if (!target)
|
|
145
|
+
return { ok: true, mode: current, applied: null, stale: [] };
|
|
146
|
+
// ② 进入目标模式:校验 → 快照 → 先落盘 mode(中途失败也留可退快照)→ 应用已定义段 → 记忆收窄。
|
|
147
|
+
const archive = slice.archives[target];
|
|
148
|
+
if (!(await deps.sceneExists(target)))
|
|
149
|
+
return { ok: false, error: `场景不存在: ${target}` };
|
|
150
|
+
if (!archive || (!hasSection(archive, 'mcp') && !hasSection(archive, 'skills'))) {
|
|
151
|
+
return { ok: false, error: `场景「${target}」的档案没有 MCP / 技能段(仅记忆或仅子智能体的场景不需要进入模式)` };
|
|
152
|
+
}
|
|
153
|
+
const snapshot = snapshotRuntime(await deps.currentMcpRaw(), await deps.currentSkills());
|
|
154
|
+
const entered = { ...slice, mode: { scene: target, snapshot }, active: [target] };
|
|
155
|
+
try {
|
|
156
|
+
await deps.saveSlice({ ...slice, mode: entered.mode });
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
return { ok: false, error: '进入模式前落盘快照失败(运行时未改动):' + msg(e) };
|
|
160
|
+
}
|
|
161
|
+
const stale = [];
|
|
162
|
+
try {
|
|
163
|
+
if (archive.mcp) {
|
|
164
|
+
const plan = computeMcpPlan(archive.mcp, {
|
|
165
|
+
configuredServers: await deps.configuredServers(),
|
|
166
|
+
knownTools: await deps.serverKnownTools(),
|
|
167
|
+
});
|
|
168
|
+
stale.push(...plan.stale);
|
|
169
|
+
await deps.applyMcpEntries(plan.entries);
|
|
170
|
+
}
|
|
171
|
+
if (archive.skills) {
|
|
172
|
+
const plan = computeSkillsPlan(archive.skills, await deps.knownSkillKeys());
|
|
173
|
+
stale.push(...plan.stale);
|
|
174
|
+
await deps.applySkills(plan.target);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
return await rollback(slice, snapshot, '应用档案', e);
|
|
179
|
+
}
|
|
180
|
+
// ③ 记忆启用集收窄为 {S}(`_shared` 由 resolveActiveScenes 恒常加入;退出不恢复)。
|
|
181
|
+
try {
|
|
182
|
+
await deps.saveSlice(entered);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
return await rollback(slice, snapshot, '记忆收窄落盘', e);
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, mode: entered.mode, applied: { mcp: !!archive.mcp, skills: !!archive.skills }, stale, narrowedTo: [target] };
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
return { ops, writeOps: new Set(['scene-archive-save', 'scene-mode-set']) };
|
|
191
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/rules/archive.ts —— 场景档案数据模型与模式切换纯逻辑(无 I/O;I/O 由 archive-engine 注入)。
|
|
2
|
+
// 勾选集语义(设计 §2.1):段内存储「勾选的启用集合」;应用时该域置为与勾选集完全一致;
|
|
3
|
+
// 段未定义 = 该域不碰;段已定义但全不勾 = 合法(全部停用)。清单只对"实时发现"的条目生效。
|
|
4
|
+
// v2:MCP 段为两级——服务器勾选(mcp 键存在)+ 可选工具明细('*' = 整台,string[] = 指定工具)。
|
|
5
|
+
export function normalizeStringList(raw) {
|
|
6
|
+
if (typeof raw === 'string')
|
|
7
|
+
raw = raw.split(/[,,]/);
|
|
8
|
+
if (!Array.isArray(raw))
|
|
9
|
+
return [];
|
|
10
|
+
return [...new Set(raw.map((x) => String(x).trim()).filter(Boolean))];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* MCP 规范化:serverName → '*' 或工具名清单;非法形态丢弃。
|
|
14
|
+
* 空清单是**合法值**(勾了服务器但一个工具都不勾 = 该服务器全部停用),必须保留——
|
|
15
|
+
* 否则「段已定义但全不勾 = 全部停用」(设计 §2.1)无法持久化。
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeMcpSpec(raw) {
|
|
18
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
19
|
+
return undefined;
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [serverRaw, v] of Object.entries(raw)) {
|
|
22
|
+
const server = serverRaw.trim();
|
|
23
|
+
if (!server)
|
|
24
|
+
continue;
|
|
25
|
+
if (v === '*' || (typeof v === 'string' && v.trim() === '*')) {
|
|
26
|
+
out[server] = '*';
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (typeof v !== 'string' && !Array.isArray(v))
|
|
30
|
+
continue; // 畸形形态(null/数字/对象)丢弃
|
|
31
|
+
out[server] = normalizeStringList(v);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** 'mcp'/'skills'/'subagents'/'memories' 键存在且值非 null 才视为"段已定义"——存在性独立于集合空否(null/缺失 = 未定义)。 */
|
|
36
|
+
export function normalizeArchive(raw) {
|
|
37
|
+
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
38
|
+
const out = {};
|
|
39
|
+
if (obj.mcp != null) {
|
|
40
|
+
const mcp = normalizeMcpSpec(obj.mcp);
|
|
41
|
+
if (mcp)
|
|
42
|
+
out.mcp = mcp;
|
|
43
|
+
}
|
|
44
|
+
if (obj.skills != null)
|
|
45
|
+
out.skills = normalizeStringList(obj.skills);
|
|
46
|
+
if (obj.subagents != null)
|
|
47
|
+
out.subagents = normalizeStringList(obj.subagents);
|
|
48
|
+
if (obj.memories != null)
|
|
49
|
+
out.memories = normalizeStringList(obj.memories);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function hasSection(archive, section) {
|
|
53
|
+
return archive[section] !== undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* MCP 段应用计划:段已定义 → 对并集里每台服务器产出**精确停用名单(勾选集的补集)**。
|
|
57
|
+
* 服务器未勾(键缺失)→ ['*'] 整台停用(guard/restrict 原生支持通配,服务器后加载也会被拦);
|
|
58
|
+
* 服务器勾 '*' → [] 全启用;
|
|
59
|
+
* 服务器勾清单 → known − 勾选(未运行且无已知工具名的服务器无法枚举补集:不做停用,
|
|
60
|
+
* 上报 `mcp/<server>/*` 让用户知道未勾项这次没生效);
|
|
61
|
+
* 段内服务器若配置里根本不存在 → stale 上报且不写。
|
|
62
|
+
*/
|
|
63
|
+
export function computeMcpPlan(mcp, input) {
|
|
64
|
+
const configured = new Set(input.configuredServers);
|
|
65
|
+
const servers = new Set([...configured, ...Object.keys(input.knownTools), ...Object.keys(mcp)]);
|
|
66
|
+
const entries = {};
|
|
67
|
+
const wildcards = [];
|
|
68
|
+
const stale = [];
|
|
69
|
+
for (const server of servers) {
|
|
70
|
+
if (!configured.has(server)) {
|
|
71
|
+
if (mcp[server] !== undefined)
|
|
72
|
+
stale.push('mcp/' + server);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const spec = mcp[server];
|
|
76
|
+
if (spec === undefined) {
|
|
77
|
+
entries[server] = ['*'];
|
|
78
|
+
wildcards.push(server);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (spec === '*') {
|
|
82
|
+
entries[server] = [];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const known = input.knownTools[server] || [];
|
|
86
|
+
if (!known.length) {
|
|
87
|
+
entries[server] = [];
|
|
88
|
+
stale.push('mcp/' + server + '/*');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const checked = new Set(spec);
|
|
92
|
+
entries[server] = known.filter((t) => !checked.has(t));
|
|
93
|
+
for (const t of spec)
|
|
94
|
+
if (known.indexOf(t) < 0)
|
|
95
|
+
stale.push('mcp/' + server + '/' + t);
|
|
96
|
+
}
|
|
97
|
+
return { entries, wildcards, stale };
|
|
98
|
+
}
|
|
99
|
+
/** 技能段应用计划:已知技能全集 → 勾选集决定启停。 */
|
|
100
|
+
export function computeSkillsPlan(skills, knownSkillKeys) {
|
|
101
|
+
const target = {};
|
|
102
|
+
for (const key of knownSkillKeys)
|
|
103
|
+
target[key] = skills.indexOf(key) >= 0;
|
|
104
|
+
const stale = skills.filter((k) => !knownSkillKeys.has(k)).map((k) => 'skills/' + k);
|
|
105
|
+
return { target, stale };
|
|
106
|
+
}
|
|
107
|
+
export function snapshotRuntime(mcpRaw, skills) {
|
|
108
|
+
return {
|
|
109
|
+
mcp: Object.fromEntries(Object.entries(mcpRaw).map(([k, v]) => [k, v.slice()])),
|
|
110
|
+
skills: { ...skills },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ── 记忆勾选(v3)──────────────────────────────────────────────────────────
|
|
114
|
+
//
|
|
115
|
+
// 记忆段与其余三段有一处关键差别:MCP / 技能是**运行时启停**(改了要落表、进/退模式要回滚),
|
|
116
|
+
// 记忆只是**投影**(正文进 system prompt)。所以记忆段不产生"应用计划"、不进模式快照,
|
|
117
|
+
// 而是在渲染时被查询——勾选后下一个请求即生效,退出模式无需回滚任何东西。
|
|
118
|
+
/**
|
|
119
|
+
* 记忆是否应当注入(纯函数,供渲染路径同步调用)。
|
|
120
|
+
*
|
|
121
|
+
* 口径与其余段同构:
|
|
122
|
+
* - 该记忆所属场景**没有** memories 段 → 全部记忆照常注入(老档案 = 未定义 = 不碰);
|
|
123
|
+
* - 有段 → 只有列在段里的记忆注入(段已定义但空 = 该场景记忆全部不注入)。
|
|
124
|
+
*
|
|
125
|
+
* 入参 `sceneEnabled` 是索引里的单条启停(`enabled === false` 的条目在上游已被过滤,
|
|
126
|
+
* 这里只为把两种"不注入"的原因分开,便于体检报告区分)。
|
|
127
|
+
*/
|
|
128
|
+
export function memoryAllowed(archives, scene, id) {
|
|
129
|
+
const archive = archives ? archives[scene] : undefined;
|
|
130
|
+
if (!archive || archive.memories === undefined)
|
|
131
|
+
return true;
|
|
132
|
+
return archive.memories.indexOf(id) >= 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 记忆段的 stale 上报(档案保存时调用):段里引用了已经不存在的记忆 id。
|
|
136
|
+
* 记忆段不写任何运行时状态,因此除 stale 外没有别的计划产物。
|
|
137
|
+
*/
|
|
138
|
+
export function computeMemoriesPlan(memories, knownMemoryIds) {
|
|
139
|
+
return { stale: memories.filter((id) => !knownMemoryIds.has(id)).map((id) => 'memories/' + id) };
|
|
140
|
+
}
|