dsh-plugin-prompt-tool 0.1.5 → 0.2.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/README.md +75 -171
- package/lib/client.js +73 -9
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +16 -2
- package/lib/index.mjs +184 -24
- package/lib/preset-core.d.mts +12 -1
- package/lib/preset-core.mjs +94 -21
- package/package.json +5 -6
- package/plan.md +292 -265
- package/preset/near-anchor.mjs +117 -0
- package/preset/router-first-turn.mjs +59 -0
- package/{vendor → upstream}/dsh-anchored-standard/NOTICE +5 -0
- package/upstream/dsh-anchored-standard/REVISION +1 -0
- package/{vendor → upstream}/dsh-anchored-standard/preset/agent.cordis.yml +53 -24
- package/upstream/dsh-anchored-standard/preset/context-gate.mjs +202 -0
- package/{vendor → upstream}/dsh-anchored-standard/preset/dev-tool-search.mjs +10 -8
- package/{vendor → upstream}/dsh-anchored-standard/preset/instruction-hint.mjs +66 -13
- package/{vendor → upstream}/dsh-anchored-standard/preset/tool-bootstrap.mjs +41 -78
- package/preset/turn-anchor.mjs +0 -83
- /package/{vendor → upstream}/dsh-anchored-standard/LICENSE +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/custom-bash.mjs +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/preset.yml +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/skill-search.mjs +0 -0
package/lib/index.mjs
CHANGED
|
@@ -10,7 +10,9 @@ const name = "prompt-tool";
|
|
|
10
10
|
const inject = [
|
|
11
11
|
"skills",
|
|
12
12
|
"webServer",
|
|
13
|
-
"commands"
|
|
13
|
+
"commands",
|
|
14
|
+
"llm",
|
|
15
|
+
"subagents"
|
|
14
16
|
];
|
|
15
17
|
const PRESET_FILE_URL = new URL("../preset.md", import.meta.url);
|
|
16
18
|
const PRESET_FILE_PATH = fileURLToPath(PRESET_FILE_URL);
|
|
@@ -26,8 +28,9 @@ const DEFAULT_PRESET_ORDER = 5;
|
|
|
26
28
|
const DEFAULT_SKILL_RANK_BASE = 250;
|
|
27
29
|
const PRESET_TEMPLATE_META = fileURLToPath(new URL("../preset/preset.yml", import.meta.url));
|
|
28
30
|
const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injector.mjs", import.meta.url));
|
|
29
|
-
const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/
|
|
30
|
-
const
|
|
31
|
+
const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/near-anchor.mjs", import.meta.url));
|
|
32
|
+
const PRESET_TEMPLATE_ROUTER = fileURLToPath(new URL("../preset/router-first-turn.mjs", import.meta.url));
|
|
33
|
+
const VENDOR_PRESET_DIR = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset", import.meta.url));
|
|
31
34
|
const Config = z.object({
|
|
32
35
|
text: z.string().default(""),
|
|
33
36
|
agentsText: z.string().default(""),
|
|
@@ -37,7 +40,12 @@ const Config = z.object({
|
|
|
37
40
|
injectPrompt: z.boolean().default(true),
|
|
38
41
|
skillSwitches: z.dict(z.boolean()).default({}),
|
|
39
42
|
anchorFirstTurn: z.boolean().default(false),
|
|
40
|
-
anchorText: z.string().default("
|
|
43
|
+
anchorText: z.string().default(""),
|
|
44
|
+
anchorCustom: z.boolean().default(false),
|
|
45
|
+
subagentFlash: z.boolean().default(false),
|
|
46
|
+
subagentFlashProvider: z.string().default("deepseek-official"),
|
|
47
|
+
subagentFlashModel: z.string().default("deepseek-v4-flash"),
|
|
48
|
+
customBashPath: z.string().default("bash.exe"),
|
|
41
49
|
skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
|
|
42
50
|
skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
|
|
43
51
|
residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
|
|
@@ -52,7 +60,10 @@ const PromptSettingsSchema = z.object({
|
|
|
52
60
|
agentsPath: z.string().default(""),
|
|
53
61
|
injectAgentsPrompt: z.boolean().default(false),
|
|
54
62
|
anchorFirstTurn: z.boolean().default(false),
|
|
55
|
-
anchorText: z.string().default("
|
|
63
|
+
anchorText: z.string().default(""),
|
|
64
|
+
anchorCustom: z.boolean().default(false),
|
|
65
|
+
subagentFlash: z.boolean().default(false),
|
|
66
|
+
deepseekAvailable: z.boolean().default(true),
|
|
56
67
|
injectPrompt: z.boolean().default(true),
|
|
57
68
|
skillSwitches: z.dict(z.boolean()).default({}),
|
|
58
69
|
skillCatalog: z.array(z.object({
|
|
@@ -271,10 +282,12 @@ async function readBridgeBody(req) {
|
|
|
271
282
|
/** dsh-tui 暴露的布尔开关:键名与 settings 路径一致。 */
|
|
272
283
|
const TUI_BOOLEAN_SWITCHES = [
|
|
273
284
|
["writeAgents", "写入常驻规则 AGENTS.md"],
|
|
274
|
-
["writePreset", "
|
|
285
|
+
["writePreset", "启用锚定预设"],
|
|
275
286
|
["injectPrompt", "锚定确认后注入 preset.md"],
|
|
276
287
|
["injectAgentsPrompt", "用 AGENTS.md 替换 instruction-hint 提示"],
|
|
277
|
-
["anchorFirstTurn", "
|
|
288
|
+
["anchorFirstTurn", "追加任务引导"],
|
|
289
|
+
["anchorCustom", "使用自定义引导"],
|
|
290
|
+
["subagentFlash", "子代理固定 Flash 模型"]
|
|
278
291
|
];
|
|
279
292
|
/** 把布尔开关渲染成 dsh-tui 命令输出。 */
|
|
280
293
|
function renderTuiStatus(source) {
|
|
@@ -285,6 +298,9 @@ function renderTuiStatus(source) {
|
|
|
285
298
|
const value = source[key];
|
|
286
299
|
return `${key.padEnd(22)}${onOff(typeof value === "boolean" ? value : false)} ${label}`;
|
|
287
300
|
}),
|
|
301
|
+
"锚点文本:",
|
|
302
|
+
` anchorText ${source.anchorText.length > 0 ? source.anchorText : "(空 = 按任务自动选择)"}`,
|
|
303
|
+
` deepseekAvailable ${source.deepseekAvailable ? "是" : "否(未检测到 DeepSeek 模型,subagentFlash 不可用)"}`,
|
|
288
304
|
"技能开关:"
|
|
289
305
|
];
|
|
290
306
|
for (const skill of source.skillCatalog) {
|
|
@@ -300,7 +316,7 @@ function parseTuiBoolean(token, current) {
|
|
|
300
316
|
if (token === "toggle") return !current;
|
|
301
317
|
}
|
|
302
318
|
/** 通过 DSH 命令注册表暴露 /prompt-tool,Web 与 dsh-tui 都能执行。 */
|
|
303
|
-
function registerTuiCommand(ctx, getSource) {
|
|
319
|
+
function registerTuiCommand(ctx, getSource, getDeepseekAvailable, getDeepseekState) {
|
|
304
320
|
ctx.inject(["settings"], (sctx) => {
|
|
305
321
|
return sctx.commands.register({
|
|
306
322
|
name: "prompt-tool",
|
|
@@ -309,14 +325,18 @@ function registerTuiCommand(ctx, getSource) {
|
|
|
309
325
|
handler: async (invocation) => {
|
|
310
326
|
const usage = () => ({
|
|
311
327
|
kind: "error",
|
|
312
|
-
text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn>\n /prompt-tool skill <技能目录名> on|off|toggle"
|
|
328
|
+
text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|subagentFlash>\n /prompt-tool skill <技能目录名> on|off|toggle"
|
|
313
329
|
});
|
|
314
330
|
const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
|
|
315
331
|
const source = getSource();
|
|
316
|
-
if (tokens.length === 0 || tokens[0] === "status")
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
332
|
+
if (tokens.length === 0 || tokens[0] === "status") {
|
|
333
|
+
const detection = getDeepseekState();
|
|
334
|
+
const deepseekLine = detection.available ? `检测到的 DeepSeek 模型路由: ${detection.providers.join(", ") || "(无)"}` : `未检测到 DeepSeek 模型路由。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`;
|
|
335
|
+
return {
|
|
336
|
+
kind: "success",
|
|
337
|
+
text: renderTuiStatus(source) + "\n" + deepseekLine
|
|
338
|
+
};
|
|
339
|
+
}
|
|
320
340
|
if (tokens[0] === "skill") {
|
|
321
341
|
const folder = tokens[1];
|
|
322
342
|
if (folder === void 0) return usage();
|
|
@@ -339,6 +359,13 @@ ${renderTuiStatus(getSource())}`
|
|
|
339
359
|
const key = tokens[1];
|
|
340
360
|
if (action !== "on" && action !== "off" && action !== "toggle") return usage();
|
|
341
361
|
if (key === void 0 || !TUI_BOOLEAN_SWITCHES.some(([candidate]) => candidate === key)) return usage();
|
|
362
|
+
if (key === "subagentFlash" && !getDeepseekAvailable()) {
|
|
363
|
+
const detection = getDeepseekState();
|
|
364
|
+
return {
|
|
365
|
+
kind: "error",
|
|
366
|
+
text: `未检测到 DeepSeek 模型路由,subagentFlash 开关不可用。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
342
369
|
const currentValue = source[key];
|
|
343
370
|
if (typeof currentValue !== "boolean") return {
|
|
344
371
|
kind: "error",
|
|
@@ -362,7 +389,7 @@ ${renderTuiStatus(getSource())}`
|
|
|
362
389
|
});
|
|
363
390
|
}
|
|
364
391
|
/** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
|
|
365
|
-
function registerSettingsBridge(ctx) {
|
|
392
|
+
function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
|
|
366
393
|
ctx.inject(["settings"], (sctx) => {
|
|
367
394
|
sctx.effect(() => {
|
|
368
395
|
const findDescriptor = () => sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
|
|
@@ -400,9 +427,13 @@ function registerSettingsBridge(ctx) {
|
|
|
400
427
|
});
|
|
401
428
|
return;
|
|
402
429
|
}
|
|
430
|
+
const detection = getDeepseekState();
|
|
403
431
|
writeBridgeJson(res, 200, {
|
|
404
432
|
ok: true,
|
|
405
|
-
value: descriptor
|
|
433
|
+
value: descriptor,
|
|
434
|
+
deepseekAvailable: detection.available,
|
|
435
|
+
deepseekProviders: detection.providers,
|
|
436
|
+
deepseekError: detection.error
|
|
406
437
|
});
|
|
407
438
|
}
|
|
408
439
|
}),
|
|
@@ -516,19 +547,125 @@ function registerSettingsBridge(ctx) {
|
|
|
516
547
|
}, "prompt-tool: settings bridge");
|
|
517
548
|
});
|
|
518
549
|
}
|
|
550
|
+
/** 旧版“每块强制 we need”默认锚句;已存 settings 时归一化为自动模式。 */
|
|
551
|
+
const LEGACY_ANCHOR_TEXT = [
|
|
552
|
+
"You are a helpful software assistant.",
|
|
553
|
+
"",
|
|
554
|
+
"Begin every reasoning block with 'We need'."
|
|
555
|
+
].join("\n");
|
|
556
|
+
/** 旧默认值归一化为空(自动);用户自定义文本原样保留。 */
|
|
557
|
+
function normalizeAnchorText(text) {
|
|
558
|
+
const value = typeof text === "string" ? text : "";
|
|
559
|
+
return value.trim() === LEGACY_ANCHOR_TEXT.trim() ? "" : value;
|
|
560
|
+
}
|
|
561
|
+
/** 检测 DeepSeek 模型:live provider + 可配置 provider 目录双通道匹配。 */
|
|
562
|
+
function detectDeepseek(ctx) {
|
|
563
|
+
const empty = {
|
|
564
|
+
available: false,
|
|
565
|
+
providers: []
|
|
566
|
+
};
|
|
567
|
+
try {
|
|
568
|
+
const llm = ctx.get("llm");
|
|
569
|
+
if (llm === void 0) return {
|
|
570
|
+
...empty,
|
|
571
|
+
error: "ctx.get(\"llm\") 返回 undefined"
|
|
572
|
+
};
|
|
573
|
+
const live = llm.listProviders?.() ?? [];
|
|
574
|
+
const configured = llm.listConfigurableProviders?.() ?? [];
|
|
575
|
+
const names = /* @__PURE__ */ new Set();
|
|
576
|
+
const matches = (id, name) => /deepseek/i.test(id ?? "") || /deepseek/i.test(name ?? "");
|
|
577
|
+
for (const provider of live) {
|
|
578
|
+
const id = typeof provider.id === "string" ? provider.id : String(provider.id ?? "");
|
|
579
|
+
names.add(id || provider.name || "(unnamed)");
|
|
580
|
+
if (matches(provider.id, provider.name)) return {
|
|
581
|
+
available: true,
|
|
582
|
+
providers: [...names]
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
for (const provider of configured) {
|
|
586
|
+
const id = typeof provider.provider === "string" ? provider.provider : "";
|
|
587
|
+
if (id.length > 0) names.add(id);
|
|
588
|
+
if (matches(provider.provider, provider.displayName)) return {
|
|
589
|
+
available: true,
|
|
590
|
+
providers: [...names]
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
available: false,
|
|
595
|
+
providers: [...names],
|
|
596
|
+
...live.length === 0 && configured.length === 0 ? { error: "llm 服务未返回任何 provider" } : {}
|
|
597
|
+
};
|
|
598
|
+
} catch (error) {
|
|
599
|
+
return {
|
|
600
|
+
...empty,
|
|
601
|
+
error: error instanceof Error ? error.message : String(error)
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/** 给宿主直派的子代理补 Flash 路由;调用方显式 provider/model 优先,不覆盖 persona 与工具白名单。 */
|
|
606
|
+
function installSubagentFlashRoute(ctx, isEnabled, provider, model) {
|
|
607
|
+
ctx.inject(["subagents"], (sctx) => {
|
|
608
|
+
const service = sctx.get("subagents");
|
|
609
|
+
if (service === void 0 || typeof service.start !== "function") return;
|
|
610
|
+
const original = service.start;
|
|
611
|
+
const wrapped = (name, request) => {
|
|
612
|
+
if (!isEnabled() || request === null || typeof request !== "object") return original.call(service, name, request);
|
|
613
|
+
const agentOptions = request.agentOptions !== null && typeof request.agentOptions === "object" ? request.agentOptions : {};
|
|
614
|
+
if (agentOptions.provider === void 0 && agentOptions.model === void 0) return original.call(service, name, {
|
|
615
|
+
...request,
|
|
616
|
+
agentOptions: {
|
|
617
|
+
...agentOptions,
|
|
618
|
+
provider,
|
|
619
|
+
model
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
return original.call(service, name, request);
|
|
623
|
+
};
|
|
624
|
+
service.start = wrapped;
|
|
625
|
+
return () => {
|
|
626
|
+
if (service.start === wrapped) service.start = original;
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/** prompt-tool 补丁:子代理直接全量放行(assembled.tools 本身已是动态白名单)。 */
|
|
631
|
+
function patchToolBootstrap(source) {
|
|
632
|
+
source = source.replace(/\r\n/g, "\n");
|
|
633
|
+
const original = [
|
|
634
|
+
" const assembled = await next()",
|
|
635
|
+
" try {",
|
|
636
|
+
" const status = promotion.status(context.agent)"
|
|
637
|
+
].join("\n");
|
|
638
|
+
const replacement = [
|
|
639
|
+
" const assembled = await next()",
|
|
640
|
+
" try {",
|
|
641
|
+
" // prompt-tool 补丁:子代理跳过目录裁剪,直接使用组装结果;",
|
|
642
|
+
" // 调用方(如 dsh-mnemon)的工具白名单已先行过滤 assembled.tools,",
|
|
643
|
+
" // 因此任意前缀的新插件工具都会自动出现在子代理第一次会话。",
|
|
644
|
+
" if ((context.agent?.session?.header?.delegationDepth ?? 0) > 0) return assembled",
|
|
645
|
+
" const status = promotion.status(context.agent)"
|
|
646
|
+
].join("\n");
|
|
647
|
+
if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
|
|
648
|
+
return source.replace(original, replacement);
|
|
649
|
+
}
|
|
519
650
|
function writePreset(prompt, options) {
|
|
520
651
|
const presetDir = options.presetDir;
|
|
521
652
|
mkdirSync(presetDir, { recursive: true });
|
|
522
653
|
writeFileSync(join(presetDir, "agent.cordis.yml"), buildCordis(prompt, {
|
|
523
654
|
anchorFirstTurn: options.anchorFirstTurn,
|
|
524
655
|
anchorText: options.anchorText,
|
|
525
|
-
|
|
656
|
+
anchorCustom: options.anchorCustom,
|
|
657
|
+
injectPrompt: options.injectPrompt,
|
|
658
|
+
subagentFlash: options.subagentFlash,
|
|
659
|
+
subagentFlashProvider: options.subagentFlashProvider,
|
|
660
|
+
subagentFlashModel: options.subagentFlashModel,
|
|
661
|
+
bashPath: options.customBashPath
|
|
526
662
|
}), "utf8");
|
|
527
663
|
const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
|
|
528
664
|
writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
|
|
529
665
|
for (const file of readdirSync(VENDOR_PRESET_DIR)) {
|
|
530
666
|
if (!file.endsWith(".mjs")) continue;
|
|
531
|
-
|
|
667
|
+
const vendorSource = readFileSync(join(VENDOR_PRESET_DIR, file), "utf8");
|
|
668
|
+
writeFileSync(join(presetDir, file), file === "tool-bootstrap.mjs" ? patchToolBootstrap(vendorSource) : vendorSource, "utf8");
|
|
532
669
|
}
|
|
533
670
|
const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
|
|
534
671
|
if (options.agentsInstructionText !== void 0) {
|
|
@@ -539,11 +676,16 @@ function writePreset(prompt, options) {
|
|
|
539
676
|
const injectorPath = join(presetDir, "prompt-injector.mjs");
|
|
540
677
|
if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
|
|
541
678
|
else rmSync(injectorPath, { force: true });
|
|
542
|
-
|
|
679
|
+
writeFileSync(join(presetDir, "router-first-turn.mjs"), readFileSync(PRESET_TEMPLATE_ROUTER, "utf8"), "utf8");
|
|
680
|
+
rmSync(join(presetDir, "turn-anchor.mjs"), { force: true });
|
|
681
|
+
const anchorPath = join(presetDir, "near-anchor.mjs");
|
|
543
682
|
if (options.anchorFirstTurn) writeFileSync(anchorPath, readFileSync(PRESET_TEMPLATE_ANCHOR, "utf8"), "utf8");
|
|
544
683
|
else rmSync(anchorPath, { force: true });
|
|
545
684
|
}
|
|
546
685
|
function apply(ctx, config) {
|
|
686
|
+
const deepseekState = () => detectDeepseek(ctx);
|
|
687
|
+
const getDeepseekAvailable = () => deepseekState().available;
|
|
688
|
+
const getDeepseekState = () => deepseekState();
|
|
547
689
|
let current = config.text || readPromptFile(config.fallbackText);
|
|
548
690
|
let currentAgents = config.agentsText || readAgents();
|
|
549
691
|
const skillCatalog = readSkills(config.skillsDir).map((skill) => ({
|
|
@@ -598,7 +740,7 @@ function apply(ctx, config) {
|
|
|
598
740
|
}
|
|
599
741
|
};
|
|
600
742
|
});
|
|
601
|
-
registerSettingsBridge(ctx);
|
|
743
|
+
registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState);
|
|
602
744
|
const runtime = {
|
|
603
745
|
writeAgents: config.writeAgents,
|
|
604
746
|
writePreset: config.writePreset,
|
|
@@ -606,8 +748,11 @@ function apply(ctx, config) {
|
|
|
606
748
|
injectPrompt: config.injectPrompt,
|
|
607
749
|
skillSwitches: { ...config.skillSwitches },
|
|
608
750
|
anchorFirstTurn: config.anchorFirstTurn,
|
|
609
|
-
anchorText: config.anchorText
|
|
751
|
+
anchorText: config.anchorText,
|
|
752
|
+
anchorCustom: config.anchorCustom,
|
|
753
|
+
subagentFlash: config.subagentFlash && getDeepseekAvailable()
|
|
610
754
|
};
|
|
755
|
+
installSubagentFlashRoute(ctx, () => runtime.subagentFlash, config.subagentFlashProvider, config.subagentFlashModel);
|
|
611
756
|
let currentSource = () => ({
|
|
612
757
|
promptText: current,
|
|
613
758
|
promptPath: PRESET_FILE_PATH,
|
|
@@ -615,14 +760,17 @@ function apply(ctx, config) {
|
|
|
615
760
|
agentsPath: AGENTS_FILE_PATH,
|
|
616
761
|
injectAgentsPrompt: runtime.injectAgentsPrompt,
|
|
617
762
|
anchorFirstTurn: runtime.anchorFirstTurn,
|
|
618
|
-
anchorText: runtime.anchorText,
|
|
763
|
+
anchorText: normalizeAnchorText(runtime.anchorText),
|
|
764
|
+
anchorCustom: runtime.anchorCustom,
|
|
765
|
+
subagentFlash: runtime.subagentFlash,
|
|
766
|
+
deepseekAvailable: getDeepseekAvailable(),
|
|
619
767
|
injectPrompt: runtime.injectPrompt,
|
|
620
768
|
skillSwitches: runtime.skillSwitches,
|
|
621
769
|
skillCatalog,
|
|
622
770
|
writeAgents: runtime.writeAgents,
|
|
623
771
|
writePreset: runtime.writePreset
|
|
624
772
|
});
|
|
625
|
-
registerTuiCommand(ctx, () => currentSource());
|
|
773
|
+
registerTuiCommand(ctx, () => currentSource(), getDeepseekAvailable, getDeepseekState);
|
|
626
774
|
let needsInitialApply = true;
|
|
627
775
|
const applyState = () => {
|
|
628
776
|
const next = currentSource();
|
|
@@ -633,12 +781,14 @@ function apply(ctx, config) {
|
|
|
633
781
|
injectPrompt: typeof next.injectPrompt === "boolean" ? next.injectPrompt : config.injectPrompt,
|
|
634
782
|
skillSwitches: next.skillSwitches !== void 0 ? next.skillSwitches : config.skillSwitches,
|
|
635
783
|
anchorFirstTurn: typeof next.anchorFirstTurn === "boolean" ? next.anchorFirstTurn : config.anchorFirstTurn,
|
|
636
|
-
anchorText: typeof next.anchorText === "string"
|
|
784
|
+
anchorText: normalizeAnchorText(typeof next.anchorText === "string" ? next.anchorText : config.anchorText),
|
|
785
|
+
anchorCustom: typeof next.anchorCustom === "boolean" ? next.anchorCustom : config.anchorCustom,
|
|
786
|
+
subagentFlash: (typeof next.subagentFlash === "boolean" ? next.subagentFlash : config.subagentFlash) && getDeepseekAvailable()
|
|
637
787
|
};
|
|
638
788
|
const promptChanged = next.promptText !== current;
|
|
639
789
|
const agentsChanged = next.agentsText !== currentAgents;
|
|
640
790
|
const skillSwitchesChanged = JSON.stringify(runtime.skillSwitches) !== JSON.stringify(nextRuntime.skillSwitches);
|
|
641
|
-
const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText;
|
|
791
|
+
const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText || runtime.anchorCustom !== nextRuntime.anchorCustom || runtime.subagentFlash !== nextRuntime.subagentFlash;
|
|
642
792
|
if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
|
|
643
793
|
needsInitialApply = false;
|
|
644
794
|
if (promptChanged) current = next.promptText;
|
|
@@ -650,6 +800,8 @@ function apply(ctx, config) {
|
|
|
650
800
|
runtime.skillSwitches = nextRuntime.skillSwitches;
|
|
651
801
|
runtime.anchorFirstTurn = nextRuntime.anchorFirstTurn;
|
|
652
802
|
runtime.anchorText = nextRuntime.anchorText;
|
|
803
|
+
runtime.anchorCustom = nextRuntime.anchorCustom;
|
|
804
|
+
runtime.subagentFlash = nextRuntime.subagentFlash;
|
|
653
805
|
skillSwitches = runtime.skillSwitches;
|
|
654
806
|
if (skillSwitchesChanged) invalidateSkills?.();
|
|
655
807
|
let residentAgentsWritten = false;
|
|
@@ -663,7 +815,12 @@ function apply(ctx, config) {
|
|
|
663
815
|
if (runtime.writePreset) writePreset(runtime.injectPrompt && current.length > 0 ? current : "", {
|
|
664
816
|
anchorFirstTurn: runtime.anchorFirstTurn,
|
|
665
817
|
anchorText: runtime.anchorText,
|
|
818
|
+
anchorCustom: runtime.anchorCustom,
|
|
666
819
|
injectPrompt: runtime.injectPrompt,
|
|
820
|
+
subagentFlash: runtime.subagentFlash,
|
|
821
|
+
subagentFlashProvider: config.subagentFlashProvider,
|
|
822
|
+
subagentFlashModel: config.subagentFlashModel,
|
|
823
|
+
customBashPath: config.customBashPath,
|
|
667
824
|
agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
|
|
668
825
|
presetDir: config.presetDir,
|
|
669
826
|
presetOrder: config.presetOrder
|
|
@@ -685,6 +842,9 @@ function apply(ctx, config) {
|
|
|
685
842
|
injectAgentsPrompt: config.injectAgentsPrompt,
|
|
686
843
|
anchorFirstTurn: config.anchorFirstTurn,
|
|
687
844
|
anchorText: config.anchorText,
|
|
845
|
+
anchorCustom: config.anchorCustom,
|
|
846
|
+
subagentFlash: config.subagentFlash && getDeepseekAvailable(),
|
|
847
|
+
deepseekAvailable: getDeepseekAvailable(),
|
|
688
848
|
injectPrompt: config.injectPrompt,
|
|
689
849
|
skillSwitches: { ...config.skillSwitches },
|
|
690
850
|
skillCatalog,
|
package/lib/preset-core.d.mts
CHANGED
|
@@ -10,11 +10,22 @@ declare function parseFrontmatter(text: string): {
|
|
|
10
10
|
body: string;
|
|
11
11
|
};
|
|
12
12
|
interface BuildCordisOptions {
|
|
13
|
-
/**
|
|
13
|
+
/** 首轮近距离锚定:首条真实用户消息后追加一次性首句锚点。 */
|
|
14
14
|
anchorFirstTurn?: boolean;
|
|
15
|
+
/** 自定义锚点文本;anchorCustom=true 时固定使用。 */
|
|
15
16
|
anchorText?: string;
|
|
17
|
+
/** 自定义锚点开关:true 固定使用 anchorText;false 按任务自动选择。 */
|
|
18
|
+
anchorCustom?: boolean;
|
|
16
19
|
/** 锚定确认后注入 preset.md;关闭时仍保留工具引导,但不注册 prompt-injector 行。 */
|
|
17
20
|
injectPrompt?: boolean;
|
|
21
|
+
/** 子代理固定 Flash 模型:true 时给 subagent/subagent_fork 行加 agentOptions。 */
|
|
22
|
+
subagentFlash?: boolean;
|
|
23
|
+
/** 子代理 Flash 路由 provider(默认 deepseek-official)。 */
|
|
24
|
+
subagentFlashProvider?: string;
|
|
25
|
+
/** 子代理 Flash 模型名(默认 deepseek-v4-flash)。 */
|
|
26
|
+
subagentFlashModel?: string;
|
|
27
|
+
/** Windows custom-bash 可执行名或路径;默认 bash.exe(PATH 查找)。 */
|
|
28
|
+
bashPath?: string;
|
|
18
29
|
}
|
|
19
30
|
declare function buildCordis(prompt: string, options?: BuildCordisOptions): string;
|
|
20
31
|
//#endregion
|
package/lib/preset-core.mjs
CHANGED
|
@@ -6,26 +6,32 @@ import { parse } from "yaml";
|
|
|
6
6
|
* prompt-tool preset 生成核心:与 dsh 插件上下文无关的纯函数,
|
|
7
7
|
* 独立成文件以便单元测试与复用。失败一律抛出(fail loud)。
|
|
8
8
|
*/
|
|
9
|
-
const VENDOR_CORDIS = fileURLToPath(new URL("../
|
|
10
|
-
const
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
9
|
+
const VENDOR_CORDIS = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset/agent.cordis.yml", import.meta.url));
|
|
10
|
+
const ROUTER_FIRST_TURN_BLOCK = `# prompt-tool 本地附加件(最优组合):首轮只保留计划段 + 训练原句,
|
|
11
|
+
# 并清空自动注入 contexts;子代理直接放行。注册在 tool-bootstrap 之后,
|
|
12
|
+
# 不参与首轮工具目录裁剪——tools 仍由原版 tool-bootstrap 负责。
|
|
13
|
+
- id: router-first-turn
|
|
14
|
+
name: ./router-first-turn.mjs
|
|
15
|
+
`;
|
|
16
|
+
const NEAR_ANCHOR_BLOCK = `# prompt-tool 近距离首句锚定:在首条真实用户消息之后追加一次性锚点。
|
|
17
|
+
# 不拆轮、不挪任务;useCustom=false 时按任务自动选择 we/let 首句,
|
|
18
|
+
# useCustom=true 时固定使用 anchorText。
|
|
19
|
+
- id: near-anchor
|
|
20
|
+
name: ./near-anchor.mjs
|
|
21
|
+
config:
|
|
22
|
+
anchorText: |-
|
|
23
|
+
__ANCHOR_TEXT__
|
|
24
|
+
useCustom: __USE_CUSTOM__
|
|
25
|
+
`;
|
|
26
|
+
const LOCAL_INJECTOR_BLOCK = `# prompt-tool 附加件:锚定确认后注入 preset.md。注册在工具引导之后,
|
|
27
|
+
# 不参与首轮剥离顺序;tools / 上下文剥离由 tool-bootstrap 与
|
|
28
|
+
# router-first-turn 负责(首轮 = Minimal 真实 schema + 训练原句)。
|
|
14
29
|
- id: prompt-injector
|
|
15
30
|
name: ./prompt-injector.mjs
|
|
16
31
|
config:
|
|
17
32
|
promptText: |-
|
|
18
33
|
__PROMPT_TOOL_TEXT__
|
|
19
34
|
`;
|
|
20
|
-
const TURN_ANCHOR_BLOCK = `# prompt-tool 可选附加件:首轮独立锚定轮。启用后首个真实用户消息先入
|
|
21
|
-
# next-step inbox,首步只发 anchorText;模型回应锚定句后,driver 在同轮
|
|
22
|
-
# 内自动消费任务继续执行。关闭时不生成此行。
|
|
23
|
-
- id: turn-anchor
|
|
24
|
-
name: ./turn-anchor.mjs
|
|
25
|
-
config:
|
|
26
|
-
anchorText: |-
|
|
27
|
-
__ANCHOR_TEXT__
|
|
28
|
-
`;
|
|
29
35
|
function parseFrontmatter(text) {
|
|
30
36
|
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
31
37
|
if (!match) return {
|
|
@@ -45,12 +51,76 @@ function parseFrontmatter(text) {
|
|
|
45
51
|
body: text.slice(match[0].length)
|
|
46
52
|
};
|
|
47
53
|
}
|
|
54
|
+
/** dsh-router-standard 的 Flash 弱路由 persona(子代理版)。 */
|
|
55
|
+
const ROUTER_FLASH_PERSONA = [
|
|
56
|
+
"You are a helpful assistant.",
|
|
57
|
+
"Before acting, decide the task type (build or fix) and adopt the matching style: build → hands-on production; fix → inspect-and-plan.",
|
|
58
|
+
"Before acting, briefly review what you have already done in this session and continue from where you left off; do not repeat completed steps. Do not run environment checks (echo, whoami, uname, node --version, date) or exhaustive grep/glob scans.",
|
|
59
|
+
"Think deeply first, then produce."
|
|
60
|
+
].join("\n");
|
|
61
|
+
/** 适配上游 context-gate:保留 near-anchor 消息;子代理沿用全量放行策略。 */
|
|
62
|
+
function normalizeContextGate(source) {
|
|
63
|
+
const block = `- id: context-gate
|
|
64
|
+
name: ./context-gate.mjs
|
|
65
|
+
config:
|
|
66
|
+
promoteOn: either
|
|
67
|
+
includeSubagents: true
|
|
68
|
+
allowKinds: [skill-invocation]`;
|
|
69
|
+
const replaced = `- id: context-gate
|
|
70
|
+
name: ./context-gate.mjs
|
|
71
|
+
config:
|
|
72
|
+
promoteOn: either
|
|
73
|
+
includeSubagents: false
|
|
74
|
+
allowKinds: [skill-invocation, near-anchor]`;
|
|
75
|
+
if (!source.includes(block)) throw new Error("vendor agent.cordis.yml missing context-gate row");
|
|
76
|
+
return source.replace(block, replaced);
|
|
77
|
+
}
|
|
78
|
+
/** 上游可能携带作者机器固定路径;生成时统一改写为可配置 bashPath。 */
|
|
79
|
+
function normalizeBashPath(source, bashPath) {
|
|
80
|
+
const marker = /bashPath:\s*'[^']*'/.exec(source);
|
|
81
|
+
if (marker === null) throw new Error("vendor agent.cordis.yml missing bashPath row");
|
|
82
|
+
return source.replace(marker[0], `bashPath: '${bashPath}'`);
|
|
83
|
+
}
|
|
84
|
+
/** 给 subagent/subagent_fork 行加 dsh-router-standard 的 Flash 子代理方案;关闭时保持继承主会话路由。 */
|
|
85
|
+
function applySubagentFlash(source, enabled, provider, model) {
|
|
86
|
+
if (!enabled) return source;
|
|
87
|
+
const agentOptions = ` agentOptions:
|
|
88
|
+
provider: ${provider}
|
|
89
|
+
model: ${model}`;
|
|
90
|
+
const persona = ROUTER_FLASH_PERSONA.split("\n").map((line) => ` ${line}`).join("\n");
|
|
91
|
+
const targets = [{
|
|
92
|
+
id: "tool-subagent",
|
|
93
|
+
toolName: "subagent",
|
|
94
|
+
provider: "spawn"
|
|
95
|
+
}, {
|
|
96
|
+
id: "tool-subagent-fork",
|
|
97
|
+
toolName: "subagent_fork",
|
|
98
|
+
provider: "fork"
|
|
99
|
+
}];
|
|
100
|
+
let out = source;
|
|
101
|
+
for (const target of targets) {
|
|
102
|
+
const block = ` - id: ${target.id}
|
|
103
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
104
|
+
config:
|
|
105
|
+
provider: ${target.provider}
|
|
106
|
+
toolName: ${target.toolName}
|
|
107
|
+
backgroundMode: continuable`;
|
|
108
|
+
const replaced = `${block}
|
|
109
|
+
${agentOptions}
|
|
110
|
+
persona: |-
|
|
111
|
+
${persona}`;
|
|
112
|
+
if (!out.includes(block)) throw new Error(`vendor agent.cordis.yml missing ${target.id} row`);
|
|
113
|
+
out = out.replace(block, replaced);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
48
117
|
function buildCordis(prompt, options = {}) {
|
|
49
118
|
const anchorFirstTurn = options.anchorFirstTurn === true;
|
|
119
|
+
const anchorCustom = options.anchorCustom === true;
|
|
50
120
|
const injectPrompt = options.injectPrompt !== false;
|
|
51
|
-
const anchorText = typeof options.anchorText === "string" && options.anchorText.length > 0 ? options.anchorText : "
|
|
121
|
+
const anchorText = typeof options.anchorText === "string" && options.anchorText.length > 0 ? options.anchorText : "";
|
|
52
122
|
const indent = (s) => s.split(/\r?\n/).map((l) => l.length === 0 ? "" : " " + l).join("\n");
|
|
53
|
-
const up = readFileSync(VENDOR_CORDIS, "utf8").replace(/\r\n/g, "\n");
|
|
123
|
+
const up = normalizeContextGate(normalizeBashPath(applySubagentFlash(readFileSync(VENDOR_CORDIS, "utf8").replace(/\r\n/g, "\n"), options.subagentFlash === true, typeof options.subagentFlashProvider === "string" && options.subagentFlashProvider.length > 0 ? options.subagentFlashProvider : "deepseek-official", typeof options.subagentFlashModel === "string" && options.subagentFlashModel.length > 0 ? options.subagentFlashModel : "deepseek-v4-flash"), typeof options.bashPath === "string" && options.bashPath.length > 0 ? options.bashPath : "bash.exe"));
|
|
54
124
|
const bootstrap = /^-\s+id:\s+tool-bootstrap\s*$/m.exec(up);
|
|
55
125
|
if (!bootstrap) throw new Error("vendor agent.cordis.yml missing tool-bootstrap row");
|
|
56
126
|
let cursor = up.indexOf("\n", bootstrap.index);
|
|
@@ -71,11 +141,12 @@ function buildCordis(prompt, options = {}) {
|
|
|
71
141
|
const head = up.slice(0, insertAt);
|
|
72
142
|
const tail = up.slice(insertAt);
|
|
73
143
|
const separator = head.endsWith("\n\n") ? "" : head.endsWith("\n") ? "\n" : "\n\n";
|
|
74
|
-
const parts = [];
|
|
144
|
+
const parts = [ROUTER_FIRST_TURN_BLOCK];
|
|
75
145
|
if (anchorFirstTurn) {
|
|
76
146
|
const anchorMarker = " anchorText: |-\n __ANCHOR_TEXT__";
|
|
77
|
-
if (!
|
|
78
|
-
|
|
147
|
+
if (!NEAR_ANCHOR_BLOCK.includes(anchorMarker)) throw new Error("internal error: near-anchor template lost its anchorText placeholder");
|
|
148
|
+
const anchorBlock = NEAR_ANCHOR_BLOCK.replace(" useCustom: __USE_CUSTOM__", ` useCustom: ${anchorCustom}`).replace(anchorMarker, " anchorText: |-\n" + (anchorText.length > 0 ? indent(anchorText) : " "));
|
|
149
|
+
parts.push(anchorBlock);
|
|
79
150
|
}
|
|
80
151
|
if (injectPrompt) {
|
|
81
152
|
const promptMarker = " promptText: |-\n __PROMPT_TOOL_TEXT__";
|
|
@@ -84,7 +155,7 @@ function buildCordis(prompt, options = {}) {
|
|
|
84
155
|
}
|
|
85
156
|
const extra = parts.join("\n");
|
|
86
157
|
const out = head + separator + extra + "\n" + tail;
|
|
87
|
-
if (injectPrompt && out.includes("__PROMPT_TOOL_TEXT__") || anchorFirstTurn && out.includes("__ANCHOR_TEXT__")) throw new Error("internal error: preset template placeholder was not replaced");
|
|
158
|
+
if (injectPrompt && out.includes("__PROMPT_TOOL_TEXT__") || anchorFirstTurn && (out.includes("__ANCHOR_TEXT__") || out.includes("__USE_CUSTOM__"))) throw new Error("internal error: preset template placeholder was not replaced");
|
|
88
159
|
let parsed;
|
|
89
160
|
try {
|
|
90
161
|
parsed = parse(out, { logLevel: "silent" });
|
|
@@ -93,9 +164,11 @@ function buildCordis(prompt, options = {}) {
|
|
|
93
164
|
}
|
|
94
165
|
if (!Array.isArray(parsed)) throw new Error("generated agent.cordis.yml is not a YAML array");
|
|
95
166
|
const ids = new Set(parsed.map((row) => row?.id));
|
|
167
|
+
if (!ids.has("router-first-turn")) throw new Error("generated agent.cordis.yml is missing the router-first-turn row");
|
|
96
168
|
if (injectPrompt && !ids.has("prompt-injector")) throw new Error("generated agent.cordis.yml is missing the prompt-injector row");
|
|
97
169
|
if (!injectPrompt && ids.has("prompt-injector")) throw new Error("generated agent.cordis.yml still contains the prompt-injector row");
|
|
98
|
-
if (anchorFirstTurn && !ids.has("
|
|
170
|
+
if (anchorFirstTurn && !ids.has("near-anchor")) throw new Error("generated agent.cordis.yml is missing the near-anchor row");
|
|
171
|
+
if (!anchorFirstTurn && ids.has("near-anchor")) throw new Error("generated agent.cordis.yml still contains the near-anchor row");
|
|
99
172
|
return out;
|
|
100
173
|
}
|
|
101
174
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-prompt-tool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DSH 插件:提示词工具,规范模型的思维链与回答内容,提供 Web UI 编辑 preset.md 与 AGENTS.md,并按 skills 目录注册可开关技能。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -74,21 +74,20 @@
|
|
|
74
74
|
"cordis.patch.yml",
|
|
75
75
|
"LICENSE",
|
|
76
76
|
"preset",
|
|
77
|
-
"vendor/dsh-anchored-standard/preset",
|
|
78
|
-
"vendor/dsh-anchored-standard/LICENSE",
|
|
79
|
-
"vendor/dsh-anchored-standard/NOTICE",
|
|
80
77
|
"skills",
|
|
81
78
|
"preset.md",
|
|
82
79
|
"AGENTS.md",
|
|
83
80
|
"plan.md",
|
|
84
|
-
"README.md"
|
|
81
|
+
"README.md",
|
|
82
|
+
"upstream/dsh-anchored-standard"
|
|
85
83
|
],
|
|
86
84
|
"scripts": {
|
|
87
85
|
"build": "tsdown",
|
|
88
86
|
"prepare": "tsdown",
|
|
89
87
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
90
88
|
"lint": "oxlint src test tsdown.config.ts",
|
|
91
|
-
"test": "pnpm build && node --test \"test/*.test.mjs\""
|
|
89
|
+
"test": "pnpm build && node --test \"test/*.test.mjs\"",
|
|
90
|
+
"sync:anchored": "node scripts/sync-anchored.mjs"
|
|
92
91
|
},
|
|
93
92
|
"dependencies": {
|
|
94
93
|
"yaml": "^2.9.0"
|