dsh-plugin-tool-management 0.13.0 → 0.15.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 +560 -474
- 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 +391 -337
- package/lib/audit-log.js +157 -0
- package/lib/client.js +4204 -2454
- package/lib/compat/patch-dialect.js +3 -1
- package/lib/compat/preset-reach.js +43 -9
- package/lib/compat/probe.js +149 -7
- package/lib/context-inject.js +91 -35
- package/lib/index.js +479 -114
- package/lib/mcp/loader-token.js +53 -28
- package/lib/mcp/manager.js +149 -21
- package/lib/mcp/secret-guard.js +21 -0
- package/lib/mcp/state-section.js +13 -11
- package/lib/memories/archive-engine.js +70 -2
- package/lib/memories/archive.js +8 -1
- package/lib/memories/constants.js +72 -8
- package/lib/memories/index-io.js +7 -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 +26 -6
- package/lib/ops/candidates.js +11 -0
- package/lib/ops/compat.js +113 -31
- package/lib/ops/scene-records.js +34 -0
- package/lib/ops/scene-sync.js +12 -1
- package/lib/ops/sessions.js +24 -9
- package/lib/ops/snapshot.js +596 -0
- package/lib/prompts/service.js +25 -2
- package/lib/scenes/candidates.js +66 -5
- package/lib/sessions/bridge.js +5 -0
- package/lib/sessions/workspace.js +17 -5
- package/lib/skills/core.js +105 -3
- 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 +16 -1
- 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 +208 -15
- package/package.json +24 -20
package/lib/scenes/candidates.js
CHANGED
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
// 依赖全部经 deps 显式传入。注意 \`toolKeyParts\` 留在 index.ts:场景档案引擎也在用它,
|
|
15
15
|
// 放进本文件会让两个域互相引用。
|
|
16
16
|
import { presetRosterOf } from '../compat/preset-reach.js';
|
|
17
|
-
import { injectionFactsOf, readCompositionFacts } from '../compat/preset-reach.js';
|
|
17
|
+
import { injectionFactsOf, readCompositionFacts, readCompositionText } 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;
|
|
@@ -101,7 +110,9 @@ export function createCandidates(deps) {
|
|
|
101
110
|
const presetFactsCache = new Map();
|
|
102
111
|
async function presetFactsForAgent(agent) {
|
|
103
112
|
const roster = presetRoster();
|
|
104
|
-
if (!roster || typeof roster.composedPreset !== 'function'
|
|
113
|
+
if (!roster || typeof roster.composedPreset !== 'function')
|
|
114
|
+
return undefined;
|
|
115
|
+
if (typeof roster.read !== 'function' && typeof roster.readDocument !== 'function')
|
|
105
116
|
return undefined;
|
|
106
117
|
let presetId = '';
|
|
107
118
|
try {
|
|
@@ -116,7 +127,7 @@ export function createCandidates(deps) {
|
|
|
116
127
|
if (hit && Date.now() - hit.at < PRESET_FACTS_TTL_MS)
|
|
117
128
|
return hit.value;
|
|
118
129
|
try {
|
|
119
|
-
const text =
|
|
130
|
+
const text = await readCompositionText(roster, presetId);
|
|
120
131
|
const value = injectionFactsOf(readCompositionFacts(text));
|
|
121
132
|
presetFactsCache.set(presetId, { at: Date.now(), value });
|
|
122
133
|
return value;
|
|
@@ -207,7 +218,10 @@ export function createCandidates(deps) {
|
|
|
207
218
|
presets.push({
|
|
208
219
|
id,
|
|
209
220
|
name: String((p && (p.name || p.id)) || id),
|
|
210
|
-
|
|
221
|
+
// 官方 registry 的预设**没有** trust 字段 —— 缺省必须是 ''(未知)而不是 'user':
|
|
222
|
+
// 客户端只对显式 user 隐藏内置模式的词典名,缺省 'user' 会让四个内置模式永远
|
|
223
|
+
// 显示原始 id(2026-09-28 用户截图)。
|
|
224
|
+
trust: String((p && p.trust) || ''),
|
|
211
225
|
broken: typeof (p && p.broken) === 'string',
|
|
212
226
|
tools: names,
|
|
213
227
|
});
|
|
@@ -278,7 +292,7 @@ export function createCandidates(deps) {
|
|
|
278
292
|
try {
|
|
279
293
|
const roster = await agentPresets.list();
|
|
280
294
|
return (roster || [])
|
|
281
|
-
.map((p) => ({ id: String((p && p.id) || ''), name: String((p && (p.name || p.id)) || ''), trust: String((p && p.trust) || '
|
|
295
|
+
.map((p) => ({ id: String((p && p.id) || ''), name: String((p && (p.name || p.id)) || ''), trust: String((p && p.trust) || '') }))
|
|
282
296
|
.filter((p) => p.id !== '');
|
|
283
297
|
}
|
|
284
298
|
catch {
|
|
@@ -329,6 +343,52 @@ export function createCandidates(deps) {
|
|
|
329
343
|
}
|
|
330
344
|
return { models };
|
|
331
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* 某个 (provider, model) 的思考强度档位。见 `Candidates.modelReasoning` 的注释。
|
|
348
|
+
*
|
|
349
|
+
* `llm` 与 `modelCandidates` 同一路径取(**不在 inject 声明里**,所以必须 `ctx.get('llm')`
|
|
350
|
+
* 而不是 `ctx.llm` —— 后者在未声明该服务时 cordis 代理会抛
|
|
351
|
+
* "cannot get property without inject")。
|
|
352
|
+
*/
|
|
353
|
+
async function modelReasoning(provider, model) {
|
|
354
|
+
const llm = (typeof ctx.get === 'function' ? ctx.get('llm') : undefined);
|
|
355
|
+
if (!llm || typeof llm.resolveModelInfo !== 'function') {
|
|
356
|
+
return { ok: false, error: '宿主没有提供 llm.resolveModelInfo(拿不到档位清单)' };
|
|
357
|
+
}
|
|
358
|
+
const ac = typeof AbortController === 'function' ? new AbortController() : null;
|
|
359
|
+
const timer = ac ? setTimeout(() => ac.abort(), MODEL_REASONING_TIMEOUT_MS) : null;
|
|
360
|
+
try {
|
|
361
|
+
const info = await llm.resolveModelInfo(provider, model, ac ? ac.signal : undefined);
|
|
362
|
+
const reasoning = info && info.reasoning;
|
|
363
|
+
const raw = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts : [];
|
|
364
|
+
const efforts = [];
|
|
365
|
+
for (const e of raw) {
|
|
366
|
+
const id = String((e && e.id) || '');
|
|
367
|
+
if (!id)
|
|
368
|
+
continue;
|
|
369
|
+
// 官方结构:`{ id, name, description? }`(`dsh-llm/lib/types/types.d.ts:295-302`)。
|
|
370
|
+
// name 是给人看的档位名,id 才是要传回去的值 —— 两者都要留着,界面按 name 显示、按 id 提交。
|
|
371
|
+
efforts.push({
|
|
372
|
+
id,
|
|
373
|
+
name: String((e && e.name) || id),
|
|
374
|
+
...(e && typeof e.description === 'string' && e.description ? { description: e.description } : {}),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
ok: true,
|
|
379
|
+
efforts,
|
|
380
|
+
// 缺省(null)= 由 provider 自己的默认决定(官方注释:Absence preserves the provider's own default)。
|
|
381
|
+
defaultEffort: reasoning && reasoning.defaultEffort ? String(reasoning.defaultEffort) : null,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
return { ok: false, error: (e && e.message) ? String(e.message) : String(e) };
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
if (timer)
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
332
392
|
return {
|
|
333
393
|
presetRoster,
|
|
334
394
|
presetFactsForAgent,
|
|
@@ -338,6 +398,7 @@ export function createCandidates(deps) {
|
|
|
338
398
|
presetToolNames,
|
|
339
399
|
presetNames,
|
|
340
400
|
modelCandidates,
|
|
401
|
+
modelReasoning,
|
|
341
402
|
memoryCandidates,
|
|
342
403
|
memorySceneCandidates,
|
|
343
404
|
};
|
package/lib/sessions/bridge.js
CHANGED
|
@@ -62,6 +62,8 @@ function acquireCacheGuard(cache) {
|
|
|
62
62
|
kind: "delete",
|
|
63
63
|
fallback: "inform-only",
|
|
64
64
|
detail: `投影缓存适配未完成(requireTable 抛错):${String(error?.message ?? error)};宿主对象未被改动。`,
|
|
65
|
+
detailKey: "projection-cache-adapter.setup",
|
|
66
|
+
params: { reason: String(error?.message ?? error) },
|
|
65
67
|
});
|
|
66
68
|
throw new CapabilityRefusalError("delete", [{
|
|
67
69
|
id: "projection.table-delete",
|
|
@@ -218,6 +220,9 @@ function acquireCacheGuard(cache) {
|
|
|
218
220
|
detail: reason === "taken-over"
|
|
219
221
|
? "宿主缓存的 put/write 已被第三方包装接管:本插件的删除屏障这一轮已退役(缓存行删除少了墓碑拦截),下一次删除会重新安装。"
|
|
220
222
|
: "投影缓存适配在首次使用时抛错(宿主形状可能变了):已恢复宿主原方法,下一次删除会重新安装并再探一次。",
|
|
223
|
+
detailKey: reason === "taken-over"
|
|
224
|
+
? "projection-cache-adapter.taken-over"
|
|
225
|
+
: "projection-cache-adapter.first-use",
|
|
221
226
|
});
|
|
222
227
|
}
|
|
223
228
|
cacheGuards.set(cache, self);
|
|
@@ -20,6 +20,8 @@ function reportNativeDeleteAvailable(entry) {
|
|
|
20
20
|
kind: "delete",
|
|
21
21
|
fallback: "inform-only",
|
|
22
22
|
detail: `宿主已提供原生 ${entry}:本插件仍走自有完整序列(级联子会话 + spill + 记账 + 缓存行),尚未切换到原生入口。`,
|
|
23
|
+
detailKey: "workspace.delete-native",
|
|
24
|
+
params: { entry },
|
|
23
25
|
});
|
|
24
26
|
}
|
|
25
27
|
//#region lib/types/index.js
|
|
@@ -697,8 +699,8 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
697
699
|
}
|
|
698
700
|
/**
|
|
699
701
|
* 列出已归档会话的展示元数据:sessionId / createdAt / cwd / title / archivedAt。
|
|
700
|
-
* title 经投影缓存 best-effort
|
|
701
|
-
*
|
|
702
|
+
* title 经投影缓存 best-effort 读取(cachedSnapshot 的标题行,旧格式记录退
|
|
703
|
+
* cachedPredecessorTitle);任何一步失败只降级为缺字段,不阻断列表。
|
|
702
704
|
*/
|
|
703
705
|
async archivedSessionDetails() {
|
|
704
706
|
await this.reconcileArchiveLedger();
|
|
@@ -717,10 +719,19 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
717
719
|
entry.cwd = header.cwd;
|
|
718
720
|
const cache = this.ctx.get("sessionProjectionCache");
|
|
719
721
|
if (cache !== undefined && typeof cache.cachedSnapshot === "function") {
|
|
720
|
-
|
|
722
|
+
// 官方契约是 cachedSnapshot(meta, keys?) 两参;keys 传数字会在
|
|
723
|
+
// viewCheckpoint 的 new Set 里抛错,被外层 best-effort 吞成"永远没标题"。
|
|
724
|
+
const snap = cache.cachedSnapshot(header, ["title"]);
|
|
721
725
|
const title = snap?.values?.title;
|
|
722
726
|
if (typeof title === "string")
|
|
723
727
|
entry.title = title;
|
|
728
|
+
else if (typeof cache.cachedPredecessorTitle === "function") {
|
|
729
|
+
// 旧格式代际的缓存记录过不了上面的身份校验,官方另有只认标题的提示面。
|
|
730
|
+
const legacy = cache.cachedPredecessorTitle(header);
|
|
731
|
+
const legacyTitle = legacy?.values?.title;
|
|
732
|
+
if (typeof legacyTitle === "string")
|
|
733
|
+
entry.title = legacyTitle;
|
|
734
|
+
}
|
|
724
735
|
}
|
|
725
736
|
}
|
|
726
737
|
catch (error) {
|
|
@@ -775,7 +786,8 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
775
786
|
return false;
|
|
776
787
|
try {
|
|
777
788
|
// 未播种会话的继承事件数恒为零,先查缓存可避免读取完整会话原文。
|
|
778
|
-
|
|
789
|
+
// keys 缺省 = 任一 wire 行可用即算已有缓存(官方两参契约,勿再传数字)。
|
|
790
|
+
if (!header.isSeeded && cache.cachedSnapshot(header) !== void 0)
|
|
779
791
|
return false;
|
|
780
792
|
const stored = await this.readStoredProjectionSource(persistence, header.id);
|
|
781
793
|
const meta = stored.meta ?? header;
|
|
@@ -788,7 +800,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
788
800
|
this.ctx.logger.warn(`archive-manager: projection repair for unseeded archived session "${header.id}" skipped because its inherited event count is not zero`);
|
|
789
801
|
return false;
|
|
790
802
|
}
|
|
791
|
-
if (cache.cachedSnapshot(meta
|
|
803
|
+
if (cache.cachedSnapshot(meta) !== void 0)
|
|
792
804
|
return false;
|
|
793
805
|
const restored = projections.restore({}, stored.events, 0, meta, inheritedEventCount);
|
|
794
806
|
if (restored === void 0 ||
|
package/lib/skills/core.js
CHANGED
|
@@ -305,8 +305,13 @@ async function nearestProjectRoot(cwd) {
|
|
|
305
305
|
if (!(await fs.stat(start)).isDirectory())
|
|
306
306
|
return null;
|
|
307
307
|
}
|
|
308
|
-
catch {
|
|
309
|
-
|
|
308
|
+
catch (error) {
|
|
309
|
+
// 目录不存在(会话还开着、项目已被删)是最常见的一类:带上系统错误码,
|
|
310
|
+
// 调用方对 ENOENT 静默跳过 —— 报警留给真正读不了的目录。
|
|
311
|
+
const code = error && typeof error === "object" && "code" in error
|
|
312
|
+
? String(error.code)
|
|
313
|
+
: undefined;
|
|
314
|
+
return { unavailable: true, cwd, ...(code ? { code } : {}) };
|
|
310
315
|
}
|
|
311
316
|
let current = start;
|
|
312
317
|
for (;;) {
|
|
@@ -344,6 +349,10 @@ export async function projectRoots(projectCwds = [], diagnostics) {
|
|
|
344
349
|
for (const cwd of Array.isArray(projectCwds) ? projectCwds : []) {
|
|
345
350
|
const found = await nearestProjectRoot(cwd);
|
|
346
351
|
if (!found || !found.root) {
|
|
352
|
+
// 目录不存在(ENOENT)= 会话开着、项目已删:这是常态而不是故障,静默跳过。
|
|
353
|
+
// 报警只留给真实读不了的目录(权限、损坏的链接等)。
|
|
354
|
+
if (found && found.unavailable && found.code === "ENOENT")
|
|
355
|
+
continue;
|
|
347
356
|
if (found &&
|
|
348
357
|
found.unavailable &&
|
|
349
358
|
Array.isArray(diagnostics) &&
|
|
@@ -352,7 +361,7 @@ export async function projectRoots(projectCwds = [], diagnostics) {
|
|
|
352
361
|
diagnostics.push({
|
|
353
362
|
code: "warning.project.unavailable",
|
|
354
363
|
params: { path: cwd },
|
|
355
|
-
error:
|
|
364
|
+
error: `活动会话的项目目录不可访问,项目技能未显示: ${cwd}`,
|
|
356
365
|
});
|
|
357
366
|
}
|
|
358
367
|
continue;
|
|
@@ -2696,6 +2705,99 @@ export async function createSkill(input, log, options = {}) {
|
|
|
2696
2705
|
log("create", `创建 ${join(target, "SKILL.md")}`);
|
|
2697
2706
|
return { name, path: join(target, "SKILL.md"), root: definition.key };
|
|
2698
2707
|
}
|
|
2708
|
+
/**
|
|
2709
|
+
* 改写一份**已存在**的技能(bundle 的 `SKILL.md` 或 flat 的 `<name>.md`)。
|
|
2710
|
+
*
|
|
2711
|
+
* 为什么是独立函数、而不是给 `createSkill` 加一个 `allowOverwriteInOwnRoot` 开关:
|
|
2712
|
+
* 「静默覆盖」这件事不该出现在同一个入口的签名里 —— 调用方读到的函数名必须已经说明了
|
|
2713
|
+
* 它是破坏性的,否则一次参数写错就从"新建"变成"覆盖别人的技能"。
|
|
2714
|
+
*
|
|
2715
|
+
* 写入复用同一套原子写(stage 文件 + `renameWithRetry`)。bundle 形态**只换 SKILL.md**,
|
|
2716
|
+
* 目录里其余文件(附件、脚本、参考文档)原样留着 —— 整目录替换会把它们全删掉。
|
|
2717
|
+
*
|
|
2718
|
+
* 边界:只在 `options.root`(默认 hub = `$DSH_HOME/tool-management/skills`)**里面已经
|
|
2719
|
+
* 存在**的那一份上写。文件不在那里就是"不存在",**绝不新建**(新建是 `createSkill` 的事,
|
|
2720
|
+
* 它的冲突检查会拒掉同名)。
|
|
2721
|
+
*
|
|
2722
|
+
* 「胜出者必须是 hub 这一份」这层判定不在这里:它要读来源排序与首选设置,属于状态层
|
|
2723
|
+
* (见 service.ts 的 `skill-update` op)。本函数只保证"不越出给定根"。
|
|
2724
|
+
*/
|
|
2725
|
+
export async function updateSkill(input, log, options = {}) {
|
|
2726
|
+
const requestedRoot = Object.prototype.hasOwnProperty.call(options, "root")
|
|
2727
|
+
? options.root
|
|
2728
|
+
: rootByKey("hub") || rootByKey("dsh");
|
|
2729
|
+
const definition = await checkedWritableRootDefinition(requestedRoot);
|
|
2730
|
+
if (definition && definition.ok === false)
|
|
2731
|
+
return definition;
|
|
2732
|
+
if (!definition) {
|
|
2733
|
+
if (!rootDefinition(requestedRoot)) {
|
|
2734
|
+
return {
|
|
2735
|
+
ok: false,
|
|
2736
|
+
code: "error.root.unknown",
|
|
2737
|
+
params: { root: String(requestedRoot == null ? "" : requestedRoot) },
|
|
2738
|
+
error: `技能来源不存在:${requestedRoot == null || requestedRoot === "" ? "(空)" : requestedRoot}`,
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
return readonlyError("update");
|
|
2742
|
+
}
|
|
2743
|
+
const root = definition.path;
|
|
2744
|
+
const requestedName = String((input && input.name) || "").trim();
|
|
2745
|
+
const name = toKebab(requestedName);
|
|
2746
|
+
if (!name || !KEBAB_RE.test(name) || entryPath(root, name) === null)
|
|
2747
|
+
return {
|
|
2748
|
+
ok: false,
|
|
2749
|
+
error: `无法生成合法 kebab-case 名称(原始名: ${requestedName})`,
|
|
2750
|
+
code: "error.import.invalidName",
|
|
2751
|
+
params: { name: requestedName },
|
|
2752
|
+
};
|
|
2753
|
+
// 形态判定与 `resolveEntry` 同口径:bundle 目录优先,其次 flat 单文件。
|
|
2754
|
+
const bundleDir = entryPath(root, name);
|
|
2755
|
+
const bundleDoc = join(bundleDir, "SKILL.md");
|
|
2756
|
+
const flatDoc = resolve(root, `${name}.md`);
|
|
2757
|
+
const exists = async (p) => await fs.stat(p).then(() => true).catch(() => false);
|
|
2758
|
+
const existingDoc = (await exists(bundleDoc)) ? bundleDoc : (await exists(flatDoc)) ? flatDoc : null;
|
|
2759
|
+
if (!existingDoc)
|
|
2760
|
+
return {
|
|
2761
|
+
ok: false,
|
|
2762
|
+
code: "error.update.notFound",
|
|
2763
|
+
error: `技能不存在(本插件落点里没有 ${name}):本工具只改本插件自己写的技能,不新建、也不动官方根里的`,
|
|
2764
|
+
params: { name },
|
|
2765
|
+
};
|
|
2766
|
+
const current = parseSkillDoc(await fs.readFile(existingDoc, "utf8"));
|
|
2767
|
+
const curDescription = unquote(String(current.map.description ?? "")).trim();
|
|
2768
|
+
const curBody = String(current.body ?? "").trim();
|
|
2769
|
+
// 省略 = 保持。注意区分"未给"与"给了空串":空串是显式意图,会被下面的必填校验挡下并
|
|
2770
|
+
// 说明缺哪个 —— 静默保留会让模型以为清掉了。
|
|
2771
|
+
const description = input && input.description !== undefined
|
|
2772
|
+
? String(input.description).trim()
|
|
2773
|
+
: curDescription;
|
|
2774
|
+
const body = input && input.body !== undefined ? String(input.body).trim() : curBody;
|
|
2775
|
+
if (!description)
|
|
2776
|
+
return { ok: false, error: "技能简介不能为空", code: "error.create.descriptionRequired" };
|
|
2777
|
+
if (!body)
|
|
2778
|
+
return { ok: false, error: "技能正文不能为空", code: "error.create.bodyRequired" };
|
|
2779
|
+
if (description.length > 500 || body.length > 1 << 18)
|
|
2780
|
+
return { ok: false, error: "技能内容过长", code: "error.create.tooLarge" };
|
|
2781
|
+
const content = `---\nname: ${name}\ndescription: ${yamlString(description)}\n---\n\n${body}\n`;
|
|
2782
|
+
if (existingDoc === flatDoc) {
|
|
2783
|
+
await writeFileAtomically(flatDoc, content);
|
|
2784
|
+
if (log)
|
|
2785
|
+
log("update", `更新 ${flatDoc}`);
|
|
2786
|
+
return { name, path: flatDoc, root: definition.key };
|
|
2787
|
+
}
|
|
2788
|
+
const stage = temporaryPath(existingDoc, "update");
|
|
2789
|
+
try {
|
|
2790
|
+
await fs.writeFile(stage, content, "utf8");
|
|
2791
|
+
await renameWithRetry(stage, existingDoc);
|
|
2792
|
+
}
|
|
2793
|
+
catch (error) {
|
|
2794
|
+
await fs.rm(stage, { force: true }).catch(() => undefined);
|
|
2795
|
+
throw error;
|
|
2796
|
+
}
|
|
2797
|
+
if (log)
|
|
2798
|
+
log("update", `更新 ${existingDoc}`);
|
|
2799
|
+
return { name, path: existingDoc, root: definition.key };
|
|
2800
|
+
}
|
|
2699
2801
|
export async function skillDetail(keyOrRoot, name, options = {}) {
|
|
2700
2802
|
const scopedRoots = await projectRoots(options.projectCwds);
|
|
2701
2803
|
// 支持传 key 或 definition(自定义来源由 service 层解析后传入)。
|
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/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)
|
package/lib/subagents/tools.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function filterBySceneBinding(docs, enabledSceneLists) {
|
|
|
22
22
|
export function defineSubagentManagerListTool(subagents) {
|
|
23
23
|
return {
|
|
24
24
|
name: 'subagent_manager_list',
|
|
25
|
-
description: 'List personas (pre-configured subagent profiles) with their descriptions. The「可委派的子智能体」reminder carries the same catalog; call this for the always-current full list
|
|
25
|
+
description: 'List personas (pre-configured subagent profiles) with their descriptions. The「可委派的子智能体」reminder carries the same catalog; call this for the always-current full list.',
|
|
26
26
|
parameters: {},
|
|
27
27
|
output: {
|
|
28
28
|
schema: { type: 'string' },
|
|
@@ -66,11 +66,18 @@ export function defineSubagentManagerRunTool(subagents) {
|
|
|
66
66
|
// 代价如实记下:子智能体域被关掉、或走压制型预设时上下文里没有那条 how 行,模型只剩本
|
|
67
67
|
// 描述与 `agent` 参数说明("Persona name from subagent_manager_list")—— 够它认出这条是
|
|
68
68
|
// 带人设的委派通道,但"没有人设贴合时才用官方那两个"这层分界就没人说了。
|
|
69
|
+
//
|
|
70
|
+
// 2026-09-23(用户裁定):本工具的 `task` / `inherit` 两个参数说明与上面的 Modes 段
|
|
71
|
+
// **逐字重复**,而工具表每轮都发 —— 同一件事付两次 token。实测 `subagent_manager_run`
|
|
72
|
+
// 整份 407 tok(参数段:`inherit` 86 / `task` 57 / `agent` 19),其中 `inherit` 是全表
|
|
73
|
+
// 最贵的单个参数。收完两处重复后目标 ≈325 tok。判据是"删掉的那句在描述里已经有了吗":
|
|
74
|
+
// Modes 段留着(它是模型的入口),参数说明只留"这一段独有的信息"。
|
|
75
|
+
// `inherit` 那句"轮中委派拿不到当前轮"必须留 —— 它是这条参数唯一会让人写错 task 的地方。
|
|
69
76
|
description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output.\n\nModes: by default a fresh child that cannot see this conversation, so `task` must be self-contained. With `inherit: true` it also gets this conversation\'s **finished** turns (like the host\'s `subagent_fork`) — the current turn is never included, so a mid-turn hand-off still needs a self-contained `task`.\n\n`task` = the goal plus the context it needs; leave method and output format to the persona.\n\nUse it when the work matches a persona in the「可委派的子智能体」reminder (a review, an investigation, a piece of writing) and the detail should not sit in your own context. Not for reading a file (Read), finding a definition (Grep/Glob), or touching two or three files.',
|
|
70
77
|
parameters: {
|
|
71
|
-
agent: { type: 'string', required: true, description: 'Persona name
|
|
72
|
-
task: { type: 'string', required: true, description: 'The task for the subagent
|
|
73
|
-
inherit: { type: 'boolean', description: 'Let the subagent
|
|
78
|
+
agent: { type: 'string', required: true, description: 'Persona name.' },
|
|
79
|
+
task: { type: 'string', required: true, description: 'The task for the subagent. Self-contained by default; with inherit: true only state what is new.' },
|
|
80
|
+
inherit: { type: 'boolean', description: 'Let the subagent see this conversation\'s finished turns (the current turn is never included). Default false = a fresh child.' },
|
|
74
81
|
},
|
|
75
82
|
output: {
|
|
76
83
|
schema: { type: 'string' },
|
package/lib/tools/deps.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
// model 工具(ctx.tools.register + defineTool)各域共用的依赖与 helper。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 为什么单独一个文件:各注入域的工具都要同一批依赖(defineTool 包装、注册出口、场景锁定
|
|
4
4
|
// 守卫、注入边界提示),各写一份会漂移。
|
|
5
5
|
/** 每个工具 output.render 的统一出口:把字符串包成宿主要的 content 数组。 */
|
|
6
6
|
export function text(value) {
|
|
7
7
|
return [{ type: 'text', text: value }];
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* 名单渲染:超过 `TOOL_NAME_LIST_CAP` 项就截断,并把总数写在后面("等 N 项")。
|
|
11
|
+
*
|
|
12
|
+
* 为什么两个地方都要它:`scene_manager_list` 用它列档案,`scene_manager_save` 用它
|
|
13
|
+
* 在回执里回显"这一次替换挤掉了什么"。两处都必须**点名、不能只给数量** —— 只给数量
|
|
14
|
+
* 答不了"我这次给出去的段会不会盖掉原来勾着的那些",而那正是会静默出错的地方。
|
|
15
|
+
* 截断则是因为这些文字都要进上下文,不能无上限。
|
|
16
|
+
*/
|
|
17
|
+
export const TOOL_NAME_LIST_CAP = 6;
|
|
18
|
+
export function toolNameList(list) {
|
|
19
|
+
if (!list.length)
|
|
20
|
+
return '空';
|
|
21
|
+
const head = list.slice(0, TOOL_NAME_LIST_CAP).join('、');
|
|
22
|
+
return list.length > TOOL_NAME_LIST_CAP ? head + ' 等 ' + list.length + ' 项' : head;
|
|
23
|
+
}
|