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/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/turn-anchor.mjs", import.meta.url));
30
- const VENDOR_PRESET_DIR = fileURLToPath(new URL("../vendor/dsh-anchored-standard/preset", import.meta.url));
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("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
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("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
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", "生成 prompt-tool 独立 preset"],
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") return {
317
- kind: "success",
318
- text: renderTuiStatus(source)
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
- injectPrompt: options.injectPrompt
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
- writeFileSync(join(presetDir, file), readFileSync(join(VENDOR_PRESET_DIR, file), "utf8"), "utf8");
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
- const anchorPath = join(presetDir, "turn-anchor.mjs");
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" && next.anchorText.length > 0 ? next.anchorText : config.anchorText
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,
@@ -10,11 +10,22 @@ 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;
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
@@ -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("../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 确认或兜底)注入一次提示词。
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 : "You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'.";
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 (!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)));
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("turn-anchor")) throw new Error("generated agent.cordis.yml is missing the turn-anchor row");
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.1.5",
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"