dsh-plugin-tool-management 0.13.0 → 0.14.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 +45 -0
- package/README.md +91 -123
- package/README_EN.md +99 -126
- 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 +28 -0
- package/lib/client.js +578 -124
- package/lib/compat/preset-reach.js +6 -3
- package/lib/context-inject.js +86 -32
- package/lib/index.js +251 -92
- package/lib/mcp/manager.js +3 -14
- package/lib/mcp/secret-guard.js +21 -0
- package/lib/mcp/state-section.js +13 -11
- package/lib/memories/constants.js +72 -8
- package/lib/memories/index-io.js +1 -1
- package/lib/memories/projection.js +67 -31
- package/lib/memories/service.js +17 -2
- package/lib/memories/snapshot.js +113 -22
- package/lib/op-registry.js +8 -2
- package/lib/ops/candidates.js +11 -0
- package/lib/ops/compat.js +10 -2
- package/lib/ops/sessions.js +1 -1
- package/lib/prompts/service.js +25 -2
- package/lib/scenes/candidates.js +56 -0
- package/lib/skills/core.js +93 -0
- package/lib/skills/service.js +34 -2
- package/lib/subagents/service.js +37 -4
- package/lib/subagents/tools.js +11 -4
- package/lib/tools/deps.js +15 -0
- package/lib/tools/mcp.js +291 -50
- package/lib/tools/memory.js +131 -80
- package/lib/tools/prompt.js +27 -13
- package/lib/tools/scene.js +351 -0
- package/lib/tools/skills.js +93 -17
- package/lib/tools/subagent.js +56 -45
- package/lib/tools/table.js +204 -15
- package/package.json +108 -108
package/lib/tools/memory.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
|
-
// 记忆(rules)域的 model 工具(2026-09-19 从 index.ts
|
|
2
|
-
// memory_manager_list / _read /
|
|
1
|
+
// 记忆(rules)域的 model 工具(2026-09-19 从 index.ts 的注册区抽出;2026-09-23 改名合并、
|
|
2
|
+
// 同日把场景能力分出去):memory_manager_list / _read / _switch / _save。
|
|
3
|
+
//
|
|
4
|
+
// 为什么族名在 0.14.0 里绕了一圈回到 `memory_manager_`:中途改成 `scene_memory_manager_`
|
|
5
|
+
// 是为了跟注入段名(`scene-memory-manager-catalog`)对齐,但那个名字描述的是**域**,
|
|
6
|
+
// 而这个族操作的全是记忆。加场景能力时这个错配就现形了 —— 场景是另一个对象(容器,
|
|
7
|
+
// 进入时切换 mcp / 技能 / 人设),硬塞进 `save` 会让 `description` 变成双义参数(给
|
|
8
|
+
// `memory` 时是记忆描述、给 `scene` 时是场景描述),门禁也得按参数分叉(记忆写入有开关
|
|
9
|
+
// 可放行,场景写入是改运行时环境、不该有那条路)。所以拆成两族:这里管记忆内容,
|
|
10
|
+
// `scene_manager_*`(tools/scene.ts)管场景本身。
|
|
11
|
+
//
|
|
12
|
+
// 场景清单也不挂在这条 list 的尾巴上(同日第四轮裁定:「记忆就管记忆,场景就管场景」)——
|
|
13
|
+
// 它在 `scene_manager_list`,理由记在 tools/scene.ts 头部,别当"少写了一段"再加回来。
|
|
14
|
+
//
|
|
15
|
+
// 为什么 write + update 并成 save:两者的判定(同名 = 改、无同名 = 建)本来就只有
|
|
16
|
+
// `rules-create` 知道 —— 它对同名是**拒绝**的,op 层不会替你 upsert。分开两条工具时,
|
|
17
|
+
// 模型必须自己先查一次再决定调哪条,而它手里的清单可能已经过期。合并后由工具侧兜住。
|
|
3
18
|
//
|
|
4
19
|
// 活动场景的记忆正文会自动注入上下文(无需调用工具读取);这里的工具用于查询/编辑规则
|
|
5
|
-
// 本身。
|
|
20
|
+
// 本身。memory_manager_save 受 tools/pre-execute 审批门禁(D2)。
|
|
6
21
|
// 路径锚点:$DSH_HOME/tool-management/memories/<场景>/…(场景 `global` = 界面「全局」)。
|
|
7
22
|
import { text } from './deps.js';
|
|
8
23
|
export function buildMemoryTools(deps) {
|
|
9
24
|
const { defineTool, register } = deps;
|
|
10
25
|
register(defineTool({
|
|
11
26
|
name: 'memory_manager_list',
|
|
12
|
-
|
|
27
|
+
// 末尾**不点名** `scene_manager_list`:场景一族在出厂默认里就是关着的,指向一条模型没有的
|
|
28
|
+
// 工具只会让它去猜名字(用户 2026-09-23 把默认名单扩到十五条时去掉的这句)。
|
|
29
|
+
description: 'List memories (id, scene, enabled, description) plus the scene injection currently follows. What is injected each turn is in the「本机当前的记忆」reminder; use this for ids and for entries that are off. all=true lists everything.',
|
|
13
30
|
parameters: {
|
|
14
31
|
group: { type: 'string', description: 'Optional scene filter.' },
|
|
15
32
|
all: { type: 'boolean', description: 'Include memories that are off, in an inactive scene, or shadowed (default false).' },
|
|
@@ -38,43 +55,56 @@ export function buildMemoryTools(deps) {
|
|
|
38
55
|
const shown = showAll ? rows : rows.filter((row) => row.injects);
|
|
39
56
|
const lines = shown.map(({ x, label }) => ('- ' + x.id + ' [' + (x.group || '未归属场景') + '] ' + label +
|
|
40
57
|
(x.description ? ' — ' + x.description : '')));
|
|
41
|
-
|
|
58
|
+
// 注入按哪个场景走 —— 上面那句「场景未启用」只有配上它才可解读:少了这一行,
|
|
59
|
+
// 模型看到一片"未启用"会以为是条目自己的问题。(场景本身的清单在 `scene_manager_list`,
|
|
60
|
+
// 2026-09-23 拆过去了;这里只留"记忆按哪一集注入",因为那是记忆域自己的现状。)
|
|
61
|
+
const scope = r.activeMode === 'all'
|
|
62
|
+
? '全部场景(历史默认,未收窄)'
|
|
63
|
+
: (r.activeScene ? '「' + String(r.activeScene) + '」' : '未启用任何场景');
|
|
42
64
|
const header = '记忆:' + (showAll
|
|
43
65
|
? rows.length + ' 条'
|
|
44
66
|
: shown.length + ' 条会注入 / 共 ' + rows.length + ' 条' +
|
|
45
|
-
(shown.length === rows.length ? '' : '(传 all=true 看全部)')) +
|
|
67
|
+
(shown.length === rows.length ? '' : '(传 all=true 看全部)')) +
|
|
68
|
+
'\n注入范围:' + scope + ' + global(global 恒常注入)\n';
|
|
46
69
|
// 注入边界:压制型预设(persona complete / 关闭运行时上下文)下本插件默认不注入,
|
|
47
70
|
// 此时列出的记忆**不在**模型上下文里。必须说出来,否则模型会假设自己已经看到正文。
|
|
48
71
|
const notice = await deps.reachNoticeForAgent(deps.presetRoster(), exec && exec.agent && exec.agent.ctx, deps.injectNoticeOptions());
|
|
49
|
-
return header + (lines.join('\n') || '(无记忆)') +
|
|
50
|
-
'\n场景:' + (scenes || '(无)') + (r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)') + notice;
|
|
72
|
+
return header + (lines.join('\n') || '(无记忆)') + notice;
|
|
51
73
|
},
|
|
52
74
|
}));
|
|
53
75
|
register(defineTool({
|
|
54
76
|
name: 'memory_manager_read',
|
|
55
77
|
description: 'Read the full body of one memory. Only for ones not already in your context (disabled, scene not active, or dropped by the byte budget).',
|
|
56
78
|
parameters: {
|
|
57
|
-
|
|
79
|
+
memory: { type: 'string', required: true, description: 'Memory id like <scene>/<name>.' },
|
|
58
80
|
},
|
|
59
81
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
60
82
|
async execute(args) {
|
|
61
|
-
|
|
83
|
+
// op 的字段名是 `id`,不改 op:这里显式转换(键名对模型是 `<场景>/<名字>`,对 op 是 `id`)。
|
|
84
|
+
const r = await deps.rulesOps['rules-read']({ id: String((args && args.memory) || '') });
|
|
62
85
|
if (!r || r.ok === false)
|
|
63
86
|
throw new Error((r && r.error) || '读取规则失败');
|
|
64
87
|
return '# ' + r.rule.id + '\n\n' + (r.rule.body || '');
|
|
65
88
|
},
|
|
66
89
|
}));
|
|
67
90
|
register(defineTool({
|
|
68
|
-
name: '
|
|
91
|
+
name: 'memory_manager_switch',
|
|
92
|
+
// 为什么叫 `_switch` 而不是 `_set_enabled`(2026-09-23 用户裁定):六个域里"拨一个开关"
|
|
93
|
+
// 是同一件事,名字该同形 —— `mcp_manager_switch` 早就是这个名字,`_set_enabled` 只剩
|
|
94
|
+
// 记忆 / 技能 / 人设三族在叫。**参数保留 `enabled` 布尔**:这里没有 `restart` 那第三个
|
|
95
|
+
// 动词,`action: 'on' | 'off'` 只会在 `true`/`false` 之上多一层映射、多付一段 schema。
|
|
96
|
+
// 旧名不注册别名(同 0.14.0 的口径:注册就进工具表、就要付 token);用户的旧设置由
|
|
97
|
+
// `tools/table.ts` 的 `LEGACY_TOOL_NAME_MAP` 翻译。
|
|
98
|
+
//
|
|
69
99
|
// 为什么单独一个工具:注入实况里「这个域注入了几次 / 模型调了几次」都看得到,但模型此前
|
|
70
100
|
// 看得到一份记忆却开关不了它 —— 只能回一句"请你去界面上点"。启停是它替用户调整环境时
|
|
71
101
|
// 最常碰的一格,而 `rules-toggle` 这个 op 早就带齐了门禁(写令牌 + 场景冻结)。
|
|
72
102
|
// 三个写侧工具共用一句 consent(`Only when the user asks or approves.` / `Only on the
|
|
73
103
|
// user's instruction.`):这些动的都是"以后每一轮都会带上"的东西,模型不该顺手做。
|
|
74
|
-
// 描述同时按"省 token"重写了一遍 ——
|
|
104
|
+
// 描述同时按"省 token"重写了一遍 —— 这几段文字每次请求都要发出去,废话是常驻成本。
|
|
75
105
|
description: 'Enable or disable one memory (index switch; the .md file is untouched). Disabled memories stop being injected, freeing the byte budget. Prefer this over deleting — it is reversible. Only when the user asks or approves.',
|
|
76
106
|
parameters: {
|
|
77
|
-
|
|
107
|
+
memory: { type: 'string', required: true, description: 'Memory id like <scene>/<name>.' },
|
|
78
108
|
enabled: { type: 'boolean', required: true, description: 'true = enable, false = disable; omission is refused.' },
|
|
79
109
|
},
|
|
80
110
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
@@ -82,12 +112,17 @@ export function buildMemoryTools(deps) {
|
|
|
82
112
|
const blocked = await deps.lockedSceneGuard();
|
|
83
113
|
if (blocked)
|
|
84
114
|
throw new Error(blocked);
|
|
85
|
-
const
|
|
115
|
+
const memory = String((args && args.memory) || '');
|
|
116
|
+
// `enabled` 必须是布尔:`rules-toggle` 缺了它直接拒(ops/memory.ts),但那是给用户看的
|
|
117
|
+
// 中文 op 错;这里先挡一句说清"这条工具要什么"。
|
|
118
|
+
if (typeof (args && args.enabled) !== 'boolean')
|
|
119
|
+
throw new Error('缺少参数:enabled 必须是 true 或 false');
|
|
120
|
+
const r = await deps.rulesOps['rules-toggle']({ id: memory, enabled: args.enabled });
|
|
86
121
|
if (!r || r.ok === false)
|
|
87
122
|
throw new Error((r && r.error) || '切换记忆启停失败');
|
|
88
123
|
const rule = r.rule || {};
|
|
89
|
-
//
|
|
90
|
-
//
|
|
124
|
+
// 「启用了一条不在启用场景里的记忆」是一次静默无效:单条开关拨上去了,注入集里却没有它。
|
|
125
|
+
// 这一句不说,模型会以为已经生效 —— 与 skill_manager_switch 说清影子副本同一条理由。
|
|
91
126
|
let sceneNote = '';
|
|
92
127
|
try {
|
|
93
128
|
const all = await deps.rulesOps['rules-list']({});
|
|
@@ -101,87 +136,103 @@ export function buildMemoryTools(deps) {
|
|
|
101
136
|
catch {
|
|
102
137
|
sceneNote = '';
|
|
103
138
|
}
|
|
104
|
-
return 'OK: memory ' + String(rule.id ||
|
|
105
|
-
},
|
|
106
|
-
}));
|
|
107
|
-
register(defineTool({
|
|
108
|
-
name: 'memory_manager_update',
|
|
109
|
-
description: 'Update a memory: body, description, or name (renaming keeps the old file in the trash). Omit a field to keep it — this merges. Read it with memory_manager_read first and change one part rather than restating the file; if a memory already covers this, update that one instead of adding a second. Only on the user\'s instruction.',
|
|
110
|
-
parameters: {
|
|
111
|
-
id: { type: 'string', required: true, description: 'Memory id like <scene>/<name> (from memory_manager_list).' },
|
|
112
|
-
body: { type: 'string', description: 'New Markdown body (<=256 KiB, non-empty). Omit to keep the current body.' },
|
|
113
|
-
description: { type: 'string', description: 'New one-sentence description (<=500 chars). Empty string = drop it and re-derive from the body; omit = keep.' },
|
|
114
|
-
name: { type: 'string', description: 'New memory name (= file name). Omit to keep it.' },
|
|
115
|
-
},
|
|
116
|
-
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
117
|
-
async execute(args) {
|
|
118
|
-
const blocked = await deps.lockedSceneGuard();
|
|
119
|
-
if (blocked)
|
|
120
|
-
throw new Error(blocked);
|
|
121
|
-
// 三个可改字段一个都没给 → 直接拒。`rules-update` 会按"保留现有"落一次盘并刷新
|
|
122
|
-
// updatedAt,模型以为改了点什么,其实只是把文件重写了一遍。
|
|
123
|
-
const touches = ['body', 'description', 'name'].filter((k) => args[k] !== undefined);
|
|
124
|
-
if (!touches.length)
|
|
125
|
-
throw new Error('没有要改的东西:body / description / name 至少给一个');
|
|
126
|
-
const r = await deps.rulesOps['rules-update'](args);
|
|
127
|
-
if (!r || r.ok === false)
|
|
128
|
-
throw new Error((r && r.error) || '更新记忆失败');
|
|
129
|
-
const rule = r.rule || {};
|
|
130
|
-
return 'OK: updated ' + String(rule.id || args.id) + '(改了 ' + touches.join('、') + ')' +
|
|
131
|
-
(rule.id && String(rule.id) !== String(args.id) ? ' → 现在是 ' + rule.id : '');
|
|
139
|
+
return 'OK: memory ' + String(rule.id || memory) + ' ' + (rule.enabled === false ? 'disabled' : 'enabled') + sceneNote;
|
|
132
140
|
},
|
|
133
141
|
}));
|
|
134
142
|
register(defineTool({
|
|
135
|
-
name: '
|
|
143
|
+
name: 'memory_manager_save',
|
|
136
144
|
// 「能推导的别记」「先查重」这两句是 0.13.0 加的:记忆是唯一直接吃注入预算的域
|
|
137
145
|
// (场景段 128 KiB),而模型很乐意把「这个项目用 pnpm」记一条 —— 那看一眼 `package.json`
|
|
138
|
-
//
|
|
139
|
-
|
|
146
|
+
// 就知道,记下来却永久占着每一轮请求。合并 write/update 后这两句更要留:upsert 降低了
|
|
147
|
+
// "再记一条"的摩擦,而摩擦正是这里唯一拦得住它的东西。
|
|
148
|
+
description: 'Create a memory, or update the one that already has this id — the receipt says which happened. Memories cost context on EVERY later request, so record only what cannot be re-derived from code, config or git, and check the list for one that already says this — update that instead of adding a near-duplicate. Only on the user\'s instruction.',
|
|
140
149
|
parameters: {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
memory: { type: 'string', required: true, description: 'Memory id like <scene>/<name> — copy it from the list. `global` is the always-on scene.' },
|
|
151
|
+
description: { type: 'string', description: 'One-sentence description (<=500 chars). Required when creating. Empty string = drop it and re-derive from the body; omit = keep.' },
|
|
152
|
+
body: { type: 'string', description: 'Markdown body (<=256 KiB). Required when creating. Omit to keep the current body.' },
|
|
153
|
+
nextName: { type: 'string', description: 'New memory name (= file name); omit to keep it. The old file goes to the trash.' },
|
|
145
154
|
},
|
|
146
155
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
147
156
|
async execute(args) {
|
|
148
157
|
const blocked = await deps.lockedSceneGuard();
|
|
149
158
|
if (blocked)
|
|
150
159
|
throw new Error(blocked);
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const before = await deps.rulesOps['rules-list']({});
|
|
159
|
-
if (before && before.ok !== false) {
|
|
160
|
-
similar = (before.rules || [])
|
|
161
|
-
.filter((x) => {
|
|
162
|
-
const have = norm(x.name);
|
|
163
|
-
const desc = norm(x.description);
|
|
164
|
-
if (have === wantName)
|
|
165
|
-
return true;
|
|
166
|
-
if (wantDesc !== '' && desc !== '' && desc === wantDesc)
|
|
167
|
-
return true;
|
|
168
|
-
// 名字互相包含:只在两边都不短于 4 个字符时算,否则 "git" 会把所有条目都匹配上。
|
|
169
|
-
return have.length >= 4 && wantName.length >= 4 && (have.includes(wantName) || wantName.includes(have));
|
|
170
|
-
})
|
|
171
|
-
.map((x) => String(x.id))
|
|
172
|
-
.slice(0, 3);
|
|
173
|
-
}
|
|
160
|
+
const memory = String((args && args.memory) || '').trim();
|
|
161
|
+
// 拆成 `<场景>/<名字>`:要求两段都非空。**不做"没有斜杠就落 global"的兜底** —— 那会把
|
|
162
|
+
// 一次拼错的 id 变成一条"每轮都注入"的全局记忆,而清单上看起来完全正常。
|
|
163
|
+
// 清单里打印的就是 `<场景>/<名字>`,照抄比拼装准(§2 规则一)。
|
|
164
|
+
const slash = memory.indexOf('/');
|
|
165
|
+
if (slash <= 0 || slash === memory.length - 1) {
|
|
166
|
+
throw new Error('memory 必须是 <场景>/<名字> 两段(照抄清单里那一行;全局场景写 global/<名字>):' + (memory || '(空)'));
|
|
174
167
|
}
|
|
175
|
-
|
|
176
|
-
const
|
|
168
|
+
const group = memory.slice(0, slash);
|
|
169
|
+
const name = memory.slice(slash + 1);
|
|
170
|
+
// 先取全量:既用来定位(命中 = 改、未命中 = 建),也是查重的数据源。判定必须在工具
|
|
171
|
+
// 这一侧 —— `rules-create` 对同名是**拒绝**的(`error.rules.exists`),op 不会 upsert。
|
|
172
|
+
const all = await deps.rulesOps['rules-list']({});
|
|
173
|
+
if (!all || all.ok === false)
|
|
174
|
+
throw new Error((all && all.error) || '读取规则失败');
|
|
175
|
+
const hit = (all.rules || []).find((x) => String(x.id) === memory);
|
|
176
|
+
if (hit) {
|
|
177
|
+
// 改的分支一律 read-modify-write(省略 = 保持),因为 `rules-update` 是整份重写。
|
|
178
|
+
// 三个可改字段一个都没给就拒:op 会按"保留现有"落一次盘并刷新 updatedAt,
|
|
179
|
+
// 模型以为改了点什么,其实只是把文件重写了一遍。
|
|
180
|
+
const touched = ['body', 'description', 'nextName'].filter((k) => args[k] !== undefined);
|
|
181
|
+
if (!touched.length)
|
|
182
|
+
throw new Error('没有要改的东西:body / description / nextName 至少给一个');
|
|
183
|
+
const payload = { id: memory };
|
|
184
|
+
// 只带模型真给过的键:`description` 传空串在 op 里是"删除并重新派生",与"未给"是两种
|
|
185
|
+
// 意图,都必须原样透传 —— 所以用 `!== undefined` 而不是真值判断。
|
|
186
|
+
if (args.body !== undefined)
|
|
187
|
+
payload.body = args.body;
|
|
188
|
+
if (args.description !== undefined)
|
|
189
|
+
payload.description = args.description;
|
|
190
|
+
if (args.nextName !== undefined)
|
|
191
|
+
payload.name = args.nextName;
|
|
192
|
+
const r = await deps.rulesOps['rules-update'](payload);
|
|
193
|
+
if (!r || r.ok === false)
|
|
194
|
+
throw new Error((r && r.error) || '更新记忆失败');
|
|
195
|
+
const rule = r.rule || {};
|
|
196
|
+
return 'OK: updated ' + String(rule.id || memory) + '(改了 ' + touched.join('、') + ')' +
|
|
197
|
+
(rule.id && String(rule.id) !== memory ? ' → 现在是 ' + rule.id : '');
|
|
198
|
+
}
|
|
199
|
+
// 建的分支:正文与描述都必填。`rules-create` 自己会拒空正文与非法名(ops/memory.ts),
|
|
200
|
+
// 但那是给用户看的中文 op 错;模型需要的是"这条工具要什么",所以在工具侧先挡、点名缺哪个。
|
|
201
|
+
const body = args && args.body !== undefined ? String(args.body) : '';
|
|
202
|
+
const description = args && args.description !== undefined ? String(args.description) : '';
|
|
203
|
+
const missing = [
|
|
204
|
+
body.trim() === '' ? 'body' : '',
|
|
205
|
+
description.trim() === '' ? 'description' : '',
|
|
206
|
+
].filter((x) => x !== '');
|
|
207
|
+
if (missing.length)
|
|
208
|
+
throw new Error('新建记忆必须给 ' + missing.join(' 与 ') + '(' + memory + ' 还不存在)');
|
|
209
|
+
// 查重只**说出来**,不拒绝 —— 猜错一次就把该记的东西记不下去,比多记一条代价大得多;
|
|
210
|
+
// 而且"像不像同一条"本来就是人判断的事。只在建的分支提示:改的分支没有"再记一条"的风险。
|
|
211
|
+
const norm = (value) => String(value ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
212
|
+
const wantName = norm(name);
|
|
213
|
+
const wantDesc = norm(description);
|
|
214
|
+
const similar = (all.rules || [])
|
|
215
|
+
.filter((x) => {
|
|
216
|
+
const have = norm(x.name);
|
|
217
|
+
const desc = norm(x.description);
|
|
218
|
+
if (have === wantName)
|
|
219
|
+
return true;
|
|
220
|
+
if (wantDesc !== '' && desc !== '' && desc === wantDesc)
|
|
221
|
+
return true;
|
|
222
|
+
// 名字互相包含:只在两边都不短于 4 个字符时算,否则 "git" 会把所有条目都匹配上。
|
|
223
|
+
return have.length >= 4 && wantName.length >= 4 && (have.includes(wantName) || wantName.includes(have));
|
|
224
|
+
})
|
|
225
|
+
.map((x) => String(x.id))
|
|
226
|
+
.slice(0, 3);
|
|
227
|
+
const r = await deps.rulesOps['rules-create']({ group, name, description, body });
|
|
177
228
|
if (!r || r.ok === false)
|
|
178
229
|
throw new Error((r && r.error) || '创建规则失败');
|
|
179
230
|
// 刚建好的这条自己会在列表里,从名单里去掉。
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
? `\n注意:已有 ${
|
|
231
|
+
const dup = similar.filter((id) => id !== String(r.rule.id));
|
|
232
|
+
const warn = dup.length
|
|
233
|
+
? `\n注意:已有 ${dup.length} 条名字或描述与它很像(${dup.join('、')})—— 若是同一件事,改那一条而不是留着两条。`
|
|
183
234
|
: '';
|
|
184
|
-
return 'OK:
|
|
235
|
+
return 'OK: created ' + r.rule.id + '(场景「' + (r.rule.group || '未归属') + '」启用后自动生效)' + warn;
|
|
185
236
|
},
|
|
186
237
|
}));
|
|
187
238
|
}
|
package/lib/tools/prompt.js
CHANGED
|
@@ -3,35 +3,46 @@
|
|
|
3
3
|
//
|
|
4
4
|
// 模型可查、可切,**不能造 / 不能删** —— 避免模型乱删用户预设。
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { normalizePresetDescription } from '../prompts/service.js';
|
|
6
7
|
import { text } from './deps.js';
|
|
7
8
|
export function buildPromptTools(deps) {
|
|
8
9
|
const { defineTool, register } = deps;
|
|
9
10
|
register(defineTool({
|
|
10
11
|
name: 'prompt_manager_list',
|
|
11
|
-
description: 'List AGENTS.md presets (id, active state, file path); read that file for a preset body. The one in effect is in the「本机提示词」reminder, whose「来源:」line names the file. Defaults to the one in effect; all=true for the whole library.',
|
|
12
|
+
description: 'List AGENTS.md presets (id, description, active state, file path); read that file for a preset body. The one in effect is in the「本机提示词」reminder, whose「来源:」line names the file. Defaults to the one in effect; all=true for the whole library.',
|
|
12
13
|
parameters: {
|
|
13
14
|
all: { type: 'boolean', description: 'Include inactive presets (default false).' },
|
|
14
15
|
},
|
|
15
16
|
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
16
17
|
async execute(args, exec) {
|
|
17
18
|
const showAll = Boolean(args && args.all === true);
|
|
18
|
-
const r = await deps.
|
|
19
|
-
if (!r.ok)
|
|
20
|
-
throw new Error(r.error);
|
|
19
|
+
const r = await deps.promptOps['agentsmd-list']({});
|
|
20
|
+
if (!r || r.ok === false)
|
|
21
|
+
throw new Error((r && r.error) || '读取提示词预设失败');
|
|
21
22
|
const all = r.presets || [];
|
|
22
23
|
// 默认只列「当前这份 ~/.dsh/AGENTS.md 是从哪个预设来的」(用户裁定 2026-09-16)。
|
|
23
|
-
//
|
|
24
|
-
//
|
|
24
|
+
// 判定分两级:`active`(场景驱动时 = 场景绑的那份;否则 = 内容逐字节相同)优先,
|
|
25
|
+
// 否则看服务层记下的「最近一次应用」—— 用户手改过全局文件时,第二级仍能给出答案,
|
|
26
|
+
// 不必退化成全列(那会白烧上下文)。
|
|
25
27
|
const active = all.filter((p) => p.active === true);
|
|
26
28
|
const fallback = active.length ? active : all.filter((p) => p.lastApplied === true);
|
|
27
29
|
const shown = showAll ? all : fallback;
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
+
// 三列各有用途:
|
|
31
|
+
// · 路径 —— 「名单 → 全文」的闭环。预设正文既不进提示词、也没有读取工具,给出文件
|
|
32
|
+
// 路径让模型用宿主的文件工具读,比再造一个 read 工具省一条常驻 schema。
|
|
33
|
+
// · 描述 —— 0.14.0 加的。模型要能在预设之间做选择,而 id 本身不说明用途。描述在
|
|
34
|
+
// 这里按 `DEFAULT_PRESET_DESC_MAX_LENGTH` 压行截断(与 MCP 备注同一条口径),
|
|
35
|
+
// 因为它从这一版起是**常驻成本**而不是"只给使用者看"。
|
|
36
|
+
// · mark —— `activeVia` 必须打出来:`fileApplied` 但场景绑着别的,说明文件里那份
|
|
37
|
+
// 已经**不生效**了。0.14.0 之前这里会错报成 [active](那正是被修掉的那句假话)。
|
|
30
38
|
const summary = shown.map((p) => {
|
|
31
|
-
const mark = p.active
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
const mark = p.active
|
|
40
|
+
? (p.activeVia === 'scene' ? ' [active via scene]' : ' [active via file]')
|
|
41
|
+
: (p.fileApplied
|
|
42
|
+
? ' [matches ~/.dsh/AGENTS.md, but a scene binding drives the baseline]'
|
|
43
|
+
: (p.lastApplied ? ' [last applied — ~/.dsh/AGENTS.md has changed since]' : ''));
|
|
44
|
+
const desc = normalizePresetDescription(p.description);
|
|
45
|
+
return p.id + mark + ' | ' + (desc || '(no description)') + ' | ' + join(deps.promptsDir, p.id, 'AGENTS.md');
|
|
35
46
|
});
|
|
36
47
|
const header = 'AGENTS.md presets: ' + (showAll
|
|
37
48
|
? all.length + ' in the library'
|
|
@@ -64,7 +75,10 @@ export function buildPromptTools(deps) {
|
|
|
64
75
|
if (r.viaScene) {
|
|
65
76
|
return 'OK: preset ' + args.id + ' is now the prompt binding of scene 「' + (r.scene || '') + '」 and the scene was re-synced (effective next turn; ~/.dsh/AGENTS.md now carries that scene\'s binding).';
|
|
66
77
|
}
|
|
67
|
-
|
|
78
|
+
// 生效时机与宿主行为对齐(index.ts 提示词服务一节考证过):dsh-agent-instructions
|
|
79
|
+
// 每个 pre-step 都 stat 比对版本,写文件即下一轮重读 —— 不是"下个会话"。说错了
|
|
80
|
+
// 模型会劝用户重开会话,而其实下一句话就已经在新基线下跑。
|
|
81
|
+
return 'OK: preset ' + args.id + ' applied to ~/.dsh/AGENTS.md — effective next turn' + (r.backedUp ? ' (previous backed up to __last-applied__)' : '');
|
|
68
82
|
},
|
|
69
83
|
}));
|
|
70
84
|
}
|