dsh-plugin-tool-management 0.8.5 → 0.9.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 +40 -1
- package/README.md +50 -48
- package/README_EN.md +44 -44
- 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 +25 -0
- package/lib/agents-md/preset-id.js +1 -1
- package/lib/agents-md/service.js +46 -9
- package/lib/approval-policy.js +1 -1
- package/lib/client.js +335 -92
- package/lib/compat/preset-reach.js +155 -93
- package/lib/context-inject.js +304 -0
- package/lib/hub.js +155 -13
- package/lib/imports/upload.js +34 -14
- package/lib/index.js +680 -233
- package/lib/mcp/state-section.js +46 -15
- package/lib/rules/archive-engine.js +137 -63
- package/lib/rules/archive.js +34 -0
- package/lib/rules/service.js +376 -158
- package/lib/scene-prompt-sync.js +101 -12
- package/lib/skills/catalog.js +125 -0
- package/lib/skills/core.js +6 -4
- package/lib/subagents/catalog.js +15 -6
- package/lib/subagents/service.js +25 -12
- package/lib/subagents/tools.js +18 -13
- package/package.json +11 -6
- package/screenshots.json +8 -8
- package/docs/images/1/345/234/272/346/231/257.png +0 -0
- package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
- package/docs/images/2MCP.png +0 -0
- package/docs/images/2MCP_en.png +0 -0
- package/docs/images/3/346/212/200/350/203/275.png +0 -0
- package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
- package/docs/images/6/350/256/260/345/277/206.png +0 -0
- package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
- package/docs/images/7/344/274/232/350/257/235.png +0 -0
- package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
- package/docs/images/8/345/205/274/345/256/271.png +0 -0
- package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
- package/lib/prompt-sections.js +0 -148
package/lib/scene-prompt-sync.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// dsh-plugin-tool-management —— 场景绑定提示词 ⇄ 全局基线(`~/.dsh/AGENTS.md`)同步。
|
|
2
2
|
//
|
|
3
3
|
// 用户裁定(2026-09-15):「切换场景,对应的提示词直接把 AGENTS.md 直接修改」。
|
|
4
|
-
//
|
|
5
|
-
// 与「提示词」页的「应用」走同一条路(覆盖前多代备份到 `
|
|
4
|
+
// 因此场景绑定的提示词不再作为提示词段/注入发送,而是**真写全局基线文件**,
|
|
5
|
+
// 与「提示词」页的「应用」走同一条路(覆盖前多代备份到 `prompts/__last-applied__/`)。
|
|
6
|
+
// 例外(2026-09-16):预设**挂不到**官方 `dsh-agent-instructions` 行时(极简),文件没人读,
|
|
7
|
+
// 注入通道会拿同一份正文兜底送达(rules 服务的 `promptText()`)。
|
|
6
8
|
//
|
|
7
|
-
//
|
|
9
|
+
// 六个动作会触发同步(调用方在 op 层包一层 `withSync`):
|
|
8
10
|
// ① 启用/切换场景 ② 关掉场景(恢复进场景前的基线)
|
|
9
11
|
// ③ 改场景绑定的预设 ④ 编辑"正在驱动基线的那份预设"的正文
|
|
12
|
+
// ⑤ 从回收站恢复预设(绑定重新变活) ⑥ 应用提示词(场景接管时只放行绑定的那一份)
|
|
10
13
|
//
|
|
11
14
|
// 「进场景前的基线」快照存在 hub 的 `scene-baseline.json`:进场景时记下当时的
|
|
12
15
|
// AGENTS.md 正文(以及当时匹配到的预设 id),关掉场景时**按原文**写回 ——
|
|
@@ -15,6 +18,11 @@
|
|
|
15
18
|
//
|
|
16
19
|
// 幂等与如实:文件已经是那份(`duplicate`)时不写盘;没有场景驱动、也没有快照时什么都不做;
|
|
17
20
|
// 写失败**不改**原操作的成功结论,而是把原因放进 `error` 交给界面显示成警告。
|
|
21
|
+
//
|
|
22
|
+
// 这里同时是**三条状态的事实源**(用户实测反馈「显示 A、实际注入 B、A 还能删 B 不能删」后加的):
|
|
23
|
+
// `driver()` —— 谁在驱动基线(提示词页「应用」的守卫依据:场景接管期间不许应用别的预设);
|
|
24
|
+
// `refs()` —— 每个预设被谁引用(删除保护的唯一依据:场景绑定 / 基线当前内容 / 退出恢复目标);
|
|
25
|
+
// `baseline()` —— 进场景前的基线快照(退出场景后要恢复的那一份)。
|
|
18
26
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
19
27
|
import { dirname } from 'node:path';
|
|
20
28
|
export function createScenePromptSync(deps) {
|
|
@@ -54,9 +62,12 @@ export function createScenePromptSync(deps) {
|
|
|
54
62
|
const sp = r && r.ok ? r.scenePrompt : null;
|
|
55
63
|
if (!sp)
|
|
56
64
|
return null;
|
|
65
|
+
const scene = sp.scene ? String(sp.scene) : null;
|
|
57
66
|
return {
|
|
58
|
-
scene
|
|
67
|
+
scene,
|
|
59
68
|
presetId: sp.presetId ? String(sp.presetId) : null,
|
|
69
|
+
// 显示名缺失时退回场景名(老载荷 / 桩),宁可粗糙也不要空字符串。
|
|
70
|
+
label: sp.label ? String(sp.label) : (scene || ''),
|
|
60
71
|
missing: sp.missing === true,
|
|
61
72
|
duplicate: sp.duplicate === true,
|
|
62
73
|
};
|
|
@@ -66,18 +77,96 @@ export function createScenePromptSync(deps) {
|
|
|
66
77
|
return null;
|
|
67
78
|
}
|
|
68
79
|
}
|
|
80
|
+
/** 一个场景是否真的在驱动基线:有场景、绑定非空且预设正文读得到。 */
|
|
81
|
+
const drives = (s) => !!s && s.scene !== null && !s.missing && s.presetId !== null;
|
|
82
|
+
async function driver() {
|
|
83
|
+
const current = await state();
|
|
84
|
+
if (!drives(current))
|
|
85
|
+
return null;
|
|
86
|
+
return { scene: current.scene, label: current.label || current.scene, presetId: current.presetId };
|
|
87
|
+
}
|
|
88
|
+
async function applyGuard(id) {
|
|
89
|
+
const current = await driver();
|
|
90
|
+
if (!current)
|
|
91
|
+
return null;
|
|
92
|
+
// 同一份 = 重新应用(把被手改的基线写回场景绑定的内容)→ 放行。
|
|
93
|
+
if (current.presetId === String(id ?? '').trim())
|
|
94
|
+
return null;
|
|
95
|
+
return current;
|
|
96
|
+
}
|
|
97
|
+
async function refs() {
|
|
98
|
+
const out = new Map();
|
|
99
|
+
const add = (id, ref) => {
|
|
100
|
+
const list = out.get(id);
|
|
101
|
+
if (!list) {
|
|
102
|
+
out.set(id, [ref]);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const dup = list.some((x) => {
|
|
106
|
+
if (x.kind !== ref.kind)
|
|
107
|
+
return false;
|
|
108
|
+
if (x.kind === 'scene' && ref.kind === 'scene')
|
|
109
|
+
return x.scene === ref.scene;
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
if (!dup)
|
|
113
|
+
list.push(ref);
|
|
114
|
+
};
|
|
115
|
+
// 三类探测各自失败都算「没探全」→ 整体返回 null(放行删除 + warn),
|
|
116
|
+
// 与原有「探测失败不拦删除、最坏少一份副本」的口径一致。
|
|
117
|
+
let complete = true;
|
|
118
|
+
try {
|
|
119
|
+
const r = await deps.rules.ops['rules-list']({});
|
|
120
|
+
if (!r || r.ok !== true) {
|
|
121
|
+
complete = false;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
for (const s of (Array.isArray(r.scenes) ? r.scenes : [])) {
|
|
125
|
+
const id = s && typeof s.prompt === 'string' ? s.prompt : '';
|
|
126
|
+
if (id === '')
|
|
127
|
+
continue;
|
|
128
|
+
const scene = String((s && s.name) || '');
|
|
129
|
+
add(id, { kind: 'scene', scene, label: String((s && s.label) || scene), active: s && s.active === true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
complete = false;
|
|
135
|
+
warn(`scene-prompt: refs probe (scenes) failed: ${String(e)}`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const cur = await deps.agentsMd.getCurrent();
|
|
139
|
+
if (!cur || cur.ok !== true)
|
|
140
|
+
complete = false;
|
|
141
|
+
else if (cur.presetId)
|
|
142
|
+
add(String(cur.presetId), { kind: 'file' });
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
complete = false;
|
|
146
|
+
warn(`scene-prompt: refs probe (baseline file) failed: ${String(e)}`);
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const saved = await readBaseline();
|
|
150
|
+
if (saved && saved.content !== null && saved.presetId)
|
|
151
|
+
add(saved.presetId, { kind: 'restore' });
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
complete = false;
|
|
155
|
+
warn(`scene-prompt: refs probe (snapshot) failed: ${String(e)}`);
|
|
156
|
+
}
|
|
157
|
+
return complete ? out : null;
|
|
158
|
+
}
|
|
69
159
|
async function sync() {
|
|
70
160
|
const current = await state();
|
|
71
161
|
if (!current)
|
|
72
162
|
return { unchanged: true };
|
|
73
|
-
|
|
74
|
-
if (driving) {
|
|
163
|
+
if (drives(current)) {
|
|
75
164
|
if (current.duplicate)
|
|
76
165
|
return { applied: current.presetId, unchanged: true };
|
|
77
166
|
// 进场景的第一笔写入之前,先记下当时的基线(已有快照就不覆盖:连续切场景时
|
|
78
167
|
// 基线始终是"进场景之前"那一份)。
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
168
|
+
const saved = await readBaseline();
|
|
169
|
+
if (!saved || saved.content === null) {
|
|
81
170
|
const cur = await deps.agentsMd.getCurrent();
|
|
82
171
|
await writeBaseline({
|
|
83
172
|
v: 1,
|
|
@@ -92,10 +181,10 @@ export function createScenePromptSync(deps) {
|
|
|
92
181
|
return { applied: current.presetId };
|
|
93
182
|
}
|
|
94
183
|
// 没有场景驱动 → 如果刚从驱动态退出,把进场景前的基线写回去。
|
|
95
|
-
const
|
|
96
|
-
if (!
|
|
184
|
+
const saved = await readBaseline();
|
|
185
|
+
if (!saved || saved.content === null)
|
|
97
186
|
return { unchanged: true };
|
|
98
|
-
const res = await deps.agentsMd.restore(
|
|
187
|
+
const res = await deps.agentsMd.restore(saved.content);
|
|
99
188
|
if (res && res.ok === false)
|
|
100
189
|
return { error: String(res.error || '恢复 AGENTS.md 失败') };
|
|
101
190
|
await writeBaseline({ v: 1, at: Date.now(), presetId: null, content: null });
|
|
@@ -108,5 +197,5 @@ export function createScenePromptSync(deps) {
|
|
|
108
197
|
const agentsMd = await sync();
|
|
109
198
|
return { ...res, agentsMd };
|
|
110
199
|
}
|
|
111
|
-
return { sync, withSync, state };
|
|
200
|
+
return { sync, withSync, state, driver, applyGuard, refs };
|
|
112
201
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/skills/catalog.ts —— 技能目录段(注入通道的数据源)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它:技能目录本来是官方 `@deepseek-ai/dsh-tool-skill` 的活 —— 它随预设挂载,
|
|
4
|
+
// 极简这类预设没挂,模型连"有哪些技能"都不知道(网页版把宿主那两条行交给预设后尤其如此)。
|
|
5
|
+
// 本模块提供同一份信息的**兜底通道**:只在预设没挂官方那一行时注入(判定在
|
|
6
|
+
// src/context-inject.ts 的 `CARRIER_FACT_OF`),格式与官方目录同形(`- \`name\`: description`)。
|
|
7
|
+
//
|
|
8
|
+
// 数据源用**插件自己的技能清单**(技能服务的 `skill-state`):它已经含启停 / 被同名覆盖 /
|
|
9
|
+
// 同名首选的判定 —— 那正是用户在技能页管的那些开关;官方注册表里的同一份策略也由本插件的
|
|
10
|
+
// provider 喂进去,所以两边看到的是同一批技能。
|
|
11
|
+
//
|
|
12
|
+
// 与官方目录的两处有意差别:
|
|
13
|
+
// - 只列**当前启用且可被模型调用**的(官方只过滤可调用性,不管用户在插件页关掉了谁)。
|
|
14
|
+
// - 结尾多一行:本预设没有官方 `skill` 加载工具(目录与工具是一起挂的),需要正文时得先
|
|
15
|
+
// `skill_manager_list` 取源文件路径再读那个文件 —— 不写这句,模型会以为有 `skill` 工具可调。
|
|
16
|
+
//
|
|
17
|
+
// 同步性:`text()` 必须同步返回(注入通道每个 step 同步取文本),而技能清单是异步读盘 →
|
|
18
|
+
// 与子智能体目录同构的 stale-while-revalidate:`text()` 返回缓存值并在超龄时后台重算,
|
|
19
|
+
// `refresh()` 供技能写操作后立即重算。
|
|
20
|
+
/** 描述截断长度(与官方目录的 `catalogDescriptionMaxLength` 默认值一致)。 */
|
|
21
|
+
export const SKILL_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
|
|
22
|
+
/** 目录最多列几条;超出只报数量(让模型去调 `skill_manager_list`)。 */
|
|
23
|
+
export const SKILL_CATALOG_MAX_ENTRIES = 50;
|
|
24
|
+
/** 描述归一化 + 截断(与官方 `catalogDescription` 同款:折叠空白、超出补省略号)。 */
|
|
25
|
+
function catalogDescription(value, maxLength) {
|
|
26
|
+
const normalized = String(value ?? '').replaceAll(/\s+/g, ' ').trim();
|
|
27
|
+
if (normalized.length <= maxLength)
|
|
28
|
+
return normalized;
|
|
29
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
30
|
+
}
|
|
31
|
+
/** 与 `skill_manager_list` / 界面(`isSkillEnabled`)同一套启停推导。 */
|
|
32
|
+
function isEnabled(row) {
|
|
33
|
+
if (row.enabled !== undefined)
|
|
34
|
+
return row.enabled === true;
|
|
35
|
+
return row.invocationPolicyValid === true && row.modelInvocable === true && row.userInvocable === true && row.managerEnabled !== false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 渲染技能目录段(纯函数,便于单独推理)。
|
|
39
|
+
*
|
|
40
|
+
* 返回 `''` 表示没有可注入的技能 —— 没装技能的用户零 token 成本。
|
|
41
|
+
* 名字按字典序排序:底层目录枚举顺序变化时文本仍逐字节稳定(前缀缓存契约)。
|
|
42
|
+
*/
|
|
43
|
+
export function renderSkillCatalog(data, maxEntries = SKILL_CATALOG_MAX_ENTRIES, maxDescription = SKILL_CATALOG_DESCRIPTION_MAX_LENGTH) {
|
|
44
|
+
const roots = (data && typeof data === 'object' ? data.roots : undefined) ?? [];
|
|
45
|
+
const byName = new Map();
|
|
46
|
+
if (Array.isArray(roots)) {
|
|
47
|
+
for (const root of roots) {
|
|
48
|
+
const skills = (root && typeof root === 'object' ? root.skills : undefined) ?? [];
|
|
49
|
+
if (!Array.isArray(skills))
|
|
50
|
+
continue;
|
|
51
|
+
for (const raw of skills) {
|
|
52
|
+
if (!raw || typeof raw !== 'object')
|
|
53
|
+
continue;
|
|
54
|
+
const row = raw;
|
|
55
|
+
// 影子里那份不可加载(同名只有胜出者生效),启停开关也管不到它 —— 不进目录。
|
|
56
|
+
if (row.shadowedBy)
|
|
57
|
+
continue;
|
|
58
|
+
if (!isEnabled(row) || row.modelInvocable === false)
|
|
59
|
+
continue;
|
|
60
|
+
const name = String(row.declaredName || row.name || '').trim();
|
|
61
|
+
if (name === '')
|
|
62
|
+
continue;
|
|
63
|
+
// 同名(跨根)只留一份:胜出者由 core 标出,这里按出现顺序先到先得,避免重复行。
|
|
64
|
+
if (byName.has(name))
|
|
65
|
+
continue;
|
|
66
|
+
byName.set(name, { name, description: catalogDescription(row.description, maxDescription) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (byName.size === 0)
|
|
71
|
+
return '';
|
|
72
|
+
const sorted = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
73
|
+
const shown = sorted.slice(0, Math.max(0, maxEntries));
|
|
74
|
+
const lines = shown.map((row) => (row.description ? '- `' + row.name + '`: ' + row.description : '- `' + row.name + '`'));
|
|
75
|
+
const out = [
|
|
76
|
+
'## 技能',
|
|
77
|
+
'',
|
|
78
|
+
'**本预设没有官方 `skill` 加载工具;要技能正文时用 `skill_manager_list` 取源文件路径再读。**',
|
|
79
|
+
'',
|
|
80
|
+
...lines,
|
|
81
|
+
];
|
|
82
|
+
const hidden = sorted.length - shown.length;
|
|
83
|
+
if (hidden > 0)
|
|
84
|
+
out.push('', `(另有 ${hidden} 个未列出,用 \`skill_manager_list\` 查。)`);
|
|
85
|
+
return out.join('\n');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 创建 SWR 的技能目录。计算失败时保留上一次的值(不把段清空),首次失败维持 `''`。
|
|
89
|
+
* 并发调用共享同一次计算(inflight 折叠)。
|
|
90
|
+
*/
|
|
91
|
+
export function createSkillCatalog(deps, opts = {}) {
|
|
92
|
+
const ttlMs = opts.ttlMs ?? 30_000;
|
|
93
|
+
const now = opts.now ?? (() => Date.now());
|
|
94
|
+
let value = '';
|
|
95
|
+
let loadedAt = Number.NEGATIVE_INFINITY;
|
|
96
|
+
let inflight = null;
|
|
97
|
+
const recompute = async () => {
|
|
98
|
+
try {
|
|
99
|
+
const result = await deps.state();
|
|
100
|
+
if (result && result.ok !== false) {
|
|
101
|
+
value = renderSkillCatalog(result.data ?? result, opts.maxEntries, opts.maxDescription);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* 保留上一次的值;首次失败则维持 '' */ }
|
|
105
|
+
loadedAt = now();
|
|
106
|
+
};
|
|
107
|
+
const revalidate = () => {
|
|
108
|
+
if (inflight)
|
|
109
|
+
return inflight;
|
|
110
|
+
inflight = recompute().finally(() => { inflight = null; });
|
|
111
|
+
return inflight;
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
text: () => {
|
|
115
|
+
if (now() - loadedAt > ttlMs)
|
|
116
|
+
void revalidate();
|
|
117
|
+
return value;
|
|
118
|
+
},
|
|
119
|
+
refresh: () => {
|
|
120
|
+
loadedAt = Number.NEGATIVE_INFINITY;
|
|
121
|
+
return revalidate();
|
|
122
|
+
},
|
|
123
|
+
warm: () => revalidate(),
|
|
124
|
+
};
|
|
125
|
+
}
|
package/lib/skills/core.js
CHANGED
|
@@ -411,13 +411,15 @@ export function managerHomePath() {
|
|
|
411
411
|
return join(resolveDshHome(), "tool-management");
|
|
412
412
|
}
|
|
413
413
|
export function managerStatePath() {
|
|
414
|
-
return join(managerHomePath(), "state.json");
|
|
414
|
+
return join(managerHomePath(), "skills-state.json");
|
|
415
415
|
}
|
|
416
|
+
/** 技能回收站:与其余四类同形(`hub/trash/<域>-trash/`)。 */
|
|
416
417
|
export function trashRootPath() {
|
|
417
|
-
return join(managerHomePath(), "trash");
|
|
418
|
+
return join(managerHomePath(), "trash", "skills-trash");
|
|
418
419
|
}
|
|
420
|
+
/** 插件日志(滚动 `.1`):收在 hub 内,不再躺在 `$DSH_HOME` 根下。 */
|
|
419
421
|
export function logPath() {
|
|
420
|
-
return join(
|
|
422
|
+
return join(managerHomePath(), "tool-management.log");
|
|
421
423
|
}
|
|
422
424
|
/**
|
|
423
425
|
* 为前端内嵌目录选择器列出一个本机目录层级。
|
|
@@ -885,7 +887,7 @@ async function writeFileAtomically(path, content) {
|
|
|
885
887
|
await fs.writeFile(temp, content, "utf8");
|
|
886
888
|
// Windows 上目标文件会被杀软/索引器短暂占住(EPERM/EACCES/EBUSY)——
|
|
887
889
|
// 场景模式进出时这里写的是技能来源/策略状态,rename 被撞 = 运行时已切、
|
|
888
|
-
// 状态没落盘。与
|
|
890
|
+
// 状态没落盘。与 memories-index.json 同一处理:重试瞬时占用,全失败才抛。
|
|
889
891
|
await renameWithRetry(temp, path);
|
|
890
892
|
}
|
|
891
893
|
catch (error) {
|
package/lib/subagents/catalog.js
CHANGED
|
@@ -8,9 +8,9 @@ import { filterBySceneBinding } from './tools.js';
|
|
|
8
8
|
* 官方对应的旋钮是 `catalogDescriptionMaxLength`。
|
|
9
9
|
*/
|
|
10
10
|
export const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
|
|
11
|
-
/** 段里最多列几个人设;超出部分只报数量,让模型自己去调
|
|
11
|
+
/** 段里最多列几个人设;超出部分只报数量,让模型自己去调 subagent_manager_list。 */
|
|
12
12
|
export const DEFAULT_CATALOG_MAX_ENTRIES = 40;
|
|
13
|
-
/** 无描述时的占位,与 `
|
|
13
|
+
/** 无描述时的占位,与 `subagent_manager_list` 工具的输出保持同一口径。 */
|
|
14
14
|
const NO_DESCRIPTION = '(无描述)';
|
|
15
15
|
/**
|
|
16
16
|
* 描述归一化 + 截断 —— 逐字对齐官方 `dsh-tool-skill` 的实现:
|
|
@@ -27,6 +27,13 @@ export function catalogDescription(value, maxLength) {
|
|
|
27
27
|
*
|
|
28
28
|
* 返回 `''` 表示不注入 —— `renderPrompt` 会删除空段,所以不用人设的用户零 token 成本。
|
|
29
29
|
* 名字按字典序排序:即使底层目录枚举顺序变化,段文本也保持逐字节稳定(前缀缓存契约)。
|
|
30
|
+
*
|
|
31
|
+
* 文案纪律(2026-09-16 用户裁定):**只写模型能照做的事,不写实现说明**。原文两句里
|
|
32
|
+
* 「该人设的完整提示词会成为子代理的系统提示词」「子代理在独立上下文中执行」都是宿主内部
|
|
33
|
+
* 机制,模型无法据此行动;而委派的调用语义与成本(自包含任务、只回最终结果、会开新会话)
|
|
34
|
+
* 已经写在 `subagent_manager_run` 的描述里 —— 常驻层再重复一遍等于同一件事付两次 token。
|
|
35
|
+
* 第二版(用户指出"太冗余"):引导语压成**半行** —— 只留「怎么用」,"下面是名字与摘要"这类
|
|
36
|
+
* 自明的话删掉;域是什么由消息引导语与轨迹行名交代,细节由 `subagent_manager_list` 承担。
|
|
30
37
|
*/
|
|
31
38
|
export function renderSubagentCatalog(allowed, maxEntries = DEFAULT_CATALOG_MAX_ENTRIES, maxDescription = DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH) {
|
|
32
39
|
if (!allowed.length)
|
|
@@ -36,15 +43,17 @@ export function renderSubagentCatalog(allowed, maxEntries = DEFAULT_CATALOG_MAX_
|
|
|
36
43
|
const lines = shown.map((p) => '- **' + p.name + '** — ' + (catalogDescription(p.description, maxDescription) || NO_DESCRIPTION));
|
|
37
44
|
const hidden = sorted.length - shown.length;
|
|
38
45
|
const out = [
|
|
39
|
-
'##
|
|
46
|
+
'## 子智能体',
|
|
40
47
|
'',
|
|
41
|
-
'
|
|
42
|
-
'用 `subagent_run` 委派时,该人设的完整提示词会成为子代理的系统提示词,子代理在独立上下文中执行,只返回最终结果。',
|
|
48
|
+
'**可委派给下列子智能体(调 `subagent_manager_run`)。**',
|
|
43
49
|
'',
|
|
44
50
|
...lines,
|
|
45
51
|
];
|
|
52
|
+
// 查询工具**只在真被 40 条上限截掉时**才出现:常态下不提,省常驻字符,也免得模型为了
|
|
53
|
+
// 「确认一遍」去调它(用户裁定:没列出来的就是当前不想要的)。与 MCP 状态段的
|
|
54
|
+
// `(另有 N 台未列出。)` 同一句式,但这里多给一个出口——不给人设就真的找不回来了。
|
|
46
55
|
if (hidden > 0)
|
|
47
|
-
out.push('', `(另有 ${hidden} 个未列出,用
|
|
56
|
+
out.push('', `(另有 ${hidden} 个未列出,用 \`subagent_manager_list\` 查。)`);
|
|
48
57
|
return out.join('\n');
|
|
49
58
|
}
|
|
50
59
|
/**
|
package/lib/subagents/service.js
CHANGED
|
@@ -11,12 +11,13 @@ import { resolveDshHome } from '../skills/core.js';
|
|
|
11
11
|
import { listTrashEntries, moveOutOfTrash, moveToTrash, purgeTrashEntry, readTrashEntry } from '../hub.js';
|
|
12
12
|
import { expandUploads, planPersonaImport } from '../imports/upload.js';
|
|
13
13
|
const message = (e) => String((e && e.message) || e);
|
|
14
|
-
/** 人设默认目录:`$DSH_HOME/tool-management/
|
|
14
|
+
/** 人设默认目录:`$DSH_HOME/tool-management/subagents/`(域 = 子智能体,工具 `subagent_manager_*`)。 */
|
|
15
15
|
export function defaultPersonasDir() {
|
|
16
|
-
return join(resolveDshHome(), 'tool-management', '
|
|
16
|
+
return join(resolveDshHome(), 'tool-management', 'subagents');
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* 旧目录 `$DSH_HOME/subagents/` → `tool-management/
|
|
19
|
+
* 旧目录 `$DSH_HOME/subagents/` → `tool-management/subagents/` 一次性搬移(幂等)。
|
|
20
|
+
* (hub 内 `agents/` → `subagents/` 的改名由 hub.ts 的启动迁移负责。)
|
|
20
21
|
* 只在目标不存在同名文件时搬(绝不覆盖);源目录保留空壳。每个进程只跑一次。
|
|
21
22
|
*/
|
|
22
23
|
let personasRelocated = false;
|
|
@@ -131,11 +132,11 @@ const RESULT_MAX = 16 * 1024;
|
|
|
131
132
|
export function createSubagentService(ctx, opts) {
|
|
132
133
|
const dir = opts?.subagentsDir || defaultPersonasDir();
|
|
133
134
|
const stateDir = opts?.stateDir && opts.stateDir.trim() !== '' ? opts.stateDir : join(resolveDshHome(), 'tool-management');
|
|
134
|
-
const stateFile = join(stateDir, '
|
|
135
|
+
const stateFile = join(stateDir, 'subagents-index.json');
|
|
135
136
|
const req = createRequire(import.meta.url);
|
|
136
137
|
let cache = null;
|
|
137
138
|
// ── 人设启用集合(子智能体开关)────────────────────────────────────────
|
|
138
|
-
//
|
|
139
|
+
// subagents-index.json:{ version: 1, enabled: string[] }(与 memories-index.json 同目录约定)。
|
|
139
140
|
// - 文件缺失/损坏 = **全部启用**(老用户升级零感知,行为与开关上线前一致);
|
|
140
141
|
// - 文件一旦写出即为权威:之后新建/导入/回收站恢复的人设**自动启用**(刚建就想用是常理);
|
|
141
142
|
// - 停用只影响注入与 subagent_* 工具的可见性,人设文件一个字节不动。
|
|
@@ -410,17 +411,17 @@ export function createSubagentService(ctx, opts) {
|
|
|
410
411
|
const exists = await readFile(target, 'utf8').then(() => true).catch(() => false);
|
|
411
412
|
if (!exists)
|
|
412
413
|
return { ok: false, error: `人设不存在: ${name}` };
|
|
413
|
-
const moved = await moveToTrash('
|
|
414
|
+
const moved = await moveToTrash('subagents', name, [{ from: target, dest: 'persona.md' }]);
|
|
414
415
|
if (moved.ok === false)
|
|
415
416
|
return { ok: false, error: `移入回收站失败: ${moved.error}` };
|
|
416
417
|
await removePersonaFromEnabled(name).catch(() => undefined);
|
|
417
418
|
cache = null;
|
|
418
419
|
return { ok: true, name, trashId: moved.id };
|
|
419
420
|
},
|
|
420
|
-
'subagent-trash-list': async () => ({ ok: true, trash: await listTrashEntries('
|
|
421
|
+
'subagent-trash-list': async () => ({ ok: true, trash: await listTrashEntries('subagents') }),
|
|
421
422
|
'subagent-trash-restore': async (args) => {
|
|
422
423
|
const id = String((args && args.id) || '').trim();
|
|
423
|
-
const entry = await readTrashEntry('
|
|
424
|
+
const entry = await readTrashEntry('subagents', id);
|
|
424
425
|
if (!entry)
|
|
425
426
|
return { ok: false, error: `回收站条目不存在: ${id}` };
|
|
426
427
|
if (!validPersonaName(entry.name))
|
|
@@ -432,12 +433,12 @@ export function createSubagentService(ctx, opts) {
|
|
|
432
433
|
return { ok: false, error: `无法恢复,同名人设已存在: ${entry.name}` };
|
|
433
434
|
try {
|
|
434
435
|
await mkdir(dir, { recursive: true });
|
|
435
|
-
await moveOutOfTrash('
|
|
436
|
+
await moveOutOfTrash('subagents', id, 'persona.md', target);
|
|
436
437
|
}
|
|
437
438
|
catch (e) {
|
|
438
439
|
return { ok: false, error: `恢复失败: ${message(e)}` };
|
|
439
440
|
}
|
|
440
|
-
await purgeTrashEntry('
|
|
441
|
+
await purgeTrashEntry('subagents', id);
|
|
441
442
|
// v0.8.5:回收站恢复默认不启动(与新建/导入同口径)。
|
|
442
443
|
await materializeEnabledExcluding([entry.name]).catch(() => undefined);
|
|
443
444
|
cache = null;
|
|
@@ -445,7 +446,7 @@ export function createSubagentService(ctx, opts) {
|
|
|
445
446
|
},
|
|
446
447
|
'subagent-trash-delete': async (args) => {
|
|
447
448
|
const id = String((args && args.id) || '').trim();
|
|
448
|
-
const gone = await purgeTrashEntry('
|
|
449
|
+
const gone = await purgeTrashEntry('subagents', id);
|
|
449
450
|
if (!gone)
|
|
450
451
|
return { ok: false, error: `回收站条目不存在: ${id}` };
|
|
451
452
|
return { ok: true, id };
|
|
@@ -496,7 +497,7 @@ export function createSubagentService(ctx, opts) {
|
|
|
496
497
|
return { ok: true, imported, skipped };
|
|
497
498
|
},
|
|
498
499
|
/**
|
|
499
|
-
* 子智能体开关(v0.8):停用 = 不注入目录段、
|
|
500
|
+
* 子智能体开关(v0.8):停用 = 不注入目录段、subagent_manager_list/run 不可见;文件本体不动。
|
|
500
501
|
* `enabled` 必须显式给布尔值 —— 与 rules-toggle 同一条口径:不按"翻转"推断,
|
|
501
502
|
* 免得参数丢了时界面以为改了、实际什么都没动。
|
|
502
503
|
*/
|
|
@@ -533,6 +534,18 @@ export function createSubagentService(ctx, opts) {
|
|
|
533
534
|
const state = new Map(docs.map((d) => [d.name, d.enabled !== false]));
|
|
534
535
|
return names.filter((n) => state.get(n) === false);
|
|
535
536
|
},
|
|
537
|
+
/** 指定名单里当前**开着**的(进入模式把未勾的关掉时,只记将被关闭的行)。 */
|
|
538
|
+
async enabledAmong(names) {
|
|
539
|
+
const docs = await list();
|
|
540
|
+
const state = new Map(docs.map((d) => [d.name, d.enabled !== false]));
|
|
541
|
+
return names.filter((n) => state.get(n) === true);
|
|
542
|
+
},
|
|
543
|
+
/** 当前开着的人设全名单(档案页把页面的「开关」落成场景绑定时用)。 */
|
|
544
|
+
async enabledNames() {
|
|
545
|
+
const docs = await list();
|
|
546
|
+
const state = new Map(docs.map((d) => [d.name, d.enabled !== false]));
|
|
547
|
+
return docs.map((d) => d.name).filter((n) => state.get(n) === true);
|
|
548
|
+
},
|
|
536
549
|
/** 批量启停;只碰给出的名字,人设已不存在的跳过(别把悬空名写进集合)。 */
|
|
537
550
|
async setEnabled(names, enabled) {
|
|
538
551
|
const docs = await list();
|
package/lib/subagents/tools.js
CHANGED
|
@@ -16,31 +16,36 @@ export async function filterBySceneBinding(docs, enabledSceneLists) {
|
|
|
16
16
|
const allowed = live.filter((d) => bound.has(d.name));
|
|
17
17
|
return { allowed, reason: allowed.length ? null : `当前启用场景的子智能体绑定: ${[...bound].join('、')}` };
|
|
18
18
|
}
|
|
19
|
-
export function
|
|
19
|
+
export function defineSubagentManagerListTool(subagents) {
|
|
20
20
|
return {
|
|
21
|
-
name: '
|
|
22
|
-
description: 'List available personas (pre-configured subagent profiles) with their descriptions. Call before
|
|
23
|
-
parameters: {
|
|
24
|
-
scene: { type: 'string', description: 'Optional scene name filter (informational; the tool already reflects scene binding).' },
|
|
25
|
-
},
|
|
21
|
+
name: 'subagent_manager_list',
|
|
22
|
+
description: 'List available personas (pre-configured subagent profiles) with their descriptions. Call before subagent_manager_run.',
|
|
23
|
+
parameters: {},
|
|
26
24
|
output: {
|
|
27
25
|
schema: { type: 'string' },
|
|
28
26
|
render: (_a, v) => text(String(v)),
|
|
29
27
|
},
|
|
30
|
-
async execute() {
|
|
28
|
+
async execute(_args, exec) {
|
|
31
29
|
const { allowed, reason } = await filterBySceneBinding(await subagents.list(), await subagents.sceneLists());
|
|
32
30
|
const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(无描述)'));
|
|
33
|
-
|
|
31
|
+
let notice = '';
|
|
32
|
+
try {
|
|
33
|
+
notice = typeof subagents.noticeFor === 'function' ? await subagents.noticeFor(exec) : '';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
notice = '';
|
|
37
|
+
}
|
|
38
|
+
return '人设子智能体(' + allowed.length + '):\n' + (lines.join('\n') || '(无)') + (reason ? '\n注意:' + reason : '') + notice;
|
|
34
39
|
},
|
|
35
40
|
};
|
|
36
41
|
}
|
|
37
|
-
export function
|
|
42
|
+
export function defineSubagentManagerRunTool(subagents) {
|
|
38
43
|
return {
|
|
39
|
-
name: '
|
|
40
|
-
description: 'Run a named persona as a one-shot subagent: it
|
|
44
|
+
name: 'subagent_manager_run',
|
|
45
|
+
description: 'Run a named persona as a one-shot subagent: it gets the persona as its own system prompt, works on `task` in a fresh context, and returns only its final output. Stateless — it does not see this conversation. Expensive: starts a fresh model session, so use it only for self-contained work.',
|
|
41
46
|
parameters: {
|
|
42
|
-
agent: { type: 'string', required: true, description: 'Persona name from
|
|
43
|
-
task: { type: 'string', required: true, description: '
|
|
47
|
+
agent: { type: 'string', required: true, description: 'Persona name from subagent_manager_list.' },
|
|
48
|
+
task: { type: 'string', required: true, description: 'Self-contained task, with all context the subagent needs.' },
|
|
44
49
|
},
|
|
45
50
|
output: {
|
|
46
51
|
schema: { type: 'string' },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-tool-management",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagents, AGENTS.md presets and archived sessions — scene memory injects into the system prompt, scene lock freezes all five domains, deleted workspaces re-register with one click. Settings UI + HTTP API + 14 model tools (
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagents, AGENTS.md presets and archived sessions — scene memory injects into the system prompt, scene lock freezes all five domains, deleted workspaces re-register with one click. Settings UI + HTTP API + 14 model tools (mcp_manager_*, skill_manager_*, prompt_manager_*, memory_manager_*, subagent_*). Install with one command: dsh plugin --profile web add dsh-plugin-tool-management@latest",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
|
|
42
42
|
"host-deps": "node scripts/host-deps.mjs --fix",
|
|
43
43
|
"doctor": "node scripts/doctor.mjs",
|
|
44
|
-
"test": "npm run build && npm run check:i18n && node --test test/
|
|
44
|
+
"test": "npm run build && npm run check:i18n && node --test test/client-exports.test.mjs test/client-render.test.mjs",
|
|
45
|
+
"sync:profile": "node scripts/sync-profile.mjs"
|
|
45
46
|
},
|
|
46
47
|
"keywords": [
|
|
47
48
|
"dsh",
|
|
@@ -66,12 +67,14 @@
|
|
|
66
67
|
},
|
|
67
68
|
"peerDependencies": {
|
|
68
69
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
69
|
-
"@deepseek-ai/dsh-
|
|
70
|
-
"@deepseek-ai/dsh-
|
|
71
|
-
"@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
|
|
72
72
|
"@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2",
|
|
73
74
|
"@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2",
|
|
74
75
|
"@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2",
|
|
75
78
|
"react": "^18.2.0"
|
|
76
79
|
},
|
|
77
80
|
"peerDependenciesMeta": {
|
|
@@ -81,6 +84,8 @@
|
|
|
81
84
|
},
|
|
82
85
|
"devDependencies": {
|
|
83
86
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
87
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
84
89
|
"@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
|
|
85
90
|
"@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
|
|
86
91
|
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
package/screenshots.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
-
"docs/images/1
|
|
3
|
-
"docs/images/
|
|
4
|
-
"docs/images/3
|
|
5
|
-
"docs/images/4
|
|
6
|
-
"docs/images/5
|
|
7
|
-
"docs/images/6
|
|
8
|
-
"docs/images/7
|
|
9
|
-
"docs/images/8
|
|
2
|
+
"docs/images/1.png",
|
|
3
|
+
"docs/images/2.png",
|
|
4
|
+
"docs/images/3.png",
|
|
5
|
+
"docs/images/4.png",
|
|
6
|
+
"docs/images/5.png",
|
|
7
|
+
"docs/images/6.png",
|
|
8
|
+
"docs/images/7.png",
|
|
9
|
+
"docs/images/8.png"
|
|
10
10
|
]
|
|
Binary file
|
|
Binary file
|
package/docs/images/2MCP.png
DELETED
|
Binary file
|
package/docs/images/2MCP_en.png
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|