dsh-plugin-tool-management 0.12.1 → 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 +84 -0
- package/README.md +90 -114
- package/README_EN.md +98 -119
- 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 +63 -0
- package/lib/client.js +1086 -95
- package/lib/compat/preset-reach.js +6 -3
- package/lib/context-inject.js +183 -59
- package/lib/index.js +501 -49
- package/lib/mcp/manager.js +118 -71
- package/lib/mcp/secret-guard.js +21 -0
- package/lib/mcp/state-section.js +19 -14
- package/lib/memories/archive-engine.js +199 -0
- 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 +123 -22
- package/lib/op-registry.js +279 -0
- package/lib/ops/candidates.js +11 -0
- package/lib/ops/compat.js +23 -2
- package/lib/ops/sessions.js +1 -1
- package/lib/ops/state-doctor.js +115 -0
- package/lib/prompts/service.js +25 -2
- package/lib/request-gate.js +21 -78
- package/lib/scene-settings.js +17 -0
- package/lib/scenes/candidates.js +56 -0
- package/lib/skills/catalog.js +10 -5
- package/lib/skills/core.js +102 -0
- package/lib/skills/service.js +34 -2
- package/lib/subagents/catalog.js +9 -5
- package/lib/subagents/service.js +37 -4
- package/lib/subagents/tools.js +19 -5
- package/lib/tools/deps.js +15 -0
- package/lib/tools/mcp.js +291 -50
- package/lib/tools/memory.js +170 -19
- package/lib/tools/prompt.js +28 -14
- package/lib/tools/scene.js +351 -0
- package/lib/tools/skills.js +168 -14
- package/lib/tools/subagent.js +129 -3
- package/lib/tools/table.js +290 -0
- package/package.json +2 -2
package/lib/request-gate.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 这一域管「谁能改、改哪些、什么时候不许改」:
|
|
4
4
|
// * 访问令牌(可选)—— 两条用途:宿主浏览器鉴权的逃生口,以及写操作的纵深防御;
|
|
5
5
|
// * 写 / 敏感 op 白名单 —— 哪些 op 要凭令牌(含"只读但会泄露明文"与"只读但会改宿主状态");
|
|
6
|
+
// 清单本身在 `./op-registry.ts`,本文件只拼装(`opsWith` / `frozenOps`)。
|
|
6
7
|
// * 场景锁定 —— 任一场景 locked=true 时五个管理域整体冻结;
|
|
7
8
|
// * handlers 后处理 —— 把上面三条装到 op 表上(包装顺序即语义,见 installHandlerGuards)。
|
|
8
9
|
//
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
// 都是「读原函数 → 换包装」的原地改写,先装的在内层、后装的在外层;`annotateLocked` 只挂在
|
|
15
16
|
// 五个读 op 上。整段照搬、顺序未变。
|
|
16
17
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
18
|
+
import { frozenOps, opsWith } from './op-registry.js';
|
|
17
19
|
/**
|
|
18
20
|
* 建访问令牌三件套。**必须**从 apply 的第二个参数取配置 ——
|
|
19
21
|
* 读 `ctx.config` 不是注入服务,启动时会抛 "cannot get property without inject"。
|
|
@@ -96,56 +98,18 @@ export function createAccessToken(deps) {
|
|
|
96
98
|
}
|
|
97
99
|
/** 建白名单。必须在四个 service 创建之后构造(依赖其 writeOps)。 */
|
|
98
100
|
export function createOpWhitelist(deps) {
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// (mcpm-* / skill-open / agentsmd-* / history-*)在此列举。
|
|
102
|
-
// 注意:必须在上述 service 创建之后构造(依赖其 writeOps)。
|
|
101
|
+
// 清单本身在 `./op-registry.ts` 里逐条登记(判据、口径、为什么)。这里只做拼装:
|
|
102
|
+
// 写 = 四个 service 自报的 writeOps ∪ 登记表里标了 write 的内联域;敏感 = 登记表标 sensitive。
|
|
103
103
|
const WRITE_OPS = new Set([
|
|
104
104
|
...deps.writeOps.skills,
|
|
105
105
|
...deps.writeOps.memories,
|
|
106
106
|
...deps.writeOps.archives,
|
|
107
107
|
...deps.writeOps.subagents,
|
|
108
|
-
'
|
|
109
|
-
'mcpm-compact',
|
|
110
|
-
'mcpm-export', 'mcpm-import', 'mcpm-note', 'mcpm-settings', 'mcpm-tool-enabled',
|
|
111
|
-
// mcpm-tools-refresh 为了拿实时工具表会临时启用目标服务器、结束后恢复原状(两次
|
|
112
|
-
// writePatch),恢复失败还会停留在启用态 —— 是写不是读,按写门禁(与 mcpm-restart
|
|
113
|
-
// 已在清单同理;0.6.0/0.7.0 已有两次写 op 漏列前科)。
|
|
114
|
-
'mcpm-tools-refresh',
|
|
115
|
-
// mcpm-reveal returns UNMASKED secrets; even though it is a read, it is
|
|
116
|
-
// token-gated like a write — on a LAN-exposed port the token must be the
|
|
117
|
-
// last line of defense for plaintext credentials too, not just writes.
|
|
118
|
-
'mcpm-reveal',
|
|
119
|
-
'skill-open',
|
|
120
|
-
// preset-tools 是只读枚举,但枚举会为预设建立 standing mount(官方语义:每进程只挂一次)。
|
|
121
|
-
// 未授权调用者不该触发挂载 —— 按写门禁。
|
|
122
|
-
//
|
|
123
|
-
// 这条规则的范围写清楚,别当成"凡会写盘就入门禁":`history-list` / `history-sessions`
|
|
124
|
-
// 每次都会写工作区快照、`rules-list` / `rules-read` / `rules-diagnose` 会整份覆盖写
|
|
125
|
-
// `memories-index.json`、`mcpm-list` / `mcpm-tools` 回写 `mcp-known-tools.json` ——
|
|
126
|
-
// 五个都是读 op 且**不在本清单**。判据是"会不会改**宿主或外部系统**的状态":
|
|
127
|
-
// standing mount 改的是宿主进程,而上面那些写的是插件自己的侧车(丢了可重建)。
|
|
128
|
-
// 想要它们也带令牌就显式加进来,别靠"读操作带副作用"这句话推。
|
|
129
|
-
'preset-tools',
|
|
130
|
-
// 提示词写操作(create/update/remove 改预设库;apply 写全局 AGENTS.md;import 从外部内容建预设)
|
|
131
|
-
'agentsmd-create', 'agentsmd-update', 'agentsmd-apply', 'agentsmd-remove', 'agentsmd-import',
|
|
132
|
-
// 提示词预设的回收站(恢复 / 永久删除都是写)
|
|
133
|
-
'agentsmd-trash-restore', 'agentsmd-trash-delete',
|
|
134
|
-
// history 写操作(archive/unarchive 改归档集合;delete 永久删除;retention-set 写保留期;
|
|
135
|
-
// workspace-register 会新增一条宿主工作区登记,同样是写)
|
|
136
|
-
'history-archive', 'history-unarchive', 'history-delete', 'history-retention-set',
|
|
137
|
-
'history-unarchive-batch', 'history-delete-batch', 'history-import', 'history-export',
|
|
138
|
-
// 通用导出:往用户指定的目录写文件,按写操作门禁(token)。
|
|
139
|
-
'bundle-export',
|
|
140
|
-
'history-archive-batch', 'history-workspace-register',
|
|
141
|
-
// 注入设置(五个域开关 / 压制型预设口径)写侧车,按写操作门禁。
|
|
142
|
-
'inject-settings',
|
|
143
|
-
// 清理 patch 备份:删磁盘文件(含明文凭据副本),按写操作门禁。
|
|
144
|
-
'backups-clean',
|
|
108
|
+
...opsWith('write'),
|
|
145
109
|
]);
|
|
146
110
|
// 会泄露明文凭据 / 完整配置的 op:**必须**带对的访问令牌,没配令牌就一律拒绝
|
|
147
111
|
// (判定在 http-fence.ts 的 secretOpRejection,含两种情况的区分与理由)。
|
|
148
|
-
const SENSITIVE_OPS = new Set(
|
|
112
|
+
const SENSITIVE_OPS = new Set(opsWith('sensitive'));
|
|
149
113
|
return { WRITE_OPS, SENSITIVE_OPS };
|
|
150
114
|
}
|
|
151
115
|
// ---------- 场景锁定守卫(v0.8)----------
|
|
@@ -214,37 +178,16 @@ export function installHandlerGuards(deps) {
|
|
|
214
178
|
};
|
|
215
179
|
}
|
|
216
180
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// restart 仍放行(它是"重连"这条恢复路径),但它**不是只读**:实现会两次 `writePatch`
|
|
220
|
-
//(先强制停用、轮询、再按重启前状态恢复)。所以它不改的是**用户选的启停值**,
|
|
221
|
-
// 不是"不碰补丁文件" —— 锁定期间它是唯一能落盘改补丁的入口,进程中断会把服务器
|
|
222
|
-
// 留在停用态。别按"只重连"去理解它。
|
|
223
|
-
'mcpm-add', 'mcpm-edit', 'mcpm-remove', 'mcpm-set-enabled', 'mcpm-set-all', 'mcpm-tool-enabled', 'mcpm-import', 'mcpm-compact', 'mcpm-note', 'mcpm-settings',
|
|
224
|
-
// 技能:启停 / 来源启停与移除恢复 / 首选 / 删除 / 创建导入 / 自定义目录 / 回收站 / 批量启停。
|
|
225
|
-
'skill-enable', 'skill-disable', 'skill-source-enable', 'skill-source-disable', 'skill-source-remove', 'skill-source-restore',
|
|
226
|
-
'skill-prefer', 'skill-unprefer', 'skill-delete', 'skill-create', 'skill-import', 'skill-upload', 'skill-set-all',
|
|
227
|
-
'skill-custom-add', 'skill-custom-remove', 'skill-trash-restore', 'skill-trash-delete',
|
|
228
|
-
// 子智能体:开关 / 改名保存 / 删除 / 导入 / 回收站。
|
|
229
|
-
'subagent-create', 'subagent-update', 'subagent-delete', 'subagent-toggle', 'subagent-import', 'subagent-trash-restore', 'subagent-trash-delete',
|
|
230
|
-
// 记忆:增删改 / 开关 / 导入 / 回收站 / 绑定。set-active 是场景启停,不在冻结范围。
|
|
231
|
-
'rules-create', 'rules-update', 'rules-remove', 'rules-toggle', 'rules-import', 'rules-restore', 'rules-trash-remove', 'rules-attach', 'rules-detach', 'rules-set-index',
|
|
232
|
-
// 提示词:建改删 / 应用(切换生效基线)/ 导入 / 回收站。
|
|
233
|
-
'agentsmd-create', 'agentsmd-update', 'agentsmd-remove', 'agentsmd-apply', 'agentsmd-import', 'agentsmd-trash-restore', 'agentsmd-trash-delete',
|
|
234
|
-
], '修改');
|
|
181
|
+
// 冻结清单与档案同步清单都来自 `./op-registry.ts`:加 op 时在那里登记一条,这里不再抄。
|
|
182
|
+
guardLockedOps(frozenOps('all'), '修改');
|
|
235
183
|
// 场景内「开关」类操作(用户裁定 2026-09-17):未锁定时**可用**,改动同步进当前场景档案。
|
|
236
184
|
// 与「锁定」正交:锁定冻结全部写操作,这里只是把页面开关的意图也写进档案。
|
|
237
|
-
syncSceneArchiveOnSwitch(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// 场景清空** —— 而「当前场景已锁定」是模型侧 `lockedSceneGuard` 唯一的判据,场景一空
|
|
244
|
-
// 它就返回 null,四个写工具全部放开,运行时却仍是那个场景的档案态(2026-09-19 审计
|
|
245
|
-
// T-32)。所以这里单独挡一刀:锁着的场景正在生效时,不许改启用集合(先解锁再说)。
|
|
246
|
-
// 没有锁定场景在生效时(全局态 / 场景未锁)照旧可用 —— 那本来就是允许的。
|
|
247
|
-
for (const [opName, what] of [['rules-set-active', '切换场景']]) {
|
|
185
|
+
syncSceneArchiveOnSwitch(opsWith('syncsArchive'));
|
|
186
|
+
// 口径 `frozenScope: 'active-scene'` 的那几条(判据与理由登记在 op-registry.ts 的
|
|
187
|
+
// `rules-set-active` 条目上)。没有锁定场景在生效时(全局态 / 场景未锁)照旧可用。
|
|
188
|
+
const ACTIVE_SCENE_LABEL = { 'rules-set-active': '切换场景' };
|
|
189
|
+
for (const opName of frozenOps('active-scene')) {
|
|
190
|
+
const what = ACTIVE_SCENE_LABEL[opName] ?? '改它';
|
|
248
191
|
const original = handlers[opName];
|
|
249
192
|
if (typeof original !== 'function')
|
|
250
193
|
continue;
|
|
@@ -264,16 +207,16 @@ export function installHandlerGuards(deps) {
|
|
|
264
207
|
return original(args);
|
|
265
208
|
};
|
|
266
209
|
}
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
210
|
+
// 被锁场景自身的档案与删除:只挡它自己,别的场景照常(口径 `frozenScope: 'self-scene'`)。
|
|
211
|
+
// 目标场景一律从 `args.scene` / `args.name` 里取 —— 这两个键名就是本插件 op 的惯例。
|
|
212
|
+
const SELF_SCENE_LABEL = { 'scene-archive-save': '改档案', 'rules-remove-scene': '删除' };
|
|
213
|
+
for (const opName of frozenOps('self-scene')) {
|
|
214
|
+
const what = SELF_SCENE_LABEL[opName] ?? '改它';
|
|
272
215
|
const original = handlers[opName];
|
|
273
216
|
if (typeof original !== 'function')
|
|
274
217
|
continue;
|
|
275
218
|
handlers[opName] = async (args) => {
|
|
276
|
-
const scene = String(
|
|
219
|
+
const scene = String((args && (args.scene || args.name)) || '').trim();
|
|
277
220
|
const locked = await lockedSceneNames();
|
|
278
221
|
if (scene && locked.includes(scene))
|
|
279
222
|
return { ok: false, error: `场景「${scene}」已锁定:先解锁再${what}` };
|
|
@@ -298,7 +241,7 @@ export function installHandlerGuards(deps) {
|
|
|
298
241
|
}
|
|
299
242
|
return res;
|
|
300
243
|
}
|
|
301
|
-
for (const opName of
|
|
244
|
+
for (const opName of opsWith('annotatesLock')) {
|
|
302
245
|
const original = handlers[opName];
|
|
303
246
|
if (typeof original !== 'function')
|
|
304
247
|
continue;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// 场景页的界面设置(侧车 `scene-settings.json`,界面在场景页右上的按钮)。
|
|
2
|
+
//
|
|
3
|
+
// 目前只有一项:进入场景前要不要先弹那张「会改什么」的卡。它是**界面提示**,不是管理动作 ——
|
|
4
|
+
// 所以它既不受场景锁定影响(锁着场景照样能关提醒),也不参与任何门禁判定;改了只影响下一次
|
|
5
|
+
// 点「启用这个场景」时弹不弹卡。
|
|
6
|
+
//
|
|
7
|
+
// 为什么放在服务端而不是浏览器本地:与其余设置同一个家(侧车 + op + 登记表),换浏览器
|
|
8
|
+
// 也在;写在 localStorage 里就只有那个浏览器认,而这个开关的意义正是"我不想每次都被拦一下",
|
|
9
|
+
// 换了浏览器又冒出来会显得是 bug。
|
|
10
|
+
export const DEFAULT_SCENE_SETTINGS = { enterPreview: true };
|
|
11
|
+
/** 把任意输入夹成合法设置(缺项/类型不对一律退回默认:默认不阻止任何确认)。 */
|
|
12
|
+
export function normalizeSceneSettings(raw) {
|
|
13
|
+
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
14
|
+
return {
|
|
15
|
+
enterPreview: typeof obj.enterPreview === 'boolean' ? obj.enterPreview : DEFAULT_SCENE_SETTINGS.enterPreview,
|
|
16
|
+
};
|
|
17
|
+
}
|
package/lib/scenes/candidates.js
CHANGED
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
// 放进本文件会让两个域互相引用。
|
|
16
16
|
import { presetRosterOf } from '../compat/preset-reach.js';
|
|
17
17
|
import { injectionFactsOf, readCompositionFacts } from '../compat/preset-reach.js';
|
|
18
|
+
/**
|
|
19
|
+
* 问 adapter 的等待上限。官方签名 `resolveModelInfo(provider, model, signal?)` 的 signal 是
|
|
20
|
+
* "optional cancellation for adapter-owned asynchronous lookup",所以给一个上限 —— 界面那一格
|
|
21
|
+
* 是懒加载 + loading 态,挂住不返回会让它一直转。
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ 10s 是**防御性**取值,没有实测依据:本机拿不到活着的 adapter,量不出真实耗时。
|
|
24
|
+
* 真机上若发现常见 provider 只要几十毫秒,可以调小;若某个 provider 稳定超过它,就该调大。
|
|
25
|
+
*/
|
|
26
|
+
const MODEL_REASONING_TIMEOUT_MS = 10_000;
|
|
18
27
|
export function createCandidates(deps) {
|
|
19
28
|
// 外部能力一次解构成局部名:块内代码逐字搬来,保持原样最不容易出错。
|
|
20
29
|
const { get, tools, mcp, skillsService, memoriesService, toolKeyParts } = deps;
|
|
@@ -329,6 +338,52 @@ export function createCandidates(deps) {
|
|
|
329
338
|
}
|
|
330
339
|
return { models };
|
|
331
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* 某个 (provider, model) 的思考强度档位。见 `Candidates.modelReasoning` 的注释。
|
|
343
|
+
*
|
|
344
|
+
* `llm` 与 `modelCandidates` 同一路径取(**不在 inject 声明里**,所以必须 `ctx.get('llm')`
|
|
345
|
+
* 而不是 `ctx.llm` —— 后者在未声明该服务时 cordis 代理会抛
|
|
346
|
+
* "cannot get property without inject")。
|
|
347
|
+
*/
|
|
348
|
+
async function modelReasoning(provider, model) {
|
|
349
|
+
const llm = (typeof ctx.get === 'function' ? ctx.get('llm') : undefined);
|
|
350
|
+
if (!llm || typeof llm.resolveModelInfo !== 'function') {
|
|
351
|
+
return { ok: false, error: '宿主没有提供 llm.resolveModelInfo(拿不到档位清单)' };
|
|
352
|
+
}
|
|
353
|
+
const ac = typeof AbortController === 'function' ? new AbortController() : null;
|
|
354
|
+
const timer = ac ? setTimeout(() => ac.abort(), MODEL_REASONING_TIMEOUT_MS) : null;
|
|
355
|
+
try {
|
|
356
|
+
const info = await llm.resolveModelInfo(provider, model, ac ? ac.signal : undefined);
|
|
357
|
+
const reasoning = info && info.reasoning;
|
|
358
|
+
const raw = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts : [];
|
|
359
|
+
const efforts = [];
|
|
360
|
+
for (const e of raw) {
|
|
361
|
+
const id = String((e && e.id) || '');
|
|
362
|
+
if (!id)
|
|
363
|
+
continue;
|
|
364
|
+
// 官方结构:`{ id, name, description? }`(`dsh-llm/lib/types/types.d.ts:295-302`)。
|
|
365
|
+
// name 是给人看的档位名,id 才是要传回去的值 —— 两者都要留着,界面按 name 显示、按 id 提交。
|
|
366
|
+
efforts.push({
|
|
367
|
+
id,
|
|
368
|
+
name: String((e && e.name) || id),
|
|
369
|
+
...(e && typeof e.description === 'string' && e.description ? { description: e.description } : {}),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
efforts,
|
|
375
|
+
// 缺省(null)= 由 provider 自己的默认决定(官方注释:Absence preserves the provider's own default)。
|
|
376
|
+
defaultEffort: reasoning && reasoning.defaultEffort ? String(reasoning.defaultEffort) : null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
return { ok: false, error: (e && e.message) ? String(e.message) : String(e) };
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
if (timer)
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
332
387
|
return {
|
|
333
388
|
presetRoster,
|
|
334
389
|
presetFactsForAgent,
|
|
@@ -338,6 +393,7 @@ export function createCandidates(deps) {
|
|
|
338
393
|
presetToolNames,
|
|
339
394
|
presetNames,
|
|
340
395
|
modelCandidates,
|
|
396
|
+
modelReasoning,
|
|
341
397
|
memoryCandidates,
|
|
342
398
|
memorySceneCandidates,
|
|
343
399
|
};
|
package/lib/skills/catalog.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
/** 描述截断长度(与官方目录的 `catalogDescriptionMaxLength` 默认值一致)。 */
|
|
22
22
|
export const SKILL_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
|
|
23
23
|
/** 目录最多列几条;超出只报数量(让模型去调 `skill_manager_list`)。 */
|
|
24
|
-
export const SKILL_CATALOG_MAX_ENTRIES =
|
|
24
|
+
export const SKILL_CATALOG_MAX_ENTRIES = 50;
|
|
25
25
|
/** 描述归一化 + 截断(与官方 `catalogDescription` 同款:折叠空白、超出补省略号)。 */
|
|
26
26
|
function catalogDescription(value, maxLength) {
|
|
27
27
|
const normalized = String(value ?? '').replaceAll(/\s+/g, ' ').trim();
|
|
@@ -41,7 +41,7 @@ function isEnabled(row) {
|
|
|
41
41
|
* 返回 `''` 表示没有可注入的技能 —— 没装技能的用户零 token 成本。
|
|
42
42
|
* 名字按字典序排序:底层目录枚举顺序变化时文本仍逐字节稳定(前缀缓存契约)。
|
|
43
43
|
*/
|
|
44
|
-
export function renderSkillCatalog(data, maxEntries = SKILL_CATALOG_MAX_ENTRIES, maxDescription = SKILL_CATALOG_DESCRIPTION_MAX_LENGTH) {
|
|
44
|
+
export function renderSkillCatalog(data, maxEntries = SKILL_CATALOG_MAX_ENTRIES, maxDescription = SKILL_CATALOG_DESCRIPTION_MAX_LENGTH, listToolVisible = true) {
|
|
45
45
|
const roots = (data && typeof data === 'object' ? data.roots : undefined) ?? [];
|
|
46
46
|
const byName = new Map();
|
|
47
47
|
if (Array.isArray(roots)) {
|
|
@@ -77,8 +77,13 @@ export function renderSkillCatalog(data, maxEntries = SKILL_CATALOG_MAX_ENTRIES,
|
|
|
77
77
|
// (2026-09-18 起,见 context-inject.ts 的 DOMAIN_FRAME 的 `how` 行)—— 一处内容一个出处。
|
|
78
78
|
const out = [...lines];
|
|
79
79
|
const hidden = sorted.length - shown.length;
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// 查询工具被关掉(兼容页的模型工具表)时只说"还有几个":点名一个模型手里没有的工具,
|
|
81
|
+
// 它只会去猜名字。数量照报 —— 那是实话,与有没有出口无关。
|
|
82
|
+
if (hidden > 0) {
|
|
83
|
+
out.push('', listToolVisible
|
|
84
|
+
? `(另有 ${hidden} 个未列出,用 \`skill_manager_list\` 查。)`
|
|
85
|
+
: `(另有 ${hidden} 个未列出。)`);
|
|
86
|
+
}
|
|
82
87
|
return out.join('\n');
|
|
83
88
|
}
|
|
84
89
|
/**
|
|
@@ -95,7 +100,7 @@ export function createSkillCatalog(deps, opts = {}) {
|
|
|
95
100
|
try {
|
|
96
101
|
const result = await deps.state();
|
|
97
102
|
if (result && result.ok !== false) {
|
|
98
|
-
value = renderSkillCatalog(result.data ?? result, opts.maxEntries, opts.maxDescription);
|
|
103
|
+
value = renderSkillCatalog(result.data ?? result, opts.maxEntries, opts.maxDescription, deps.listToolVisible ? deps.listToolVisible() : true);
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
catch { /* 保留上一次的值;首次失败则维持 '' */ }
|
package/lib/skills/core.js
CHANGED
|
@@ -2696,6 +2696,99 @@ export async function createSkill(input, log, options = {}) {
|
|
|
2696
2696
|
log("create", `创建 ${join(target, "SKILL.md")}`);
|
|
2697
2697
|
return { name, path: join(target, "SKILL.md"), root: definition.key };
|
|
2698
2698
|
}
|
|
2699
|
+
/**
|
|
2700
|
+
* 改写一份**已存在**的技能(bundle 的 `SKILL.md` 或 flat 的 `<name>.md`)。
|
|
2701
|
+
*
|
|
2702
|
+
* 为什么是独立函数、而不是给 `createSkill` 加一个 `allowOverwriteInOwnRoot` 开关:
|
|
2703
|
+
* 「静默覆盖」这件事不该出现在同一个入口的签名里 —— 调用方读到的函数名必须已经说明了
|
|
2704
|
+
* 它是破坏性的,否则一次参数写错就从"新建"变成"覆盖别人的技能"。
|
|
2705
|
+
*
|
|
2706
|
+
* 写入复用同一套原子写(stage 文件 + `renameWithRetry`)。bundle 形态**只换 SKILL.md**,
|
|
2707
|
+
* 目录里其余文件(附件、脚本、参考文档)原样留着 —— 整目录替换会把它们全删掉。
|
|
2708
|
+
*
|
|
2709
|
+
* 边界:只在 `options.root`(默认 hub = `$DSH_HOME/tool-management/skills`)**里面已经
|
|
2710
|
+
* 存在**的那一份上写。文件不在那里就是"不存在",**绝不新建**(新建是 `createSkill` 的事,
|
|
2711
|
+
* 它的冲突检查会拒掉同名)。
|
|
2712
|
+
*
|
|
2713
|
+
* 「胜出者必须是 hub 这一份」这层判定不在这里:它要读来源排序与首选设置,属于状态层
|
|
2714
|
+
* (见 service.ts 的 `skill-update` op)。本函数只保证"不越出给定根"。
|
|
2715
|
+
*/
|
|
2716
|
+
export async function updateSkill(input, log, options = {}) {
|
|
2717
|
+
const requestedRoot = Object.prototype.hasOwnProperty.call(options, "root")
|
|
2718
|
+
? options.root
|
|
2719
|
+
: rootByKey("hub") || rootByKey("dsh");
|
|
2720
|
+
const definition = await checkedWritableRootDefinition(requestedRoot);
|
|
2721
|
+
if (definition && definition.ok === false)
|
|
2722
|
+
return definition;
|
|
2723
|
+
if (!definition) {
|
|
2724
|
+
if (!rootDefinition(requestedRoot)) {
|
|
2725
|
+
return {
|
|
2726
|
+
ok: false,
|
|
2727
|
+
code: "error.root.unknown",
|
|
2728
|
+
params: { root: String(requestedRoot == null ? "" : requestedRoot) },
|
|
2729
|
+
error: `技能来源不存在:${requestedRoot == null || requestedRoot === "" ? "(空)" : requestedRoot}`,
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
return readonlyError("update");
|
|
2733
|
+
}
|
|
2734
|
+
const root = definition.path;
|
|
2735
|
+
const requestedName = String((input && input.name) || "").trim();
|
|
2736
|
+
const name = toKebab(requestedName);
|
|
2737
|
+
if (!name || !KEBAB_RE.test(name) || entryPath(root, name) === null)
|
|
2738
|
+
return {
|
|
2739
|
+
ok: false,
|
|
2740
|
+
error: `无法生成合法 kebab-case 名称(原始名: ${requestedName})`,
|
|
2741
|
+
code: "error.import.invalidName",
|
|
2742
|
+
params: { name: requestedName },
|
|
2743
|
+
};
|
|
2744
|
+
// 形态判定与 `resolveEntry` 同口径:bundle 目录优先,其次 flat 单文件。
|
|
2745
|
+
const bundleDir = entryPath(root, name);
|
|
2746
|
+
const bundleDoc = join(bundleDir, "SKILL.md");
|
|
2747
|
+
const flatDoc = resolve(root, `${name}.md`);
|
|
2748
|
+
const exists = async (p) => await fs.stat(p).then(() => true).catch(() => false);
|
|
2749
|
+
const existingDoc = (await exists(bundleDoc)) ? bundleDoc : (await exists(flatDoc)) ? flatDoc : null;
|
|
2750
|
+
if (!existingDoc)
|
|
2751
|
+
return {
|
|
2752
|
+
ok: false,
|
|
2753
|
+
code: "error.update.notFound",
|
|
2754
|
+
error: `技能不存在(本插件落点里没有 ${name}):本工具只改本插件自己写的技能,不新建、也不动官方根里的`,
|
|
2755
|
+
params: { name },
|
|
2756
|
+
};
|
|
2757
|
+
const current = parseSkillDoc(await fs.readFile(existingDoc, "utf8"));
|
|
2758
|
+
const curDescription = unquote(String(current.map.description ?? "")).trim();
|
|
2759
|
+
const curBody = String(current.body ?? "").trim();
|
|
2760
|
+
// 省略 = 保持。注意区分"未给"与"给了空串":空串是显式意图,会被下面的必填校验挡下并
|
|
2761
|
+
// 说明缺哪个 —— 静默保留会让模型以为清掉了。
|
|
2762
|
+
const description = input && input.description !== undefined
|
|
2763
|
+
? String(input.description).trim()
|
|
2764
|
+
: curDescription;
|
|
2765
|
+
const body = input && input.body !== undefined ? String(input.body).trim() : curBody;
|
|
2766
|
+
if (!description)
|
|
2767
|
+
return { ok: false, error: "技能简介不能为空", code: "error.create.descriptionRequired" };
|
|
2768
|
+
if (!body)
|
|
2769
|
+
return { ok: false, error: "技能正文不能为空", code: "error.create.bodyRequired" };
|
|
2770
|
+
if (description.length > 500 || body.length > 1 << 18)
|
|
2771
|
+
return { ok: false, error: "技能内容过长", code: "error.create.tooLarge" };
|
|
2772
|
+
const content = `---\nname: ${name}\ndescription: ${yamlString(description)}\n---\n\n${body}\n`;
|
|
2773
|
+
if (existingDoc === flatDoc) {
|
|
2774
|
+
await writeFileAtomically(flatDoc, content);
|
|
2775
|
+
if (log)
|
|
2776
|
+
log("update", `更新 ${flatDoc}`);
|
|
2777
|
+
return { name, path: flatDoc, root: definition.key };
|
|
2778
|
+
}
|
|
2779
|
+
const stage = temporaryPath(existingDoc, "update");
|
|
2780
|
+
try {
|
|
2781
|
+
await fs.writeFile(stage, content, "utf8");
|
|
2782
|
+
await renameWithRetry(stage, existingDoc);
|
|
2783
|
+
}
|
|
2784
|
+
catch (error) {
|
|
2785
|
+
await fs.rm(stage, { force: true }).catch(() => undefined);
|
|
2786
|
+
throw error;
|
|
2787
|
+
}
|
|
2788
|
+
if (log)
|
|
2789
|
+
log("update", `更新 ${existingDoc}`);
|
|
2790
|
+
return { name, path: existingDoc, root: definition.key };
|
|
2791
|
+
}
|
|
2699
2792
|
export async function skillDetail(keyOrRoot, name, options = {}) {
|
|
2700
2793
|
const scopedRoots = await projectRoots(options.projectCwds);
|
|
2701
2794
|
// 支持传 key 或 definition(自定义来源由 service 层解析后传入)。
|
|
@@ -2840,6 +2933,15 @@ export async function getProviderSkill(candidate, options = {}) {
|
|
|
2840
2933
|
typeof options.cwd === "string") {
|
|
2841
2934
|
root = (await projectRoots([options.cwd])).find((item) => item.key === locator.rootKey);
|
|
2842
2935
|
}
|
|
2936
|
+
// 自定义技能目录不住在 `userRoots()` 里,它住在状态文件的 `customRoots` 里,所以
|
|
2937
|
+
// `rootByKey()` 恒取不到。少了这一支就是「列得出、调不动」:`state()` 扫的是
|
|
2938
|
+
// `userRoots() + customRootsFromState()`,官方 catalog 因此把目录里的技能列给模型,
|
|
2939
|
+
// 而模型真去 `skill` 工具取正文时这里返回 undefined —— 症状是
|
|
2940
|
+
// `skill "brainstorming" is unknown or no longer available`(用户 2026-09-22 报的)。
|
|
2941
|
+
if (!root &&
|
|
2942
|
+
CUSTOM_ROOT_KEY_RE.test(locator.rootKey)) {
|
|
2943
|
+
root = customRootsFromState((await readManagerState()).state).find((item) => item.key === locator.rootKey);
|
|
2944
|
+
}
|
|
2843
2945
|
if (!root)
|
|
2844
2946
|
return undefined;
|
|
2845
2947
|
try {
|
package/lib/skills/service.js
CHANGED
|
@@ -10,7 +10,7 @@ import { spawn } from 'node:child_process';
|
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { basename, join } from 'node:path';
|
|
12
12
|
import { isInsideRootResolved } from '../paths.js';
|
|
13
|
-
import { state, setSkillEnabled, setPreferredSkill, markSkillsDisabled, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, permanentlyDeleteTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, skillDetail, listProviderCandidates, getProviderSkill, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, trashRootPath, } from './core.js';
|
|
13
|
+
import { state, setSkillEnabled, setPreferredSkill, markSkillsDisabled, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, permanentlyDeleteTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, updateSkill, skillDetail, listProviderCandidates, getProviderSkill, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, trashRootPath, } from './core.js';
|
|
14
14
|
const MAX_LOG_BYTES = 1 << 20;
|
|
15
15
|
const PROVIDER_NAME = 'dsh-plugin-tool-management-external';
|
|
16
16
|
const message = (e) => String((e && e.message) || e);
|
|
@@ -455,7 +455,7 @@ export function createSkillsService(ctx) {
|
|
|
455
455
|
// 也变),是写操作;漏在这份清单里 = 配了访问令牌也不会被要求带令牌。
|
|
456
456
|
'skill-source-remove', 'skill-source-restore',
|
|
457
457
|
'skill-prefer', 'skill-unprefer',
|
|
458
|
-
'skill-create', 'skill-import', 'skill-upload', 'skill-delete',
|
|
458
|
+
'skill-create', 'skill-update', 'skill-import', 'skill-upload', 'skill-delete',
|
|
459
459
|
// 批量启停与单条启停同权:配了访问令牌的宿主必须同样要求带令牌。
|
|
460
460
|
'skill-set-all',
|
|
461
461
|
'skill-trash-restore', 'skill-trash-delete', 'skill-custom-add', 'skill-custom-remove',
|
|
@@ -549,6 +549,38 @@ export function createSkillsService(ctx) {
|
|
|
549
549
|
}
|
|
550
550
|
return res;
|
|
551
551
|
}), afterWrite),
|
|
552
|
+
// 改写**本插件自己写的那一份**技能。只认 hub 里的胜出者:同名技能在 dsh / agents /
|
|
553
|
+
// claude / 自定义根各有一份时只有一份生效,改错那一份会返回 OK 而技能毫无变化
|
|
554
|
+
// (用户实测踩过:同一技能四份并存,启错根等于没启)。胜出者判定要读来源排序与首选
|
|
555
|
+
// 设置,属于状态层,所以在这里做;core 的 `updateSkill` 只保证不越出给定根。
|
|
556
|
+
'skill-update': wrap((args) => write(async () => {
|
|
557
|
+
const wanted = String(args.name || '').trim();
|
|
558
|
+
const s = await readState();
|
|
559
|
+
const hits = [];
|
|
560
|
+
for (const root of (s && s.roots) || []) {
|
|
561
|
+
for (const skill of root.skills || []) {
|
|
562
|
+
if (String(skill.declaredName || skill.name || '') !== wanted)
|
|
563
|
+
continue;
|
|
564
|
+
hits.push({
|
|
565
|
+
rootKey: String(root.key || ''),
|
|
566
|
+
shadowedBy: skill.shadowedBy && skill.shadowedBy.root ? String(skill.shadowedBy.root) : '',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (!hits.length) {
|
|
571
|
+
return { ok: false, code: 'error.update.notFound', params: { name: wanted }, error: `技能不存在:${wanted}` };
|
|
572
|
+
}
|
|
573
|
+
const winner = hits.find((h) => !h.shadowedBy) || hits[0];
|
|
574
|
+
if (winner.rootKey !== 'hub') {
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
code: 'error.update.notOwnRoot',
|
|
578
|
+
params: { name: wanted, root: winner.rootKey },
|
|
579
|
+
error: `技能 ${wanted} 的生效副本在来源「${winner.rootKey}」,不在本插件落点(hub = $DSH_HOME/tool-management/skills):本插件不修改官方根里的技能`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return updateSkill({ name: args.name, description: args.description, body: args.body }, log, { root: (await requestRoot('hub')) || 'hub' });
|
|
583
|
+
}), afterWrite),
|
|
552
584
|
'skill-import': wrap((args) => write(async () => {
|
|
553
585
|
const res = await importSkill(String(args.source || ''), log, {
|
|
554
586
|
conflict: args.conflict === 'overwrite' ? 'overwrite' : 'skip',
|
package/lib/subagents/catalog.js
CHANGED
|
@@ -11,7 +11,7 @@ import { filterBySceneBinding } from './tools.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
|
|
13
13
|
/** 段里最多列几个人设;超出部分只报数量,让模型自己去调 subagent_manager_list。 */
|
|
14
|
-
export const DEFAULT_CATALOG_MAX_ENTRIES =
|
|
14
|
+
export const DEFAULT_CATALOG_MAX_ENTRIES = 50;
|
|
15
15
|
/** 无描述时的占位,与 `subagent_manager_list` 工具的输出保持同一口径。 */
|
|
16
16
|
const NO_DESCRIPTION = '(无描述)';
|
|
17
17
|
/**
|
|
@@ -43,7 +43,7 @@ export function catalogDescription(value, maxLength) {
|
|
|
43
43
|
* (`- **名字** — 描述`),"这是什么 + 该拿它做什么"整句交给注入通道的引导语。于是这里
|
|
44
44
|
* 既没有标题也没有"可委派给下列子智能体"那句:一处内容只有一个出处。
|
|
45
45
|
*/
|
|
46
|
-
export function renderSubagentCatalog(allowed, maxEntries = DEFAULT_CATALOG_MAX_ENTRIES, maxDescription = DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH) {
|
|
46
|
+
export function renderSubagentCatalog(allowed, maxEntries = DEFAULT_CATALOG_MAX_ENTRIES, maxDescription = DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH, listToolVisible = true) {
|
|
47
47
|
if (!allowed.length)
|
|
48
48
|
return '';
|
|
49
49
|
const sorted = [...allowed].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
@@ -58,8 +58,12 @@ export function renderSubagentCatalog(allowed, maxEntries = DEFAULT_CATALOG_MAX_
|
|
|
58
58
|
// 查询工具**只在真被 40 条上限截掉时**才出现:常态下不提,省常驻字符,也免得模型为了
|
|
59
59
|
// 「确认一遍」去调它(用户裁定:没列出来的就是当前不想要的)。与 MCP 状态段的
|
|
60
60
|
// `(另有 N 台未列出。)` 同一句式,但这里多给一个出口——不给人设就真的找不回来了。
|
|
61
|
-
if (hidden > 0)
|
|
62
|
-
|
|
61
|
+
if (hidden > 0) {
|
|
62
|
+
// 与技能目录同一条纪律:点名一个被关掉的工具只会让模型去猜名字。数量照报。
|
|
63
|
+
out.push('', listToolVisible
|
|
64
|
+
? `(另有 ${hidden} 个未列出,用 \`subagent_manager_list\` 查。)`
|
|
65
|
+
: `(另有 ${hidden} 个未列出。)`);
|
|
66
|
+
}
|
|
63
67
|
return out.join('\n');
|
|
64
68
|
}
|
|
65
69
|
/**
|
|
@@ -101,7 +105,7 @@ export function createSubagentCatalog(deps, opts = {}) {
|
|
|
101
105
|
const cached = rendered.get(depth);
|
|
102
106
|
if (cached !== undefined)
|
|
103
107
|
return cached;
|
|
104
|
-
const out = renderSubagentCatalog(allowed.filter((p) => catalogInjectedAt(p, depth)), maxEntries, maxDescription);
|
|
108
|
+
const out = renderSubagentCatalog(allowed.filter((p) => catalogInjectedAt(p, depth)), maxEntries, maxDescription, deps.listToolVisible ? deps.listToolVisible() : true);
|
|
105
109
|
rendered.set(depth, out);
|
|
106
110
|
return out;
|
|
107
111
|
},
|
package/lib/subagents/service.js
CHANGED
|
@@ -99,6 +99,17 @@ export function parsePresetToolRules(frontmatter) {
|
|
|
99
99
|
return Object.keys(rules).length ? rules : undefined;
|
|
100
100
|
}
|
|
101
101
|
/** 行式 frontmatter 解析:只认 description / provider / model / tools / toolsDeny / toolsByPreset / catalogDepth / output。 */
|
|
102
|
+
/**
|
|
103
|
+
* 思考强度档位 id 的归一:单行、去空白、限长。**不校验它是不是合法档位** —— 清单要问 adapter
|
|
104
|
+
* 才拿得到(可能拿不到),而官方本来就会在 provider I/O 之前拒掉不支持的显式值。
|
|
105
|
+
*
|
|
106
|
+
* 为什么必须去换行:frontmatter 是**逐行**解析的(`parsePersona`),值里带换行会把后面半截
|
|
107
|
+
* 变成一行无主文本。限长是防脏值(正常档位 id 都很短)。
|
|
108
|
+
*/
|
|
109
|
+
export const REASONING_EFFORT_MAX_LENGTH = 64;
|
|
110
|
+
export function normalizeReasoningEffort(value) {
|
|
111
|
+
return String(value ?? '').replace(/[\r\n]+/g, ' ').trim().slice(0, REASONING_EFFORT_MAX_LENGTH);
|
|
112
|
+
}
|
|
102
113
|
export function parsePersona(raw, fallbackName) {
|
|
103
114
|
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
|
|
104
115
|
const data = {};
|
|
@@ -133,6 +144,8 @@ export function parsePersona(raw, fallbackName) {
|
|
|
133
144
|
description: data.description || firstLine,
|
|
134
145
|
provider: data.provider || undefined,
|
|
135
146
|
model: data.model || undefined,
|
|
147
|
+
// frontmatter 键统一小写后是 `reasoningeffort`(写出去的是 camelCase)。
|
|
148
|
+
reasoningEffort: normalizeReasoningEffort(data.reasoningeffort) || undefined,
|
|
136
149
|
tools,
|
|
137
150
|
toolsDeny,
|
|
138
151
|
...(toolsByPreset === undefined ? {} : { toolsByPreset }),
|
|
@@ -618,8 +631,22 @@ export function createSubagentService(ctx, opts) {
|
|
|
618
631
|
// provider 与 model 是模型路由的两半:DSH 的 resolveModel(provider, model) 不做
|
|
619
632
|
// `provider/model` 字符串拆分,只改 model 会落在**主会话的 provider** 上——跨来源
|
|
620
633
|
// 指定模型(如 sensenova 的 sensenova-6.8-flash-lite)必须两个键一起给。
|
|
621
|
-
|
|
622
|
-
|
|
634
|
+
// 2026-09-23 加 `reasoningEffort`:条件是"三个键任一有值"—— 只设强度(不改模型)也得
|
|
635
|
+
// 把这个对象发出去,否则那一格永远不生效。官方合并语义(`dsh-subagent/lib/index.js:471-483`)
|
|
636
|
+
// 是"先继承主会话的三项、再用这里的覆盖",所以只给 reasoningEffort 时 provider/model
|
|
637
|
+
// 照旧继承;反过来,改了 provider/model 而没给 reasoningEffort 时,**继承来的那一档会被
|
|
638
|
+
// 删掉**(回落到该模型自己的默认)—— 那不是 bug,是官方的显式语义。
|
|
639
|
+
...(p.provider || p.model || p.reasoningEffort
|
|
640
|
+
? {
|
|
641
|
+
agentOptions: {
|
|
642
|
+
...(p.provider ? { provider: p.provider } : {}),
|
|
643
|
+
...(p.model ? { model: p.model } : {}),
|
|
644
|
+
// 类型是官方的品牌串 `ReasoningEffortId`;我们的值本来就是 adapter 自己给的
|
|
645
|
+
// opaque id(界面从 `resolveModelInfo` 的清单里选出来的),所以这里只做归一、
|
|
646
|
+
// 不重新校验 —— 不支持的档位由官方在 provider I/O 之前拒。
|
|
647
|
+
...(p.reasoningEffort ? { reasoningEffort: p.reasoningEffort } : {}),
|
|
648
|
+
},
|
|
649
|
+
}
|
|
623
650
|
: {}),
|
|
624
651
|
});
|
|
625
652
|
try {
|
|
@@ -660,7 +687,8 @@ export function createSubagentService(ctx, opts) {
|
|
|
660
687
|
return {
|
|
661
688
|
ok: true,
|
|
662
689
|
// catalogDepth 报**生效值**(没写就是默认 1),界面与模型都不必各自知道默认是多少。
|
|
663
|
-
|
|
690
|
+
// 手写 map:新增字段必须**两处都加**(list 与 get),少一处就是界面读不到。
|
|
691
|
+
subagents: docs.map((p) => ({ name: p.name, enabled: p.enabled !== false, description: p.description, provider: p.provider ?? null, model: p.model ?? null, reasoningEffort: p.reasoningEffort ?? null, tools: p.tools ?? null, toolsDeny: p.toolsDeny ?? null, toolsByPreset: p.toolsByPreset ?? null, catalogDepth: catalogDepthOf(p), output: p.output ?? null })),
|
|
664
692
|
};
|
|
665
693
|
},
|
|
666
694
|
'subagent-get': async (args) => {
|
|
@@ -669,7 +697,7 @@ export function createSubagentService(ctx, opts) {
|
|
|
669
697
|
const p = docs.find((d) => d.name === name);
|
|
670
698
|
if (!p)
|
|
671
699
|
return { ok: false, error: `人设不存在: ${name}` };
|
|
672
|
-
return { ok: true, persona: { name: p.name, description: p.description, provider: p.provider ?? '', model: p.model ?? '', tools: p.tools ?? [], toolsDeny: p.toolsDeny ?? [], toolsByPreset: p.toolsByPreset ?? {}, catalogDepth: catalogDepthOf(p), output: p.output ?? '', body: p.body } };
|
|
700
|
+
return { ok: true, persona: { name: p.name, description: p.description, provider: p.provider ?? '', model: p.model ?? '', reasoningEffort: p.reasoningEffort ?? '', tools: p.tools ?? [], toolsDeny: p.toolsDeny ?? [], toolsByPreset: p.toolsByPreset ?? {}, catalogDepth: catalogDepthOf(p), output: p.output ?? '', body: p.body } };
|
|
673
701
|
},
|
|
674
702
|
'subagent-create': async (args) => {
|
|
675
703
|
const name = String((args && args.name) || '').trim();
|
|
@@ -982,6 +1010,9 @@ export function serializePersona(args) {
|
|
|
982
1010
|
const description = String((args && args.description) || '').replace(/\r?\n/g, ' ').trim();
|
|
983
1011
|
const provider = String((args && args.provider) || '').trim();
|
|
984
1012
|
const model = String((args && args.model) || '').trim();
|
|
1013
|
+
// 思考强度:空值**不落盘**(与 `catalogDepth` 默认值同规矩)—— 免得每个新建的人设都多一行
|
|
1014
|
+
// 说明"它和默认一样",也保证老的人设文件回写后逐字节不变。
|
|
1015
|
+
const reasoningEffort = normalizeReasoningEffort(args?.reasoningEffort);
|
|
985
1016
|
const tools = toStringList(args?.tools);
|
|
986
1017
|
// 黑名单字段兼容两种入参名:toolsDeny(UI/camel)与 tools_deny(snake)。
|
|
987
1018
|
const toolsDeny = toStringList(args?.toolsDeny ?? args?.tools_deny);
|
|
@@ -1006,6 +1037,8 @@ export function serializePersona(args) {
|
|
|
1006
1037
|
lines.push('provider: ' + provider);
|
|
1007
1038
|
if (model)
|
|
1008
1039
|
lines.push('model: ' + model);
|
|
1040
|
+
if (reasoningEffort)
|
|
1041
|
+
lines.push('reasoningEffort: ' + reasoningEffort);
|
|
1009
1042
|
if (hasCatalogDepth)
|
|
1010
1043
|
lines.push('catalogDepth: ' + String(catalogDepth));
|
|
1011
1044
|
for (const line of outputLines)
|