dsh-plugin-tool-management 0.19.0 → 0.20.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 +868 -798
- package/README.md +71 -59
- package/README_EN.md +91 -80
- 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/images/logo.png +0 -0
- package/docs/images/promo-cover-EN.png +0 -0
- package/docs/images/promo-cover.png +0 -0
- package/docs/update.md +31 -520
- package/lib/client.js +8308 -6307
- package/lib/hub.js +96 -6
- package/lib/index.js +54 -2
- package/lib/mcp/manager.js +24 -13
- package/lib/mcp/secret-guard.js +2 -2
- package/lib/memories/constants.js +9 -2
- package/lib/memories/service.js +6 -1
- package/lib/op-registry.js +22 -7
- package/lib/ops/prompts.js +64 -7
- package/lib/ops/sessions.js +62 -7
- package/lib/ops/trash-retention.js +55 -0
- package/lib/ops/trash.js +62 -3
- package/lib/prompts/service.js +191 -25
- package/lib/scene-prompt-sync.js +1 -1
- package/lib/sessions/history.js +32 -22
- package/lib/skills/core.js +68 -2
- package/lib/skills/manager-state.js +32 -1
- package/lib/skills/service.js +7 -1
- package/lib/tools/mcp.js +6 -0
- package/lib/tools/prompt.js +14 -2
- package/lib/tools/scene.js +146 -25
- package/lib/trash-retention.js +256 -0
- package/package.json +1 -1
- package/lib/mcp/inspect-probe.js +0 -199
|
@@ -269,6 +269,8 @@ export function defaultManagerState() {
|
|
|
269
269
|
// 也不参与 provider 候选)。与「停用来源」不同——停用仍列出技能、只是不可调用。
|
|
270
270
|
// 源文件一个字节都不动;清空这个数组即可恢复。
|
|
271
271
|
removedSources: [],
|
|
272
|
+
// 用户从回收站「永久删除」的来源:连"已移除"这条记录都不再出现(见 core-types 的说明)。
|
|
273
|
+
forgottenSources: [],
|
|
272
274
|
};
|
|
273
275
|
}
|
|
274
276
|
export function validStateSkillName(name) {
|
|
@@ -324,6 +326,12 @@ export function validManagerStateDocument(value) {
|
|
|
324
326
|
value.removedSources.some((key) => typeof key !== "string" || key.length === 0 || key.length > 128))
|
|
325
327
|
return false;
|
|
326
328
|
}
|
|
329
|
+
// 永久删除的来源:同上(可选字段、同样的 key 形状)。
|
|
330
|
+
if (value.forgottenSources !== undefined) {
|
|
331
|
+
if (!Array.isArray(value.forgottenSources) ||
|
|
332
|
+
value.forgottenSources.some((key) => typeof key !== "string" || key.length === 0 || key.length > 128))
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
327
335
|
// 同名首选表:可选字段;只校验容器与值类型,键值逐条在 normalize 里裁剪(见 validPreferredSkillName)。
|
|
328
336
|
if (value.preferredSkills !== undefined) {
|
|
329
337
|
if (!value.preferredSkills ||
|
|
@@ -426,6 +434,23 @@ export function normalizeManagerState(value) {
|
|
|
426
434
|
!isDefaultSkillSource(key))),
|
|
427
435
|
].sort();
|
|
428
436
|
}
|
|
437
|
+
// 永久删除的来源:同一套口径(已知 key、非默认来源),另外**从 removedSources 里摘掉**
|
|
438
|
+
// —— 一条来源要么"已移除"、要么"已永久删除",同时挂在两份名单上只会让两处显示打架。
|
|
439
|
+
{
|
|
440
|
+
const raw = Array.isArray(value.forgottenSources) ? value.forgottenSources : [];
|
|
441
|
+
const known = new Set([
|
|
442
|
+
...userRoots().map((root) => root.key),
|
|
443
|
+
...customKeys,
|
|
444
|
+
]);
|
|
445
|
+
const forgotten = [
|
|
446
|
+
...new Set(raw.filter((key) => typeof key === "string" &&
|
|
447
|
+
known.has(key) &&
|
|
448
|
+
!isDefaultSkillSource(key))),
|
|
449
|
+
].sort();
|
|
450
|
+
normalized.forgottenSources = forgotten;
|
|
451
|
+
if (forgotten.length)
|
|
452
|
+
normalized.removedSources = normalized.removedSources.filter((key) => !forgotten.includes(key));
|
|
453
|
+
}
|
|
429
454
|
return normalized;
|
|
430
455
|
}
|
|
431
456
|
export function normalizeCustomRoots(value) {
|
|
@@ -618,11 +643,17 @@ export async function state(options = {}) {
|
|
|
618
643
|
const removedKeys = new Set(Array.isArray(policyResult.state.removedSources)
|
|
619
644
|
? policyResult.state.removedSources
|
|
620
645
|
: []);
|
|
646
|
+
// 「永久删除」过的来源(回收站 → 目录 里那条记录的终点):连行都不留 —— 界面不该再有
|
|
647
|
+
// 它的位置,「恢复读取」也无从谈起(用户 2026-10-12 要求 codex / Claude 也能永久删除)。
|
|
648
|
+
// 注意它同时也在 `activeUserRoots` 里被摘掉,所以下面这层过滤只影响**列表展示**。
|
|
649
|
+
const forgottenKeys = new Set(Array.isArray(policyResult.state.forgottenSources)
|
|
650
|
+
? policyResult.state.forgottenSources
|
|
651
|
+
: []);
|
|
621
652
|
// 已移除的来源仍然出现在 roots 里(界面要能显示「已移除」并允许恢复),但**不扫描**:
|
|
622
653
|
// 技能不入列表、不参与重名决胜 —— 这正是用户要的「不读取这个文件夹」。
|
|
623
654
|
// 注意这里必须用**全部**来源 + 自定义来源,不能只用 activeUserRoots(),
|
|
624
655
|
// 否则被移除的来源会连行都不剩,界面就无从提供「恢复读取」。
|
|
625
|
-
const user = [...userRoots(), ...customRootsFromState(policyResult.state)];
|
|
656
|
+
const user = [...userRoots(), ...customRootsFromState(policyResult.state)].filter((root) => !forgottenKeys.has(root.key));
|
|
626
657
|
const scannable = user.filter((root) => !removedKeys.has(root.key));
|
|
627
658
|
const userScans = await scanDeduplicatedRoots(scannable);
|
|
628
659
|
const projectWarnings = [];
|
package/lib/skills/service.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 沿用上游机制:全局层接管 + 每个活动 agent scope 内二次注册。
|
|
6
6
|
import { appendFile, rename, rm, stat } from 'node:fs/promises';
|
|
7
7
|
import { isInsideRootResolved } from '../paths.js';
|
|
8
|
-
import { state, setSkillEnabled, setPreferredSkill, markSkillsDisabled, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, updateSkill, skillDetail, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, } from './core.js';
|
|
8
|
+
import { state, setSkillEnabled, setPreferredSkill, markSkillsDisabled, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, updateSkill, skillDetail, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, forgetSource, logPath, } from './core.js';
|
|
9
9
|
import { watchDirectories } from './dir-watch.js';
|
|
10
10
|
import { registerAgentSkillProviders, externalSkillProvider } from './provider-registration.js';
|
|
11
11
|
import { permanentlyDeleteTrashSafely } from './system-trash.js';
|
|
@@ -233,6 +233,8 @@ export function createSkillsService(ctx) {
|
|
|
233
233
|
// 批量启停与单条启停同权:配了访问令牌的宿主必须同样要求带令牌。
|
|
234
234
|
'skill-set-all',
|
|
235
235
|
'skill-trash-restore', 'skill-trash-delete', 'skill-custom-add', 'skill-custom-remove',
|
|
236
|
+
// 「永久删除来源记录」(回收站 → 目录):与上面两条同权,是写操作。
|
|
237
|
+
'skill-source-forget',
|
|
236
238
|
]);
|
|
237
239
|
/**
|
|
238
240
|
* 新建 / 导入 / 恢复技能后统一「默认停用」。**失败不能吞。**
|
|
@@ -385,6 +387,10 @@ export function createSkillsService(ctx) {
|
|
|
385
387
|
// 自定义技能目录:添加(绝对路径,只读接入)/ 移除(连同策略键)。
|
|
386
388
|
'skill-custom-add': wrap((args) => write(() => addCustomRoot(args && args.path, args && args.label, log)), afterWrite),
|
|
387
389
|
'skill-custom-remove': wrap((args) => write(() => removeCustomRoot(args && args.key, log)), afterWrite),
|
|
390
|
+
// 永久删除一条**按约定发现**的来源记录(agents / codex / claude)。与 `skill-custom-remove`
|
|
391
|
+
// 的分工见 core.ts 的 `forgetSource`:那条管用户手加的自定义目录,这条管推导出来的来源
|
|
392
|
+
// —— 后者不在状态文件的 customRoots 里,必须另记一份"已永久删除"名单才删得掉。
|
|
393
|
+
'skill-source-forget': wrap((args) => write(() => forgetSource(args && args.root, log)), afterWrite),
|
|
388
394
|
};
|
|
389
395
|
/**
|
|
390
396
|
* 该绝对路径是否落在「本插件认识的技能来源」内。
|
package/lib/tools/mcp.js
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
// 所以 switch 里只有 on|off 过 `lockedSceneGuard()`,restart 不过;与面板侧
|
|
14
14
|
// `guardLockedOps(frozenOps('all'))` 的口径逐字一致(两处口径不同才是真的"说假话")。
|
|
15
15
|
//
|
|
16
|
+
// restart 现在是**唯一**的重启入口:面板侧那颗重启键用户此前已删除(2026-10-12 复核,
|
|
17
|
+
// 客户端已无任何 restart 调用方)。这不是要把它也删掉 —— 工具是模型按需调用的面,与面板
|
|
18
|
+
// 按钮是两回事;面板删掉只说明"不想让人手点",而"服务器卡住、让模型重连一下"这条恢复路径
|
|
19
|
+
// 仍然成立,且它正是上面那条"锁定期间唯一不冻结的写"的立足点。删掉它等于把场景锁定时的
|
|
20
|
+
// 唯一恢复手段一并拿掉。
|
|
21
|
+
//
|
|
16
22
|
// **改一台已配置服务器时绝不读明文凭据**:`mcpm-edit` 的守卫(`resolveMaskedKv` /
|
|
17
23
|
// `resolveMaskedUrl`)就是为"接受打码值并还原"写的 —— 界面编辑框预填的本来就是打码值。
|
|
18
24
|
// 所以这里拿 `mcpmListView()` 的**打码视图**把模型没给的字段填回去,让 op 去还原。
|
package/lib/tools/prompt.js
CHANGED
|
@@ -58,15 +58,27 @@ export function buildPromptTools(deps) {
|
|
|
58
58
|
}));
|
|
59
59
|
register(defineTool({
|
|
60
60
|
name: 'prompt_manager_apply',
|
|
61
|
-
description: 'Apply one AGENTS.md preset by id; effective next turn. If a scene drives the baseline this does NOT refuse — it rebinds that scene to the preset and re-syncs it, so tell the user which scene was rebound. A locked scene blocks the call. Only on the user\'s instruction.',
|
|
61
|
+
description: 'Apply one AGENTS.md preset by id; effective next turn. If a scene drives the baseline this does NOT refuse — it rebinds that scene to the preset and re-syncs it, so tell the user which scene was rebound. A locked scene blocks the call. Pass apply=false to cancel instead — AGENTS.md goes back to what it was before the run of applies (the file is removed if there was none to begin with); a scene driving the baseline refuses that, rebind the scene instead. Only on the user\'s instruction.',
|
|
62
62
|
parameters: {
|
|
63
|
-
id: { type: 'string', required: true, description: 'Preset id (lowercase letters, digits, hyphens).' },
|
|
63
|
+
id: { type: 'string', required: true, description: 'Preset id (lowercase letters, digits, hyphens). Ignored when apply=false.' },
|
|
64
|
+
apply: { type: 'boolean', description: 'Omit or true = apply the preset in `id`; false = cancel — undo the run of applies.' },
|
|
64
65
|
},
|
|
65
66
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
66
67
|
async execute(args) {
|
|
67
68
|
const blocked = await deps.lockedSceneGuard();
|
|
68
69
|
if (blocked)
|
|
69
70
|
throw new Error(blocked);
|
|
71
|
+
if (args.apply === false) {
|
|
72
|
+
// 取消方向:`agentsmd-unapply` 自带两条护栏(场景驱动时拒绝、没有链起点时给确定
|
|
73
|
+
// 错误码),原样透传。回执要说清恢复成哪种 —— 起点没有文件时是**删文件**,
|
|
74
|
+
// 不说的话用户以为 AGENTS.md 还在(0.19.1 界面弹窗报的就是这两种)。
|
|
75
|
+
const r = await deps.unapplyPreset();
|
|
76
|
+
if (!r.ok)
|
|
77
|
+
throw new Error(r.error || '取消应用失败');
|
|
78
|
+
return r.removed === true
|
|
79
|
+
? 'OK: application cancelled — ~/.dsh/AGENTS.md did not exist before the run started, so the file was removed (effective next turn).'
|
|
80
|
+
: 'OK: application cancelled — ~/.dsh/AGENTS.md is back to what it was before the run started (effective next turn).';
|
|
81
|
+
}
|
|
70
82
|
const r = await deps.applyPresetGuarded(args.id);
|
|
71
83
|
if (!r.ok)
|
|
72
84
|
throw new Error(r.error);
|
package/lib/tools/scene.js
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
// 不因为多了一条工具就破例(工具描述本来就是写"什么时候该用它"的地方)。
|
|
27
27
|
import { text, toolNameList } from './deps.js';
|
|
28
28
|
import { FACTORY_TOOL_TABLE_PRESET } from './table.js';
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/** 档案里由模型设置的段(`memories` 段在 P5 已废弃,不再是档案的一部分)。
|
|
30
|
+
* `mcpNotes`(0.20.0)与其余三段同族:整段替换、段缺席 = 不碰这一域。 */
|
|
31
|
+
const ARCHIVE_SECTIONS = ['mcp', 'mcpNotes', 'skills', 'subagents'];
|
|
31
32
|
/** 段里的名单:数组段是名字列表,`mcp` 段是 serverName → `'*'` | 工具名数组。段缺席 = 空。 */
|
|
32
33
|
function segmentNames(value) {
|
|
33
34
|
if (Array.isArray(value))
|
|
@@ -36,6 +37,21 @@ function segmentNames(value) {
|
|
|
36
37
|
return Object.keys(value);
|
|
37
38
|
return [];
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* `quickPrompts` 入参 → 档案值:去空、去重、保序(与 op 侧 `quickPromptsArg` 同一口径)。
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ 空数组是**合法值**(= 进入后全部停用),不能在这里被当成"没给"滤掉 —— 它与
|
|
44
|
+
* "这一域不碰"(段缺席)是两句相反的话(见 `SceneArchive.quickPrompts` 的注释)。
|
|
45
|
+
*/
|
|
46
|
+
function quickPromptIds(value) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const item of Array.isArray(value) ? value : []) {
|
|
49
|
+
const id = String(item == null ? '' : item).trim();
|
|
50
|
+
if (id && out.indexOf(id) < 0)
|
|
51
|
+
out.push(id);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
39
55
|
/**
|
|
40
56
|
* 档案里绑的工具表方案 → 给人读的那一句。`factory-default` 是个哨兵值而不是本机存过的方案,
|
|
41
57
|
* 原样打出来模型会去找一份叫这个名字的方案(`_save` 那边也没有这一栏,它改不了)。
|
|
@@ -69,6 +85,11 @@ function archiveText(archive) {
|
|
|
69
85
|
const tt = toolTableLabel(archive.toolTablePreset);
|
|
70
86
|
if (tt)
|
|
71
87
|
parts.push('工具表方案「' + tt + '」');
|
|
88
|
+
// 快捷提示词绑定(0.20.0):进了这个场景会动到 `/` 菜单里哪些条目,不报就只能靠猜。
|
|
89
|
+
// 空表是"进入后全部停用",与"没绑"(这一域不碰)是两句相反的话,不能压成一个 "0 条"。
|
|
90
|
+
const qp = archive.quickPrompts;
|
|
91
|
+
if (Array.isArray(qp))
|
|
92
|
+
parts.push(qp.length ? '快捷提示词 ' + qp.length + ' 条' : '快捷提示词:进入后全部停用');
|
|
72
93
|
return parts.join(' | ');
|
|
73
94
|
}
|
|
74
95
|
/**
|
|
@@ -124,7 +145,7 @@ export function buildSceneTools(deps) {
|
|
|
124
145
|
// 默认的隐藏名单里):静态文本没法按 toolVisible 改写,点名一条模型没有的工具只会让它
|
|
125
146
|
// 白跑一趟。要说"去哪看"就用中性说法(the scene listing / the「本机当前的场景」reminder
|
|
126
147
|
// —— 后者是常驻源,只要场景域开着就在)。点名只留在**运行时回执**里、按可见性分叉。
|
|
127
|
-
description: 'List every scene with its archive (what entering it switches on: MCP servers, skills, personas, and the bound model tool-table scheme when there is one), bound prompt preset, memory count and enabled / entered / locked state. Read the archive here before saving a scene: a section you give later replaces it whole. Read-only; the scene in effect now is also the「本机当前的场景」reminder.',
|
|
148
|
+
description: 'List every scene with its archive (what entering it switches on: MCP servers, skills, personas, quick prompts, and the bound model tool-table scheme when there is one), bound prompt preset, memory count and enabled / entered / locked state. Read the archive here before saving a scene: a section you give later replaces it whole. Read-only; the scene in effect now is also the「本机当前的场景」reminder.',
|
|
128
149
|
parameters: {},
|
|
129
150
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
130
151
|
async execute() {
|
|
@@ -183,7 +204,7 @@ export function buildSceneTools(deps) {
|
|
|
183
204
|
// 为什么 `action` 是必填枚举而不是一个布尔:与 `mcp_manager_switch` 同一条理由 ——
|
|
184
205
|
// 让模型在 enter / exit 两个动词里选一个,比让它猜"scene 传 null 是什么意思"省一次往返。
|
|
185
206
|
// 参数里刻意不出现 `null`:那是 op(`scene-mode-set`)的形态,模型照着填只会填错。
|
|
186
|
-
description: 'Enter a scene, or leave the one currently entered. Entering switches on exactly what its archive lists and everything else off — MCP servers, skills, personas, and the model tool table when the scene binds a scheme — narrows injection to its memories, and applies its bound prompt preset to AGENTS.md. Leaving restores the runtime snapshot taken on entry. Check the scene\'s archive in the scene listing before entering. Only when the user asks or approves.',
|
|
207
|
+
description: 'Enter a scene, or leave the one currently entered. Entering switches on exactly what its archive lists and everything else off — MCP servers, skills, personas, quick prompts, and the model tool table when the scene binds a scheme — narrows injection to its memories, and applies its bound prompt preset to AGENTS.md. Leaving restores the runtime snapshot taken on entry. Check the scene\'s archive in the scene listing before entering. Only when the user asks or approves.',
|
|
187
208
|
parameters: {
|
|
188
209
|
action: { type: 'string', required: true, enum: ['enter', 'exit'], description: 'enter = switch to the scene named in `scene`; exit = leave the current one (no scene needed).' },
|
|
189
210
|
scene: { type: 'string', description: 'Scene name (one path segment), required for enter. Use the exact name, not the display label — the scene listing and the「本机当前的场景」reminder both print names.' },
|
|
@@ -271,12 +292,27 @@ export function buildSceneTools(deps) {
|
|
|
271
292
|
// 「写入指引」保留("写成指令而不是备注"—— 这是场景说明质量的杠杆),但不再声称
|
|
272
293
|
// "注入后模型会被要求照办":2026-09-23 定稿把场景段的授权语删了(注入只陈述现状),
|
|
273
294
|
// 参数说明与注入侧各说各话时,模型信的那份是错的。改为如实说明去向。
|
|
274
|
-
description: 'Save a scene — a named bundle the user enters: entering it switches on exactly what its archive lists and everything else off. Creates it if absent, else updates it. An archive section you give replaces that section whole — include what should stay; the receipt reports what got displaced. Saving does not enter it; entering is a separate action. Only on the user\'s instruction.',
|
|
295
|
+
description: 'Save a scene — a named bundle the user enters: entering it switches on exactly what its archive lists (MCP servers, skills, personas, quick prompts, and the model tool table when a scheme is bound) and everything else off. Creates it if absent, else updates it. An archive section you give replaces that section whole — include what should stay; the receipt reports what got displaced. Saving does not enter it; entering is a separate action. Only on the user\'s instruction.',
|
|
275
296
|
parameters: {
|
|
276
297
|
scene: { type: 'string', required: true, description: 'Scene name (one path segment, no slashes). Created if absent.' },
|
|
277
298
|
label: { type: 'string', description: 'Display name shown in the UI. Omit = keep.' },
|
|
278
299
|
description: { type: 'string', description: 'What this scene is for; shown beside the scene name in the「本机当前的场景」reminder while the scene is on — write an instruction, not a note. Omit = keep.' },
|
|
279
|
-
|
|
300
|
+
// 与下面 `mcp` / `skills` / `subagents` 同一句式("Omit = keep; X = 一个都不"):
|
|
301
|
+
// 界面 2026-10-10 补了「无」这个选项,工具侧的空串语义要跟着说清,否则模型只知道
|
|
302
|
+
// 「省略 = 保持」,不知道还有一条「解绑」的路。
|
|
303
|
+
prompt: { type: 'string', description: 'Prompt preset id to bind; while this scene is on it is the global baseline. Omit = keep; \'\' = bind none (this scene leaves AGENTS.md alone).' },
|
|
304
|
+
// 模型工具表方案绑定(0.20.0 补上模型侧入口:0.15.0 起界面可绑、档案里一直有这一栏,
|
|
305
|
+
// `_list` / `_switch` 也读它,唯独 `_save` 写不了)。'' = 解绑只能走
|
|
306
|
+
// `rules-update-scene` —— `scene-archive-save` 对载荷里**不带的栏**一律保留现值
|
|
307
|
+
// (archive-engine 的「不带 ≠ 解绑」),所以解绑与设置分成两条路,见 execute 的 ②。
|
|
308
|
+
// 哨兵 `factory-default` 与界面下拉同值(tools/table.ts 的 FACTORY_TOOL_TABLE_PRESET);
|
|
309
|
+
// 不存在的方案名由引擎按 stale 丢弃并在进入时报告,op 不硬拒(与悬空绑定同口径)。
|
|
310
|
+
toolTablePreset: { type: 'string', description: "Model tool-table scheme to bind while this scene is on. Omit = keep; '' = unbind (the scene leaves the tool table alone); 'factory-default' = the factory default; otherwise the name of a scheme saved in the panel. An unknown name is dropped and reported." },
|
|
311
|
+
// 快捷提示词绑定(0.20.0)。段语义与上面三个档案段相反:**空数组是合法值**
|
|
312
|
+
// (进入后全部停用),不是"清除这一段"。本工具不提供解绑(界面可解)—— 与
|
|
313
|
+
// mcp / skills / subagents 三段没有解绑参数同一条口径。id 从场景清单里别的场景的
|
|
314
|
+
// 绑定、或用户口述里来;不存在的 id 被引擎丢弃并在回执报出来。
|
|
315
|
+
quickPrompts: { type: 'array', items: { type: 'string' }, description: 'Quick-prompt ids to switch on when this scene is entered; every other one is switched off, and the pre-entry state is restored on exit. Omit = keep; [] = switch all off on entry. Unknown ids are dropped and reported — copy them from a scene binding in the listing, or ask the user.' },
|
|
280
316
|
mcp: {
|
|
281
317
|
type: 'object',
|
|
282
318
|
// 官方要求对象类型**显式声明开放性**(`additionalProperties` 是必需字段,且只能是
|
|
@@ -285,6 +321,14 @@ export function buildSceneTools(deps) {
|
|
|
285
321
|
additionalProperties: true,
|
|
286
322
|
description: "Archive: serverName -> '*' (whole server) or a list of tool names to leave on. Omit = keep; {} = switch none on.",
|
|
287
323
|
},
|
|
324
|
+
// 场景级 MCP 备注覆盖(0.20.0):进入这个场景时用这份盖掉全局备注,退出时还原。
|
|
325
|
+
// 与 mcp 段同一句式(省略 = 保持;{} = 一个都不覆盖)—— 界面存档编辑器早就能改,
|
|
326
|
+
// `_list` 也显示它,`_save` 此前不接这一段。
|
|
327
|
+
mcpNotes: {
|
|
328
|
+
type: 'object',
|
|
329
|
+
additionalProperties: true,
|
|
330
|
+
description: "Archive: serverName -> note that overrides that server's global note while this scene is on (the global note comes back on exit). Omit = keep; {} = drop every scene-level override.",
|
|
331
|
+
},
|
|
288
332
|
skills: { type: 'array', items: { type: 'string' }, description: 'Archive: skill keys `<root>/<name>` to switch on. Omit = keep; [] = switch none on.' },
|
|
289
333
|
subagents: { type: 'array', items: { type: 'string' }, description: 'Archive: persona names to switch on. Omit = keep; [] = switch none on.' },
|
|
290
334
|
},
|
|
@@ -323,22 +367,72 @@ export function buildSceneTools(deps) {
|
|
|
323
367
|
const created = await deps.rulesOps['rules-create-scene'](payload);
|
|
324
368
|
if (!created || created.ok === false)
|
|
325
369
|
throw new Error((created && created.error) || '保存场景失败');
|
|
326
|
-
// ②
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
370
|
+
// ② 两处「解绑」只能走 `rules-update-scene`:`scene-archive-save` 对载荷里**不带的栏**
|
|
371
|
+
// 一律保留现值(archive-engine 的「不带 ≠ 解绑」),从现值复制出来的合并档案会把
|
|
372
|
+
// 刚解掉的绑定又写回去 —— 所以解绑与设置分成两条路,合并前还要先摘掉刚解掉的键。
|
|
373
|
+
// · `prompt` 的 '' 此前也走不到实处 —— `rules-create-scene` 的 upsert 分支把空串
|
|
374
|
+
// 当"没给"(ops/scene-records.ts),旧绑定原样保留而回执报"已更新"。
|
|
375
|
+
// · `toolTablePreset` 的 '' 同解(那个 op 的更新分支认空串 = 解绑)。
|
|
376
|
+
const clearPrompt = args.prompt !== undefined && String(args.prompt).trim() === '';
|
|
377
|
+
const unbindToolTable = args.toolTablePreset !== undefined && String(args.toolTablePreset).trim() === '';
|
|
378
|
+
const setToolTable = args.toolTablePreset !== undefined && String(args.toolTablePreset).trim() !== '';
|
|
379
|
+
const setQuickPrompts = args.quickPrompts !== undefined;
|
|
380
|
+
if (setQuickPrompts && !Array.isArray(args.quickPrompts)) {
|
|
381
|
+
throw new Error('quickPrompts 必须是 id 数组([] = 进入后全部停用;要解绑请到「场景」页)');
|
|
382
|
+
}
|
|
332
383
|
const touched = ARCHIVE_SECTIONS.filter((k) => args[k] !== undefined);
|
|
333
|
-
|
|
334
|
-
|
|
384
|
+
const mustWriteArchive = touched.length > 0 || setToolTable || setQuickPrompts;
|
|
385
|
+
// 档案现状在读的时候就取一次,且必须在 ② **之前** —— 解绑落盘后这一栏已经没了,
|
|
386
|
+
// 回执的「X → 解绑」要的是解绑前的那个 X。只为看回执的 diff 而读时(解绑且不写档案)
|
|
387
|
+
// 容忍读失败:那一步已经写成功了,别为一句对比话把整次调用判失败。
|
|
388
|
+
let before = {};
|
|
389
|
+
let beforeKnown = false;
|
|
390
|
+
if (mustWriteArchive || unbindToolTable) {
|
|
335
391
|
const cur = await deps.archiveOps['scene-mode-get']({});
|
|
336
|
-
if (!cur || cur.ok === false)
|
|
337
|
-
|
|
338
|
-
|
|
392
|
+
if (!cur || cur.ok === false) {
|
|
393
|
+
if (mustWriteArchive)
|
|
394
|
+
throw new Error((cur && cur.error) || '读取场景档案失败');
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
before = (cur.archives || {})[scene] || {};
|
|
398
|
+
beforeKnown = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
let clearWarn = '';
|
|
402
|
+
if (clearPrompt || unbindToolTable) {
|
|
403
|
+
const upd = { name: scene };
|
|
404
|
+
if (clearPrompt)
|
|
405
|
+
upd.prompt = '';
|
|
406
|
+
if (unbindToolTable)
|
|
407
|
+
upd.toolTablePreset = '';
|
|
408
|
+
const u = await deps.rulesOps['rules-update-scene'](upd);
|
|
409
|
+
if (!u || u.ok === false) {
|
|
410
|
+
// 主路径已成功、解绑没写上:如实报,别让回执假装绑定已经没了。
|
|
411
|
+
clearWarn = '\nWARN: 解绑未写入(' + ((u && u.error) || '未知错误') + '),旧绑定仍然有效';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// ③ 档案 read-modify-write:段(mcp / mcpNotes / skills / subagents)与两个绑定栏的
|
|
415
|
+
// 「设置」方向合并成一份,只经 `scene-archive-save` 一条路落盘。
|
|
416
|
+
// **必须 read-modify-write**:`scene-archive-save` 是**整份替换**,只把模型给的段
|
|
417
|
+
// 传过去,没给的段会被静默清掉 —— 与 0.14.0 里 `subagent_manager_save` 省略字段被
|
|
418
|
+
// 清空同一形状,只是这次清掉的是"这个场景进入时要开哪些东西"。
|
|
419
|
+
// 注意段语义:**段不存在 = 不限制(全部照常)**,段存在但为空 = 全部停用。所以模型给
|
|
420
|
+
// `mcp: {}` 得到的是"全不勾",不是"清除该段" —— 后者只能靠不给。
|
|
421
|
+
// `quickPrompts` 与三段都不同:段缺席 = 这一域不碰,空数组 = 进入后全部停用。
|
|
422
|
+
let archiveNote = '';
|
|
423
|
+
let toolTableNote = '';
|
|
424
|
+
let quickNote = '';
|
|
425
|
+
let warnTail = '';
|
|
426
|
+
if (mustWriteArchive) {
|
|
339
427
|
const archive = { ...before };
|
|
340
428
|
for (const k of touched)
|
|
341
429
|
archive[k] = args[k];
|
|
430
|
+
if (unbindToolTable)
|
|
431
|
+
delete archive.toolTablePreset;
|
|
432
|
+
if (setToolTable)
|
|
433
|
+
archive.toolTablePreset = String(args.toolTablePreset).trim();
|
|
434
|
+
if (setQuickPrompts)
|
|
435
|
+
archive.quickPrompts = quickPromptIds(args.quickPrompts);
|
|
342
436
|
const r = await deps.archiveOps['scene-archive-save']({ scene, archive });
|
|
343
437
|
if (!r || r.ok === false)
|
|
344
438
|
throw new Error((r && r.error) || '保存场景档案失败');
|
|
@@ -346,18 +440,41 @@ export function buildSceneTools(deps) {
|
|
|
346
440
|
// 静默清掉,所以"模型给什么就是什么"),但"给出去的段盖掉了原来的勾选"是**静默**的
|
|
347
441
|
// —— 不写出来,模型只会看到一句"档案段:mcp",以为只是加了一项。
|
|
348
442
|
const diffs = touched.map((k) => segmentDiff(k, before[k], args[k]));
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
443
|
+
if (diffs.length) {
|
|
444
|
+
archiveNote = '档案段:' + diffs.map((d) => d.text).join(';');
|
|
445
|
+
if (diffs.some((d) => d.dropped.length > 0)) {
|
|
446
|
+
warnTail += '\n注意:这些段是整段替换,上面标 - 的项已被挤掉;要保留就把它们一并写进这次调用。';
|
|
447
|
+
}
|
|
352
448
|
}
|
|
353
|
-
// 当前环境里不存在的键会被 op
|
|
354
|
-
//
|
|
449
|
+
// 当前环境里不存在的键会被 op 丢弃并报告(改名或删除留下的,含工具表方案名与
|
|
450
|
+
// 快捷提示词 id):如实说,否则模型以为勾上了而实际没有。
|
|
355
451
|
if (r.stale && r.stale.length)
|
|
356
|
-
|
|
452
|
+
warnTail += '\n(丢弃了当前环境不存在的键:' + r.stale.join('、') + ')';
|
|
357
453
|
// 存盘成功但应用到运行时失败:`scene-archive-save` 对"正是当前模式的那个场景"会就地
|
|
358
454
|
// 重应用,失败不回滚档案。如实报出去,别假装生效了(与界面同口径)。
|
|
359
455
|
if (r.applyError)
|
|
360
|
-
|
|
456
|
+
warnTail += '\n(已存盘,但应用到运行时失败:' + r.applyError + ')';
|
|
457
|
+
}
|
|
458
|
+
// 解绑写失败了就别输出「X → 解绑」—— 上面那句 WARN 才是事实;设置方向不受 ② 影响。
|
|
459
|
+
if (args.toolTablePreset !== undefined && !(unbindToolTable && clearWarn !== '')) {
|
|
460
|
+
toolTableNote = beforeKnown
|
|
461
|
+
? '工具表方案:' + (toolTableLabel(before.toolTablePreset) || '未绑') + ' → ' +
|
|
462
|
+
(unbindToolTable ? '解绑' : toolTableLabel(String(args.toolTablePreset).trim()))
|
|
463
|
+
: '工具表方案:已解绑(档案现状读不到,无法对比)';
|
|
464
|
+
}
|
|
465
|
+
if (setQuickPrompts && beforeKnown) {
|
|
466
|
+
const beforeIds = Array.isArray(before.quickPrompts) ? before.quickPrompts.map((x) => String(x)) : [];
|
|
467
|
+
const afterIds = quickPromptIds(args.quickPrompts);
|
|
468
|
+
const added = afterIds.filter((id) => beforeIds.indexOf(id) < 0);
|
|
469
|
+
const dropped = beforeIds.filter((id) => afterIds.indexOf(id) < 0);
|
|
470
|
+
const marks = [];
|
|
471
|
+
if (added.length)
|
|
472
|
+
marks.push('+' + toolNameList(added));
|
|
473
|
+
if (dropped.length)
|
|
474
|
+
marks.push('-' + toolNameList(dropped));
|
|
475
|
+
quickNote = '快捷提示词:' + beforeIds.length + ' 条 → ' + afterIds.length + ' 条' +
|
|
476
|
+
(afterIds.length === 0 ? '(进入后全部停用)' : '') +
|
|
477
|
+
(marks.length ? '(' + marks.join(' ') + ')' : '');
|
|
361
478
|
}
|
|
362
479
|
// 收敛提示:`rules-create-scene` 在"历史默认(全部启用)"状态下会把启用集合收窄成
|
|
363
480
|
// 单选(`collapseActiveForNewScene`)—— 那是一次**会改变运行时**的动作,必须说出来。
|
|
@@ -366,13 +483,17 @@ export function buildSceneTools(deps) {
|
|
|
366
483
|
const parts = [touchedRecord.length ? '场景记录已更新(' + touchedRecord.join('、') + ')' : '场景已确保存在'];
|
|
367
484
|
if (archiveNote)
|
|
368
485
|
parts.push(archiveNote);
|
|
486
|
+
if (toolTableNote)
|
|
487
|
+
parts.push(toolTableNote);
|
|
488
|
+
if (quickNote)
|
|
489
|
+
parts.push(quickNote);
|
|
369
490
|
// 点名 scene_manager_switch 的那句按可见性分叉(它在出厂默认名单里就是关着的):
|
|
370
491
|
// 部分启用(开 save 不开 switch)时指向一条模型没有的工具,只会让它白跑一趟 ——
|
|
371
492
|
// 与 mcp/skill 的 listHint 同一条纪律。
|
|
372
493
|
const enterHint = deps.toolVisible('scene_manager_switch')
|
|
373
494
|
? '要让它生效,用 scene_manager_switch 进入。'
|
|
374
495
|
: '要让它生效,由用户在「场景」页进入。';
|
|
375
|
-
return 'OK: scene「' + scene + '」' + parts.join(';') +
|
|
496
|
+
return 'OK: scene「' + scene + '」' + parts.join(';') + warnTail + clearWarn +
|
|
376
497
|
(collapsed
|
|
377
498
|
? '\n注意:此前所有场景都处于启用状态(历史「全部启用」默认),本次已收窄为只启用一个,新建的这个不在其中 —— 让用户到「场景」页确认。'
|
|
378
499
|
: '') +
|