dsh-plugin-prompt-tool 0.1.5 → 0.3.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/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);
@@ -24,10 +26,14 @@ const DEFAULT_PRESET_DIR = join(DSH_HOME, ".agent-presets", "prompt-tool");
24
26
  const DEFAULT_SKILLS_DIR = SKILLS_DIR;
25
27
  const DEFAULT_PRESET_ORDER = 5;
26
28
  const DEFAULT_SKILL_RANK_BASE = 250;
29
+ /** 每轮引导文本框的默认内容:写入自动引导的两段文本。 */
30
+ const DEFAULT_GUIDE_TEXT = `简单任务自动引导:${"\nRouter: classify this task (build or fix) now, then adopt the matching style — build: direct production; fix: inspect-first. Think deeply first, then commit and act.".trim()}\n\n复杂任务自动引导:${"\nRouter: classify this task (build or fix) now, then adopt the matching style — build: direct production; fix: inspect-first. Think deeply about the architecture, edge cases, and integration points. Do not spend reasoning on the environment or tooling. Produce when your information is complete. End each reasoning block with a decision or an information need.".trim()}`;
27
31
  const PRESET_TEMPLATE_META = fileURLToPath(new URL("../preset/preset.yml", import.meta.url));
28
32
  const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injector.mjs", import.meta.url));
29
- const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/turn-anchor.mjs", import.meta.url));
30
- const VENDOR_PRESET_DIR = fileURLToPath(new URL("../vendor/dsh-anchored-standard/preset", import.meta.url));
33
+ const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/near-anchor.mjs", import.meta.url));
34
+ const PRESET_TEMPLATE_ROUTER = fileURLToPath(new URL("../preset/router-first-turn.mjs", import.meta.url));
35
+ const PRESET_TEMPLATE_GUIDE = fileURLToPath(new URL("../preset/router-guide.mjs", import.meta.url));
36
+ const VENDOR_PRESET_DIR = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset", import.meta.url));
31
37
  const Config = z.object({
32
38
  text: z.string().default(""),
33
39
  agentsText: z.string().default(""),
@@ -37,7 +43,14 @@ const Config = z.object({
37
43
  injectPrompt: z.boolean().default(true),
38
44
  skillSwitches: z.dict(z.boolean()).default({}),
39
45
  anchorFirstTurn: z.boolean().default(false),
40
- anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
46
+ anchorText: z.string().default(""),
47
+ anchorCustom: z.boolean().default(false),
48
+ guideText: z.string().default(DEFAULT_GUIDE_TEXT),
49
+ guideCustom: z.boolean().default(false),
50
+ subagentFlash: z.boolean().default(false),
51
+ subagentFlashProvider: z.string().default("deepseek-official"),
52
+ subagentFlashModel: z.string().default("deepseek-v4-flash"),
53
+ customBashPath: z.string().default("bash.exe"),
41
54
  skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
42
55
  skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
43
56
  residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
@@ -52,7 +65,12 @@ const PromptSettingsSchema = z.object({
52
65
  agentsPath: z.string().default(""),
53
66
  injectAgentsPrompt: z.boolean().default(false),
54
67
  anchorFirstTurn: z.boolean().default(false),
55
- anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
68
+ anchorText: z.string().default(""),
69
+ anchorCustom: z.boolean().default(false),
70
+ guideText: z.string().default(DEFAULT_GUIDE_TEXT),
71
+ guideCustom: z.boolean().default(false),
72
+ subagentFlash: z.boolean().default(false),
73
+ deepseekAvailable: z.boolean().default(true),
56
74
  injectPrompt: z.boolean().default(true),
57
75
  skillSwitches: z.dict(z.boolean()).default({}),
58
76
  skillCatalog: z.array(z.object({
@@ -271,10 +289,13 @@ async function readBridgeBody(req) {
271
289
  /** dsh-tui 暴露的布尔开关:键名与 settings 路径一致。 */
272
290
  const TUI_BOOLEAN_SWITCHES = [
273
291
  ["writeAgents", "写入常驻规则 AGENTS.md"],
274
- ["writePreset", "生成 prompt-tool 独立 preset"],
292
+ ["writePreset", "启用锚定预设"],
275
293
  ["injectPrompt", "锚定确认后注入 preset.md"],
276
294
  ["injectAgentsPrompt", "用 AGENTS.md 替换 instruction-hint 提示"],
277
- ["anchorFirstTurn", "开启首轮独立锚定轮"]
295
+ ["anchorFirstTurn", "追加任务引导"],
296
+ ["anchorCustom", "使用自定义引导(首句)"],
297
+ ["guideCustom", "使用自定义引导(每轮)"],
298
+ ["subagentFlash", "子代理固定 Flash 模型"]
278
299
  ];
279
300
  /** 把布尔开关渲染成 dsh-tui 命令输出。 */
280
301
  function renderTuiStatus(source) {
@@ -285,6 +306,9 @@ function renderTuiStatus(source) {
285
306
  const value = source[key];
286
307
  return `${key.padEnd(22)}${onOff(typeof value === "boolean" ? value : false)} ${label}`;
287
308
  }),
309
+ "锚点文本:",
310
+ ` anchorText ${source.anchorText.length > 0 ? source.anchorText : "(空 = 按任务自动选择)"}`,
311
+ ` deepseekAvailable ${source.deepseekAvailable ? "是" : "否(未检测到 DeepSeek 模型,subagentFlash 不可用)"}`,
288
312
  "技能开关:"
289
313
  ];
290
314
  for (const skill of source.skillCatalog) {
@@ -300,7 +324,7 @@ function parseTuiBoolean(token, current) {
300
324
  if (token === "toggle") return !current;
301
325
  }
302
326
  /** 通过 DSH 命令注册表暴露 /prompt-tool,Web 与 dsh-tui 都能执行。 */
303
- function registerTuiCommand(ctx, getSource) {
327
+ function registerTuiCommand(ctx, getSource, getDeepseekAvailable, getDeepseekState) {
304
328
  ctx.inject(["settings"], (sctx) => {
305
329
  return sctx.commands.register({
306
330
  name: "prompt-tool",
@@ -309,14 +333,18 @@ function registerTuiCommand(ctx, getSource) {
309
333
  handler: async (invocation) => {
310
334
  const usage = () => ({
311
335
  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"
336
+ text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|guideCustom|subagentFlash>\n /prompt-tool skill <技能目录名> on|off|toggle"
313
337
  });
314
338
  const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
315
339
  const source = getSource();
316
- if (tokens.length === 0 || tokens[0] === "status") return {
317
- kind: "success",
318
- text: renderTuiStatus(source)
319
- };
340
+ if (tokens.length === 0 || tokens[0] === "status") {
341
+ const detection = getDeepseekState();
342
+ const deepseekLine = detection.available ? `检测到的 DeepSeek 模型路由: ${detection.providers.join(", ") || "(无)"}` : `未检测到 DeepSeek 模型路由。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`;
343
+ return {
344
+ kind: "success",
345
+ text: renderTuiStatus(source) + "\n" + deepseekLine
346
+ };
347
+ }
320
348
  if (tokens[0] === "skill") {
321
349
  const folder = tokens[1];
322
350
  if (folder === void 0) return usage();
@@ -339,6 +367,13 @@ ${renderTuiStatus(getSource())}`
339
367
  const key = tokens[1];
340
368
  if (action !== "on" && action !== "off" && action !== "toggle") return usage();
341
369
  if (key === void 0 || !TUI_BOOLEAN_SWITCHES.some(([candidate]) => candidate === key)) return usage();
370
+ if (key === "subagentFlash" && !getDeepseekAvailable()) {
371
+ const detection = getDeepseekState();
372
+ return {
373
+ kind: "error",
374
+ text: `未检测到 DeepSeek 模型路由,subagentFlash 开关不可用。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`
375
+ };
376
+ }
342
377
  const currentValue = source[key];
343
378
  if (typeof currentValue !== "boolean") return {
344
379
  kind: "error",
@@ -362,7 +397,7 @@ ${renderTuiStatus(getSource())}`
362
397
  });
363
398
  }
364
399
  /** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
365
- function registerSettingsBridge(ctx) {
400
+ function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
366
401
  ctx.inject(["settings"], (sctx) => {
367
402
  sctx.effect(() => {
368
403
  const findDescriptor = () => sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
@@ -400,9 +435,13 @@ function registerSettingsBridge(ctx) {
400
435
  });
401
436
  return;
402
437
  }
438
+ const detection = getDeepseekState();
403
439
  writeBridgeJson(res, 200, {
404
440
  ok: true,
405
- value: descriptor
441
+ value: descriptor,
442
+ deepseekAvailable: detection.available,
443
+ deepseekProviders: detection.providers,
444
+ deepseekError: detection.error
406
445
  });
407
446
  }
408
447
  }),
@@ -516,19 +555,127 @@ function registerSettingsBridge(ctx) {
516
555
  }, "prompt-tool: settings bridge");
517
556
  });
518
557
  }
558
+ /** 旧版“每块强制 we need”默认锚句;已存 settings 时归一化为自动模式。 */
559
+ const LEGACY_ANCHOR_TEXT = [
560
+ "You are a helpful software assistant.",
561
+ "",
562
+ "Begin every reasoning block with 'We need'."
563
+ ].join("\n");
564
+ /** 旧默认值归一化为空(自动);用户自定义文本原样保留。 */
565
+ function normalizeAnchorText(text) {
566
+ const value = typeof text === "string" ? text : "";
567
+ return value.trim() === LEGACY_ANCHOR_TEXT.trim() ? "" : value;
568
+ }
569
+ /** 检测 DeepSeek 模型:live provider + 可配置 provider 目录双通道匹配。 */
570
+ function detectDeepseek(ctx) {
571
+ const empty = {
572
+ available: false,
573
+ providers: []
574
+ };
575
+ try {
576
+ const llm = ctx.get("llm");
577
+ if (llm === void 0) return {
578
+ ...empty,
579
+ error: "ctx.get(\"llm\") 返回 undefined"
580
+ };
581
+ const live = llm.listProviders?.() ?? [];
582
+ const configured = llm.listConfigurableProviders?.() ?? [];
583
+ const names = /* @__PURE__ */ new Set();
584
+ const matches = (id, name) => /deepseek/i.test(id ?? "") || /deepseek/i.test(name ?? "");
585
+ for (const provider of live) {
586
+ const id = typeof provider.id === "string" ? provider.id : String(provider.id ?? "");
587
+ names.add(id || provider.name || "(unnamed)");
588
+ if (matches(provider.id, provider.name)) return {
589
+ available: true,
590
+ providers: [...names]
591
+ };
592
+ }
593
+ for (const provider of configured) {
594
+ const id = typeof provider.provider === "string" ? provider.provider : "";
595
+ if (id.length > 0) names.add(id);
596
+ if (matches(provider.provider, provider.displayName)) return {
597
+ available: true,
598
+ providers: [...names]
599
+ };
600
+ }
601
+ return {
602
+ available: false,
603
+ providers: [...names],
604
+ ...live.length === 0 && configured.length === 0 ? { error: "llm 服务未返回任何 provider" } : {}
605
+ };
606
+ } catch (error) {
607
+ return {
608
+ ...empty,
609
+ error: error instanceof Error ? error.message : String(error)
610
+ };
611
+ }
612
+ }
613
+ /** 给宿主直派的子代理补 Flash 路由;调用方显式 provider/model 优先,不覆盖 persona 与工具白名单。 */
614
+ function installSubagentFlashRoute(ctx, isEnabled, provider, model) {
615
+ ctx.inject(["subagents"], (sctx) => {
616
+ const service = sctx.get("subagents");
617
+ if (service === void 0 || typeof service.start !== "function") return;
618
+ const original = service.start;
619
+ const wrapped = (name, request) => {
620
+ if (!isEnabled() || request === null || typeof request !== "object") return original.call(service, name, request);
621
+ const agentOptions = request.agentOptions !== null && typeof request.agentOptions === "object" ? request.agentOptions : {};
622
+ if (agentOptions.provider === void 0 && agentOptions.model === void 0) return original.call(service, name, {
623
+ ...request,
624
+ agentOptions: {
625
+ ...agentOptions,
626
+ provider,
627
+ model
628
+ }
629
+ });
630
+ return original.call(service, name, request);
631
+ };
632
+ service.start = wrapped;
633
+ return () => {
634
+ if (service.start === wrapped) service.start = original;
635
+ };
636
+ });
637
+ }
638
+ /** prompt-tool 补丁:子代理直接全量放行(assembled.tools 本身已是动态白名单)。 */
639
+ function patchToolBootstrap(source) {
640
+ source = source.replace(/\r\n/g, "\n");
641
+ const original = [
642
+ " const assembled = await next()",
643
+ " try {",
644
+ " const status = promotion.status(context.agent)"
645
+ ].join("\n");
646
+ const replacement = [
647
+ " const assembled = await next()",
648
+ " try {",
649
+ " // prompt-tool 补丁:子代理跳过目录裁剪,直接使用组装结果;",
650
+ " // 调用方(如 dsh-mnemon)的工具白名单已先行过滤 assembled.tools,",
651
+ " // 因此任意前缀的新插件工具都会自动出现在子代理第一次会话。",
652
+ " if ((context.agent?.session?.header?.delegationDepth ?? 0) > 0) return assembled",
653
+ " const status = promotion.status(context.agent)"
654
+ ].join("\n");
655
+ if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
656
+ return source.replace(original, replacement);
657
+ }
519
658
  function writePreset(prompt, options) {
520
659
  const presetDir = options.presetDir;
521
660
  mkdirSync(presetDir, { recursive: true });
522
661
  writeFileSync(join(presetDir, "agent.cordis.yml"), buildCordis(prompt, {
523
662
  anchorFirstTurn: options.anchorFirstTurn,
524
663
  anchorText: options.anchorText,
525
- injectPrompt: options.injectPrompt
664
+ anchorCustom: options.anchorCustom,
665
+ guideText: options.guideText,
666
+ guideCustom: options.guideCustom,
667
+ injectPrompt: options.injectPrompt,
668
+ subagentFlash: options.subagentFlash,
669
+ subagentFlashProvider: options.subagentFlashProvider,
670
+ subagentFlashModel: options.subagentFlashModel,
671
+ bashPath: options.customBashPath
526
672
  }), "utf8");
527
673
  const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
528
674
  writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
529
675
  for (const file of readdirSync(VENDOR_PRESET_DIR)) {
530
676
  if (!file.endsWith(".mjs")) continue;
531
- writeFileSync(join(presetDir, file), readFileSync(join(VENDOR_PRESET_DIR, file), "utf8"), "utf8");
677
+ const vendorSource = readFileSync(join(VENDOR_PRESET_DIR, file), "utf8");
678
+ writeFileSync(join(presetDir, file), file === "tool-bootstrap.mjs" ? patchToolBootstrap(vendorSource) : vendorSource, "utf8");
532
679
  }
533
680
  const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
534
681
  if (options.agentsInstructionText !== void 0) {
@@ -539,11 +686,17 @@ function writePreset(prompt, options) {
539
686
  const injectorPath = join(presetDir, "prompt-injector.mjs");
540
687
  if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
541
688
  else rmSync(injectorPath, { force: true });
542
- const anchorPath = join(presetDir, "turn-anchor.mjs");
689
+ writeFileSync(join(presetDir, "router-first-turn.mjs"), readFileSync(PRESET_TEMPLATE_ROUTER, "utf8"), "utf8");
690
+ writeFileSync(join(presetDir, "router-guide.mjs"), readFileSync(PRESET_TEMPLATE_GUIDE, "utf8"), "utf8");
691
+ rmSync(join(presetDir, "turn-anchor.mjs"), { force: true });
692
+ const anchorPath = join(presetDir, "near-anchor.mjs");
543
693
  if (options.anchorFirstTurn) writeFileSync(anchorPath, readFileSync(PRESET_TEMPLATE_ANCHOR, "utf8"), "utf8");
544
694
  else rmSync(anchorPath, { force: true });
545
695
  }
546
696
  function apply(ctx, config) {
697
+ const deepseekState = () => detectDeepseek(ctx);
698
+ const getDeepseekAvailable = () => deepseekState().available;
699
+ const getDeepseekState = () => deepseekState();
547
700
  let current = config.text || readPromptFile(config.fallbackText);
548
701
  let currentAgents = config.agentsText || readAgents();
549
702
  const skillCatalog = readSkills(config.skillsDir).map((skill) => ({
@@ -598,7 +751,7 @@ function apply(ctx, config) {
598
751
  }
599
752
  };
600
753
  });
601
- registerSettingsBridge(ctx);
754
+ registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState);
602
755
  const runtime = {
603
756
  writeAgents: config.writeAgents,
604
757
  writePreset: config.writePreset,
@@ -606,8 +759,13 @@ function apply(ctx, config) {
606
759
  injectPrompt: config.injectPrompt,
607
760
  skillSwitches: { ...config.skillSwitches },
608
761
  anchorFirstTurn: config.anchorFirstTurn,
609
- anchorText: config.anchorText
762
+ anchorText: config.anchorText,
763
+ anchorCustom: config.anchorCustom,
764
+ guideText: config.guideText,
765
+ guideCustom: config.guideCustom,
766
+ subagentFlash: config.subagentFlash && getDeepseekAvailable()
610
767
  };
768
+ installSubagentFlashRoute(ctx, () => runtime.subagentFlash, config.subagentFlashProvider, config.subagentFlashModel);
611
769
  let currentSource = () => ({
612
770
  promptText: current,
613
771
  promptPath: PRESET_FILE_PATH,
@@ -615,14 +773,19 @@ function apply(ctx, config) {
615
773
  agentsPath: AGENTS_FILE_PATH,
616
774
  injectAgentsPrompt: runtime.injectAgentsPrompt,
617
775
  anchorFirstTurn: runtime.anchorFirstTurn,
618
- anchorText: runtime.anchorText,
776
+ anchorText: normalizeAnchorText(runtime.anchorText),
777
+ anchorCustom: runtime.anchorCustom,
778
+ guideText: runtime.guideText,
779
+ guideCustom: runtime.guideCustom,
780
+ subagentFlash: runtime.subagentFlash,
781
+ deepseekAvailable: getDeepseekAvailable(),
619
782
  injectPrompt: runtime.injectPrompt,
620
783
  skillSwitches: runtime.skillSwitches,
621
784
  skillCatalog,
622
785
  writeAgents: runtime.writeAgents,
623
786
  writePreset: runtime.writePreset
624
787
  });
625
- registerTuiCommand(ctx, () => currentSource());
788
+ registerTuiCommand(ctx, () => currentSource(), getDeepseekAvailable, getDeepseekState);
626
789
  let needsInitialApply = true;
627
790
  const applyState = () => {
628
791
  const next = currentSource();
@@ -633,12 +796,16 @@ function apply(ctx, config) {
633
796
  injectPrompt: typeof next.injectPrompt === "boolean" ? next.injectPrompt : config.injectPrompt,
634
797
  skillSwitches: next.skillSwitches !== void 0 ? next.skillSwitches : config.skillSwitches,
635
798
  anchorFirstTurn: typeof next.anchorFirstTurn === "boolean" ? next.anchorFirstTurn : config.anchorFirstTurn,
636
- anchorText: typeof next.anchorText === "string" && next.anchorText.length > 0 ? next.anchorText : config.anchorText
799
+ anchorText: normalizeAnchorText(typeof next.anchorText === "string" ? next.anchorText : config.anchorText),
800
+ anchorCustom: typeof next.anchorCustom === "boolean" ? next.anchorCustom : config.anchorCustom,
801
+ guideText: typeof next.guideText === "string" ? next.guideText : config.guideText,
802
+ guideCustom: typeof next.guideCustom === "boolean" ? next.guideCustom : config.guideCustom,
803
+ subagentFlash: (typeof next.subagentFlash === "boolean" ? next.subagentFlash : config.subagentFlash) && getDeepseekAvailable()
637
804
  };
638
805
  const promptChanged = next.promptText !== current;
639
806
  const agentsChanged = next.agentsText !== currentAgents;
640
807
  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;
808
+ 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.guideText !== nextRuntime.guideText || runtime.guideCustom !== nextRuntime.guideCustom || runtime.subagentFlash !== nextRuntime.subagentFlash;
642
809
  if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
643
810
  needsInitialApply = false;
644
811
  if (promptChanged) current = next.promptText;
@@ -650,6 +817,10 @@ function apply(ctx, config) {
650
817
  runtime.skillSwitches = nextRuntime.skillSwitches;
651
818
  runtime.anchorFirstTurn = nextRuntime.anchorFirstTurn;
652
819
  runtime.anchorText = nextRuntime.anchorText;
820
+ runtime.anchorCustom = nextRuntime.anchorCustom;
821
+ runtime.guideText = nextRuntime.guideText;
822
+ runtime.guideCustom = nextRuntime.guideCustom;
823
+ runtime.subagentFlash = nextRuntime.subagentFlash;
653
824
  skillSwitches = runtime.skillSwitches;
654
825
  if (skillSwitchesChanged) invalidateSkills?.();
655
826
  let residentAgentsWritten = false;
@@ -663,7 +834,14 @@ function apply(ctx, config) {
663
834
  if (runtime.writePreset) writePreset(runtime.injectPrompt && current.length > 0 ? current : "", {
664
835
  anchorFirstTurn: runtime.anchorFirstTurn,
665
836
  anchorText: runtime.anchorText,
837
+ anchorCustom: runtime.anchorCustom,
838
+ guideText: runtime.guideText,
839
+ guideCustom: runtime.guideCustom,
666
840
  injectPrompt: runtime.injectPrompt,
841
+ subagentFlash: runtime.subagentFlash,
842
+ subagentFlashProvider: config.subagentFlashProvider,
843
+ subagentFlashModel: config.subagentFlashModel,
844
+ customBashPath: config.customBashPath,
667
845
  agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
668
846
  presetDir: config.presetDir,
669
847
  presetOrder: config.presetOrder
@@ -685,6 +863,11 @@ function apply(ctx, config) {
685
863
  injectAgentsPrompt: config.injectAgentsPrompt,
686
864
  anchorFirstTurn: config.anchorFirstTurn,
687
865
  anchorText: config.anchorText,
866
+ anchorCustom: config.anchorCustom,
867
+ guideText: config.guideText,
868
+ guideCustom: config.guideCustom,
869
+ subagentFlash: config.subagentFlash && getDeepseekAvailable(),
870
+ deepseekAvailable: getDeepseekAvailable(),
688
871
  injectPrompt: config.injectPrompt,
689
872
  skillSwitches: { ...config.skillSwitches },
690
873
  skillCatalog,
@@ -10,11 +10,26 @@ declare function parseFrontmatter(text: string): {
10
10
  body: string;
11
11
  };
12
12
  interface BuildCordisOptions {
13
- /** 首轮独立锚定轮:首个用户消息先入 next-step inbox,首步只发 anchorText。 */
13
+ /** 首轮近距离锚定:首条真实用户消息后追加一次性首句锚点。 */
14
14
  anchorFirstTurn?: boolean;
15
+ /** 自定义锚点文本;anchorCustom=true 时固定使用。 */
15
16
  anchorText?: string;
17
+ /** 自定义锚点开关:true 固定使用 anchorText;false 按任务自动选择。 */
18
+ anchorCustom?: boolean;
19
+ /** 自定义每轮引导文本;guideCustom=true 时固定使用。 */
20
+ guideText?: string;
21
+ /** 自定义每轮引导开关:true 固定使用 guideText;false 按任务自动选择。 */
22
+ guideCustom?: boolean;
16
23
  /** 锚定确认后注入 preset.md;关闭时仍保留工具引导,但不注册 prompt-injector 行。 */
17
24
  injectPrompt?: boolean;
25
+ /** 子代理固定 Flash 模型:true 时给 subagent/subagent_fork 行加 agentOptions。 */
26
+ subagentFlash?: boolean;
27
+ /** 子代理 Flash 路由 provider(默认 deepseek-official)。 */
28
+ subagentFlashProvider?: string;
29
+ /** 子代理 Flash 模型名(默认 deepseek-v4-flash)。 */
30
+ subagentFlashModel?: string;
31
+ /** Windows custom-bash 可执行名或路径;默认 bash.exe(PATH 查找)。 */
32
+ bashPath?: string;
18
33
  }
19
34
  declare function buildCordis(prompt: string, options?: BuildCordisOptions): string;
20
35
  //#endregion
@@ -6,25 +6,42 @@ import { parse } from "yaml";
6
6
  * prompt-tool preset 生成核心:与 dsh 插件上下文无关的纯函数,
7
7
  * 独立成文件以便单元测试与复用。失败一律抛出(fail loud)。
8
8
  */
9
- const VENDOR_CORDIS = fileURLToPath(new URL("../vendor/dsh-anchored-standard/preset/agent.cordis.yml", import.meta.url));
10
- const LOCAL_INJECTOR_BLOCK = `# prompt-tool 附加件:锚定确认后注入 preset.md。注册在 tool-bootstrap 之后,
11
- # 不参与首轮剥离顺序;tools / 上下文剥离全部由原版 tool-bootstrap 负责
12
- # (首轮 = Minimal 真实 schema:持久 bash + str_replace_editor,无输出 cap),
13
- # 此插件只做一件事——锚定轮结束后(we 确认或兜底)注入一次提示词。
14
- - id: prompt-injector
15
- name: ./prompt-injector.mjs
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 ROUTER_GUIDE_BLOCK = `# Flash 主会话每轮近距离深度引导(dsh-router-standard 方案)。
17
+ # 模型为 Flash 且会话晋升后,每个真实用户消息之后追加固定引导。
18
+ # useCustom=true 时固定使用 text;false 按任务自动选择。
19
+ - id: router-guide
20
+ name: ./router-guide.mjs
16
21
  config:
17
- promptText: |-
18
- __PROMPT_TOOL_TEXT__
22
+ text: |-
23
+ __GUIDE_TEXT__
24
+ useCustom: __USE_GUIDE_CUSTOM__
25
+ enabled: __GUIDE_ENABLED__
19
26
  `;
20
- const TURN_ANCHOR_BLOCK = `# prompt-tool 可选附加件:首轮独立锚定轮。启用后首个真实用户消息先入
21
- # next-step inbox,首步只发 anchorText;模型回应锚定句后,driver 在同轮
22
- # 内自动消费任务继续执行。关闭时不生成此行。
23
- - id: turn-anchor
24
- name: ./turn-anchor.mjs
27
+ const NEAR_ANCHOR_BLOCK = `# prompt-tool 近距离首句锚定:在首条真实用户消息之后追加一次性锚点。
28
+ # 不拆轮、不挪任务;useCustom=false 时按任务自动选择 we/let 首句,
29
+ # useCustom=true 时固定使用 anchorText。
30
+ - id: near-anchor
31
+ name: ./near-anchor.mjs
25
32
  config:
26
33
  anchorText: |-
27
34
  __ANCHOR_TEXT__
35
+ useCustom: __USE_CUSTOM__
36
+ `;
37
+ const LOCAL_INJECTOR_BLOCK = `# prompt-tool 附加件:锚定确认后注入 preset.md。注册在工具引导之后,
38
+ # 不参与首轮剥离顺序;tools / 上下文剥离由 tool-bootstrap 与
39
+ # router-first-turn 负责(首轮 = Minimal 真实 schema + 训练原句)。
40
+ - id: prompt-injector
41
+ name: ./prompt-injector.mjs
42
+ config:
43
+ promptText: |-
44
+ __PROMPT_TOOL_TEXT__
28
45
  `;
29
46
  function parseFrontmatter(text) {
30
47
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
@@ -45,12 +62,78 @@ function parseFrontmatter(text) {
45
62
  body: text.slice(match[0].length)
46
63
  };
47
64
  }
65
+ /** dsh-router-standard 的 Flash 弱路由 persona(子代理版)。 */
66
+ const ROUTER_FLASH_PERSONA = [
67
+ "You are a helpful assistant.",
68
+ "Before acting, decide the task type (build or fix) and adopt the matching style: build → hands-on production; fix → inspect-and-plan.",
69
+ "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.",
70
+ "Think deeply first, then produce."
71
+ ].join("\n");
72
+ /** 适配上游 context-gate:保留 near-anchor 消息;子代理沿用全量放行策略。 */
73
+ function normalizeContextGate(source) {
74
+ const block = `- id: context-gate
75
+ name: ./context-gate.mjs
76
+ config:
77
+ promoteOn: either
78
+ includeSubagents: true
79
+ allowKinds: [skill-invocation]`;
80
+ const replaced = `- id: context-gate
81
+ name: ./context-gate.mjs
82
+ config:
83
+ promoteOn: either
84
+ includeSubagents: false
85
+ allowKinds: [skill-invocation, near-anchor, router-guide]`;
86
+ if (!source.includes(block)) throw new Error("vendor agent.cordis.yml missing context-gate row");
87
+ return source.replace(block, replaced);
88
+ }
89
+ /** 上游可能携带作者机器固定路径;生成时统一改写为可配置 bashPath。 */
90
+ function normalizeBashPath(source, bashPath) {
91
+ const marker = /bashPath:\s*'[^']*'/.exec(source);
92
+ if (marker === null) throw new Error("vendor agent.cordis.yml missing bashPath row");
93
+ return source.replace(marker[0], `bashPath: '${bashPath}'`);
94
+ }
95
+ /** 给 subagent/subagent_fork 行加 dsh-router-standard 的 Flash 子代理方案;关闭时保持继承主会话路由。 */
96
+ function applySubagentFlash(source, enabled, provider, model) {
97
+ if (!enabled) return source;
98
+ const agentOptions = ` agentOptions:
99
+ provider: ${provider}
100
+ model: ${model}`;
101
+ const persona = ROUTER_FLASH_PERSONA.split("\n").map((line) => ` ${line}`).join("\n");
102
+ const targets = [{
103
+ id: "tool-subagent",
104
+ toolName: "subagent",
105
+ provider: "spawn"
106
+ }, {
107
+ id: "tool-subagent-fork",
108
+ toolName: "subagent_fork",
109
+ provider: "fork"
110
+ }];
111
+ let out = source;
112
+ for (const target of targets) {
113
+ const block = ` - id: ${target.id}
114
+ name: '@deepseek-ai/dsh-tool-subagent'
115
+ config:
116
+ provider: ${target.provider}
117
+ toolName: ${target.toolName}
118
+ backgroundMode: continuable`;
119
+ const replaced = `${block}
120
+ ${agentOptions}
121
+ persona: |-
122
+ ${persona}`;
123
+ if (!out.includes(block)) throw new Error(`vendor agent.cordis.yml missing ${target.id} row`);
124
+ out = out.replace(block, replaced);
125
+ }
126
+ return out;
127
+ }
48
128
  function buildCordis(prompt, options = {}) {
49
129
  const anchorFirstTurn = options.anchorFirstTurn === true;
130
+ const anchorCustom = options.anchorCustom === true;
131
+ const guideCustom = options.guideCustom === true;
132
+ const guideText = typeof options.guideText === "string" && options.guideText.length > 0 ? options.guideText : "";
50
133
  const injectPrompt = options.injectPrompt !== false;
51
- const anchorText = typeof options.anchorText === "string" && options.anchorText.length > 0 ? options.anchorText : "You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'.";
134
+ const anchorText = typeof options.anchorText === "string" && options.anchorText.length > 0 ? options.anchorText : "";
52
135
  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");
136
+ 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
137
  const bootstrap = /^-\s+id:\s+tool-bootstrap\s*$/m.exec(up);
55
138
  if (!bootstrap) throw new Error("vendor agent.cordis.yml missing tool-bootstrap row");
56
139
  let cursor = up.indexOf("\n", bootstrap.index);
@@ -71,11 +154,13 @@ function buildCordis(prompt, options = {}) {
71
154
  const head = up.slice(0, insertAt);
72
155
  const tail = up.slice(insertAt);
73
156
  const separator = head.endsWith("\n\n") ? "" : head.endsWith("\n") ? "\n" : "\n\n";
74
- const parts = [];
157
+ const guideBlock = ROUTER_GUIDE_BLOCK.replace(" useCustom: __USE_GUIDE_CUSTOM__", ` useCustom: ${anchorFirstTurn && guideCustom}`).replace(" enabled: __GUIDE_ENABLED__", ` enabled: ${anchorFirstTurn}`).replace(" text: |-\n __GUIDE_TEXT__", " text: |-\n" + (guideText.length > 0 ? indent(guideText) : " "));
158
+ const parts = [ROUTER_FIRST_TURN_BLOCK, guideBlock];
75
159
  if (anchorFirstTurn) {
76
160
  const anchorMarker = " anchorText: |-\n __ANCHOR_TEXT__";
77
- if (!TURN_ANCHOR_BLOCK.includes(anchorMarker)) throw new Error("internal error: turn-anchor template lost its anchorText placeholder");
78
- parts.push(TURN_ANCHOR_BLOCK.replace(anchorMarker, " anchorText: |-\n" + indent(anchorText)));
161
+ if (!NEAR_ANCHOR_BLOCK.includes(anchorMarker)) throw new Error("internal error: near-anchor template lost its anchorText placeholder");
162
+ const anchorBlock = NEAR_ANCHOR_BLOCK.replace(" useCustom: __USE_CUSTOM__", ` useCustom: ${anchorCustom}`).replace(anchorMarker, " anchorText: |-\n" + (anchorText.length > 0 ? indent(anchorText) : " "));
163
+ parts.push(anchorBlock);
79
164
  }
80
165
  if (injectPrompt) {
81
166
  const promptMarker = " promptText: |-\n __PROMPT_TOOL_TEXT__";
@@ -84,7 +169,7 @@ function buildCordis(prompt, options = {}) {
84
169
  }
85
170
  const extra = parts.join("\n");
86
171
  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");
172
+ if (injectPrompt && out.includes("__PROMPT_TOOL_TEXT__") || anchorFirstTurn && (out.includes("__ANCHOR_TEXT__") || out.includes("__USE_CUSTOM__")) || out.includes("__GUIDE_TEXT__") || out.includes("__USE_GUIDE_CUSTOM__") || out.includes("__GUIDE_ENABLED__")) throw new Error("internal error: preset template placeholder was not replaced");
88
173
  let parsed;
89
174
  try {
90
175
  parsed = parse(out, { logLevel: "silent" });
@@ -93,9 +178,11 @@ function buildCordis(prompt, options = {}) {
93
178
  }
94
179
  if (!Array.isArray(parsed)) throw new Error("generated agent.cordis.yml is not a YAML array");
95
180
  const ids = new Set(parsed.map((row) => row?.id));
181
+ if (!ids.has("router-first-turn")) throw new Error("generated agent.cordis.yml is missing the router-first-turn row");
96
182
  if (injectPrompt && !ids.has("prompt-injector")) throw new Error("generated agent.cordis.yml is missing the prompt-injector row");
97
183
  if (!injectPrompt && ids.has("prompt-injector")) throw new Error("generated agent.cordis.yml still contains the prompt-injector row");
98
- if (anchorFirstTurn && !ids.has("turn-anchor")) throw new Error("generated agent.cordis.yml is missing the turn-anchor row");
184
+ if (anchorFirstTurn && !ids.has("near-anchor")) throw new Error("generated agent.cordis.yml is missing the near-anchor row");
185
+ if (!anchorFirstTurn && ids.has("near-anchor")) throw new Error("generated agent.cordis.yml still contains the near-anchor row");
99
186
  return out;
100
187
  }
101
188
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-prompt-tool",
3
- "version": "0.1.5",
3
+ "version": "0.3.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"