dsh-plugin-prompt-tool 0.6.2 → 0.7.1

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.
Files changed (87) hide show
  1. package/README.md +11 -9
  2. package/engine/anchor-match.mjs +38 -12
  3. package/engine/anchor-turn.mjs +68 -0
  4. package/engine/classify-task.mjs +35 -0
  5. package/engine/code-presentation.mjs +103 -0
  6. package/engine/compositions/library/anchor-turn.yml +10 -0
  7. package/engine/compositions/library/bootstrap-filesystem.yml +1 -1
  8. package/engine/compositions/library/code-presentation.yml +8 -0
  9. package/engine/compositions/library/command-goal.yml +11 -0
  10. package/engine/compositions/library/compaction.yml +1 -1
  11. package/engine/compositions/library/context-gate.yml +1 -2
  12. package/engine/compositions/library/cot-drip.yml +11 -0
  13. package/engine/compositions/library/custom-bash.yml +1 -1
  14. package/engine/compositions/library/delegation.yml +11 -7
  15. package/engine/compositions/library/deliberation-gate.yml +11 -0
  16. package/engine/compositions/library/persistent-shell.yml +25 -2
  17. package/engine/compositions/library/persona.yml +6 -2
  18. package/engine/compositions/library/planning.yml +1 -1
  19. package/engine/compositions/library/prompt-config-engine.yml +1 -1
  20. package/engine/compositions/library/run-code-env.yml +1 -1
  21. package/engine/compositions/library/skill-filesystem.yml +1 -1
  22. package/engine/compositions/library/skill-search.yml +1 -1
  23. package/engine/compositions/library/str-replace-editor.yml +2 -2
  24. package/engine/compositions/library/tool-ask-user.yml +1 -1
  25. package/engine/compositions/library/tool-bash.yml +1 -1
  26. package/engine/compositions/library/tool-bootstrap.yml +1 -3
  27. package/engine/compositions/library/tool-config-engine.yml +9 -0
  28. package/engine/compositions/library/tool-filter.yml +3 -7
  29. package/engine/compositions/library/tool-fs-search.yml +1 -1
  30. package/engine/compositions/library/tool-fs.yml +1 -1
  31. package/engine/compositions/library/tool-goal.yml +1 -9
  32. package/engine/compositions/library/tool-jobs.yml +1 -1
  33. package/engine/compositions/library/tool-pwsh.yml +1 -1
  34. package/engine/compositions/library/tool-todo.yml +1 -1
  35. package/engine/compositions/library/tool-web.yml +2 -2
  36. package/engine/compositions/source/local/code-presentation.yml +8 -0
  37. package/engine/compositions/source/local/context-gate.yml +0 -1
  38. package/engine/compositions/source/local/str-replace-editor.yml +10 -0
  39. package/engine/compositions/source/local/tool-bootstrap.yml +1 -3
  40. package/engine/compositions/source/local/tool-config-engine.yml +9 -0
  41. package/engine/compositions/source/local/tool-filter.yml +8 -0
  42. package/engine/cot-drip.mjs +137 -0
  43. package/engine/custom-bash.mjs +73 -21
  44. package/engine/deliberation-gate.mjs +127 -0
  45. package/engine/layers.mjs +223 -198
  46. package/engine/run-code-env.mjs +8 -2
  47. package/engine/schema.mjs +1 -1
  48. package/engine/strategies.mjs +25 -14
  49. package/engine/tool-bootstrap.mjs +185 -59
  50. package/engine/tool-config-engine.mjs +390 -0
  51. package/lib/client.js +3840 -2311
  52. package/lib/client.js.map +1 -1
  53. package/lib/index.d.mts +186 -101
  54. package/lib/index.mjs +1012 -413
  55. package/lib/preset-core.d.mts +1 -1
  56. package/lib/preset-core.mjs +29 -3
  57. package/lib/{prompt-configs-BgFw_bFf.mjs → prompt-configs-BnQKRHVb.mjs} +211 -136
  58. package/lib/prompt-configs-CN0sJTUL.d.mts +193 -0
  59. package/package.json +121 -105
  60. package/preset/anchored/preset.yml +262 -308
  61. package/preset/creative/preset.yml +34 -32
  62. package/preset/custom/preset.yml +0 -1
  63. package/preset/liangshen/preset.yml +53 -50
  64. package/preset/minimal/preset.yml +18 -17
  65. package/preset/ptc/preset.yml +35 -33
  66. package/preset/standard/preset.yml +34 -32
  67. package/templates/10-pre-step.yml +18 -15
  68. package/templates/14-first-turn-anchor.yml +13 -13
  69. package/templates/15-guide-auto.yml +13 -13
  70. package/templates/16-custom-fallback.yml +19 -18
  71. package/templates/18-placeholder-env-facts.yml +13 -2
  72. package/templates/30-runtime-context.yml +3 -0
  73. package/templates/40-agent-request.yml +9 -9
  74. package/templates/60-tool-pipeline.yml +9 -8
  75. package/templates/tools/10-shell-command.yml +20 -0
  76. package/templates/tools/20-http-fetch.yml +24 -0
  77. package/templates/tools/30-fs-read.yml +20 -0
  78. package/templates/tools/40-delegate-character.yml +15 -0
  79. package/templates/tools/50-delegate-worldbook.yml +15 -0
  80. package/templates/tools/60-delegate-session-var.yml +15 -0
  81. package/templates/tools/70-delegate-worldbook-upsert.yml +36 -0
  82. package/lib/prompt-configs-B4vH09wx.d.mts +0 -100
  83. package/templates/11-merged-a.yml +0 -11
  84. package/templates/13-anchor.yml +0 -19
  85. package/templates/17-instruction-hint.yml +0 -23
  86. package/templates/19-placeholder-skill-catalog.yml +0 -19
  87. package/templates/31-runtime-context-placeholder.yml +0 -18
package/lib/index.mjs CHANGED
@@ -1,9 +1,8 @@
1
- import { C as userPresetsDir, D as DEFAULT_SKILLS_DIR, E as DEFAULT_RESIDENT_AGENTS_PATH, O as LEGACY_CONTAINER_DIR, S as savePresetParams, T as DEFAULT_PRESET_DIR, _ as readPluginState, a as asString, b as resolvePresetDir, c as duplicateUserPreset, d as listPresets, f as loadPresetContent, g as parseImportedPresetId, h as packageEngineDir, i as applyModuleConfigs, k as LEGACY_USER_PRESETS_DIR, l as ensurePresetSeed, m as openPresetLocation, n as mergePromptConfigs, o as assertCompositionArray, p as loadPresetSpec, r as renderPromptConfigYaml, s as cloneBuiltinPreset, t as loadPromptConfigFiles, u as listBuiltinTemplates, v as removeUserPreset, w as writePluginState, x as resolvePresetParams, y as renderComposition } from "./prompt-configs-BgFw_bFf.mjs";
2
- import { settingsNamespace } from "@deepseek-ai/dsh-settings";
1
+ import { A as DEFAULT_SKILLS_DIR, C as resolvePresetParams, D as writePluginState, E as withPresetDoc, M as LEGACY_USER_PRESETS_DIR, O as DEFAULT_PRESET_DIR, S as resolvePresetDir, T as userPresetsDir, _ as packageEngineDir, a as applyModuleConfigs, b as removeUserPreset, c as buildModuleConfigsFromParams, d as ensurePresetSeed, f as listBuiltinTemplates, g as openPresetLocation, h as loadPresetSpec, i as MODEL_SEGMENT_MAP, j as LEGACY_CONTAINER_DIR, k as DEFAULT_RESIDENT_AGENTS_PATH, l as cloneBuiltinPreset, m as loadPresetContent, n as mergePromptConfigs, o as asString, p as listPresets, r as renderPromptConfigYaml, s as assertCompositionArray, t as loadPromptConfigFiles, u as duplicateUserPreset, v as parseImportedPresetId, w as savePresetParams, x as renderComposition, y as readPluginState } from "./prompt-configs-BnQKRHVb.mjs";
3
2
  import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
- import { parse, parseDocument, stringify } from "yaml";
5
3
  import { basename, dirname, join, resolve, sep } from "node:path";
6
4
  import { fileURLToPath } from "node:url";
5
+ import { parse, parseDocument, stringify } from "yaml";
7
6
  import z from "@deepseek-ai/schemastery";
8
7
  import { createHash, randomBytes } from "node:crypto";
9
8
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -551,23 +550,20 @@ function installSubagentModelRoute(ctx, isEnabled, provider, model) {
551
550
  });
552
551
  }
553
552
  //#endregion
554
- //#region src/shared/param-keys.ts
553
+ //#region src/shared/engine-params.ts
555
554
  /**
556
- * 引擎行为参数键(按预设存储:激活预设 preset.yml 的 params + promptConfigs)。
557
- * 不进 Config schema、不进 settings namespace——每预设一份,随预设走(官方范式:
558
- * Config = 部署轴,引擎行为在预设文件)。
559
- *
560
- * 这些键在 UI 有专门编辑入口(模型设置 / 工具与深度 / 开关),writePreset 把
561
- * 预设级 params 合并进每条 promptConfig 的 params 时排除——「params(高级参数
562
- * JSON)」框只保留内容变量与配置自身策略参数,避免 UI 已管理的参数冗余回写。
563
- * 放 shared:host(write-preset)与 config/settings-bridge 共用,单一来源。
555
+ * 引擎行为参数键唯一权威(数组字面量 = 运行时事实)。
556
+ * 与 EngineParams 接口双向相等断言:新增参数必须同时改接口与列表,漏改任一侧
557
+ * typecheck 失败(防历史事故:PARAM_KEYS 漏 25 键混入 variables.yml 污染注入)。
564
558
  */
565
- const PARAM_KEYS = /* @__PURE__ */ new Set([
559
+ const ENGINE_PARAM_KEYS = [
566
560
  "firstTurnAnchor",
567
561
  "firstTurnText",
568
562
  "firstTurnCustom",
569
563
  "guideText",
570
564
  "guideCustom",
565
+ "guideEnabled",
566
+ "injectPrompt",
571
567
  "modelProvider",
572
568
  "modelName",
573
569
  "subagentModelProvider",
@@ -578,7 +574,6 @@ const PARAM_KEYS = /* @__PURE__ */ new Set([
578
574
  "subagentReasoningEffort",
579
575
  "subagentTemperature",
580
576
  "subagentMaxTokens",
581
- "subagentPersona",
582
577
  "toolFilterAllow",
583
578
  "toolFilterDeny",
584
579
  "maxDepth",
@@ -586,13 +581,147 @@ const PARAM_KEYS = /* @__PURE__ */ new Set([
586
581
  "firstTurnWord",
587
582
  "bootstrapMaxTokens",
588
583
  "usePtcMode",
584
+ "promoteGate",
585
+ "promoteAfterFirstResponse",
586
+ "maxPromoteSteps",
587
+ "bootstrapTools",
588
+ "compactionTools",
589
+ "personaSectionsOnly",
590
+ "workspaceLine",
591
+ "phase1FirstCallInstruction",
592
+ "messageSources",
593
+ "deferredSources",
594
+ "deferredGraceSteps",
595
+ "instructionHint",
596
+ "stages",
597
+ "stagePreUnlock",
598
+ "stageAdvanceTool",
599
+ "stageAdvanceDescription",
600
+ "stageSectionTemplate",
601
+ "toolFilterSubagents",
602
+ "strReplaceEditorMaxOutputChars",
603
+ "anchorTurn",
604
+ "anchorTurnText",
605
+ "deliberationGate",
606
+ "deliberationMinChars",
607
+ "deliberationMaxGatesPerTurn",
608
+ "cotDrip",
609
+ "cotDripEvery",
610
+ "cotDripMaxPerTurn"
611
+ ];
612
+ /** writePreset.runtimeOf 实际透传键(与 PresetWriterParams 双向相等断言,防 Pick 漏键)。 */
613
+ const WRITER_PARAM_KEYS = [
614
+ "firstTurnAnchor",
615
+ "firstTurnText",
616
+ "firstTurnCustom",
617
+ "guideText",
618
+ "guideCustom",
619
+ "guideEnabled",
589
620
  "injectPrompt",
621
+ "modelProvider",
622
+ "modelName",
623
+ "subagentModelProvider",
624
+ "subagentModelName",
625
+ "modelReasoningEffort",
626
+ "modelTemperature",
627
+ "modelMaxTokens",
628
+ "subagentReasoningEffort",
629
+ "subagentTemperature",
630
+ "subagentMaxTokens",
631
+ "toolFilterAllow",
632
+ "toolFilterDeny",
633
+ "maxDepth",
634
+ "allowKinds",
635
+ "firstTurnWord",
636
+ "bootstrapMaxTokens",
637
+ "usePtcMode",
638
+ "anchorTurn",
639
+ "anchorTurnText",
640
+ "deliberationGate",
641
+ "deliberationMinChars",
642
+ "deliberationMaxGatesPerTurn",
643
+ "cotDrip",
644
+ "cotDripEvery",
645
+ "cotDripMaxPerTurn"
646
+ ];
647
+ const NUMERIC_PARAM_RULES = {
648
+ modelTemperature: validateTemperature,
649
+ subagentTemperature: validateTemperature,
650
+ modelMaxTokens: validateMaxTokens,
651
+ subagentMaxTokens: validateMaxTokens
652
+ };
653
+ function validateTemperature(value) {
654
+ return Number.isFinite(value) ? void 0 : "必须是有限数字";
655
+ }
656
+ function validateMaxTokens(value) {
657
+ return Number.isSafeInteger(value) && value > 0 ? void 0 : "必须是正整数";
658
+ }
659
+ /**
660
+ * 校验参数键值(键集合合法性由调用方白名单先行校验)。返回逐字段错误;空数组 = 通过。
661
+ * 类型收窄:只接受 number(preset.yml 手写数字)与 string(UI 字符串标量);
662
+ * '' 是合法删键值,其余空白串/非数字串/越界值在保存层响亮失败。
663
+ */
664
+ function validateEngineParamValues(overrides) {
665
+ const errors = [];
666
+ for (const [key, value] of Object.entries(overrides)) {
667
+ const rule = NUMERIC_PARAM_RULES[key];
668
+ if (rule === void 0) continue;
669
+ if (value === void 0 || value === null || value === "") continue;
670
+ if (typeof value !== "number" && typeof value !== "string") {
671
+ errors.push({
672
+ key,
673
+ message: `${key}: 必须是数字或数字字符串(留空 = 不设置)`
674
+ });
675
+ continue;
676
+ }
677
+ const text = typeof value === "string" ? value.trim() : String(value);
678
+ if (text.length === 0) {
679
+ errors.push({
680
+ key,
681
+ message: `${key}: 必须是数字(留空 = 不设置)`
682
+ });
683
+ continue;
684
+ }
685
+ const numeric = Number(text);
686
+ const reason = Number.isNaN(numeric) ? "必须是数字" : rule(numeric);
687
+ if (reason !== void 0) errors.push({
688
+ key,
689
+ message: `${key}: ${reason}(留空 = 不设置)`
690
+ });
691
+ }
692
+ return errors;
693
+ }
694
+ //#endregion
695
+ //#region src/shared/param-keys.ts
696
+ /**
697
+ * 引擎行为参数键(按预设存储:激活预设 preset.yml 的 params + promptConfigs)。
698
+ * 不进 Config schema、不进 settings namespace——每预设一份,随预设走(官方范式:
699
+ * Config = 部署轴,引擎行为在预设文件)。
700
+ *
701
+ * 派生自 ENGINE_PARAM_KEYS(唯一权威)+ 少量附加键:
702
+ * - 锚定/引导内容键:writePreset 映射进 near-anchor/router-guide 的 promptConfig
703
+ * params(策略消费),须排除出 variables.yml(避免同一键双落盘为模板变量);
704
+ * - promptConfigs:settings 提示词配置数组键。
705
+ * 注意:variables.yml 的占位键(spec.variables 空值登记,供世界书条目 {{key}} 动态
706
+ * 引用)走另一套通道,不属于 PARAM_KEYS——两套体系不互串。
707
+ * 放 shared:host(write-preset)与 config/settings-bridge 共用,单一来源。
708
+ */
709
+ const EXTRA_PARAM_KEYS = [
710
+ "buildPattern",
711
+ "complexPattern",
712
+ "firstTurnBuild",
713
+ "firstTurnInspect",
714
+ "firstTurnDeep",
715
+ "guideComplexPattern",
716
+ "guideWeak",
717
+ "guideDeep",
590
718
  "promptConfigs"
591
- ]);
719
+ ];
720
+ const PARAM_KEYS = /* @__PURE__ */ new Set([...ENGINE_PARAM_KEYS, ...EXTRA_PARAM_KEYS]);
592
721
  //#endregion
593
722
  //#region src/config.ts
594
723
  /** 插件配置、settings 数据模型与默认常量(settings 接口层)。 */
595
- const NS = settingsNamespace("prompt-tool");
724
+ const NS = "prompt-tool";
596
725
  const Config = z.object({
597
726
  injectAgentsPrompt: z.boolean().default(false),
598
727
  writeAgents: z.boolean().default(true),
@@ -730,6 +859,20 @@ function loadPromptTemplates() {
730
859
  };
731
860
  });
732
861
  }
862
+ /** 工具模板库扫描(templates/tools/*.yml;损坏 fail loud——包内模板属于发布 bug)。 */
863
+ function loadToolTemplates() {
864
+ const dir = fileURLToPath(new URL("../templates/tools", import.meta.url));
865
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name)).sort((a, b) => a.name.localeCompare(b.name)).map((entry) => {
866
+ const content = readFileSync(join(dir, entry.name), "utf8");
867
+ const parsed = parse(content, { logLevel: "silent" });
868
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.id !== "string" || parsed.id.length === 0 || typeof parsed.name !== "string" || parsed.execute === null || typeof parsed.execute !== "object") throw new Error(`tool template ${entry.name} must contain id/name/execute`);
869
+ return {
870
+ file: entry.name,
871
+ content,
872
+ spec: parsed
873
+ };
874
+ });
875
+ }
733
876
  //#endregion
734
877
  //#region src/runtime/skill-fix.ts
735
878
  /**
@@ -850,6 +993,73 @@ function fixSkillEntry(skillsDir, folder) {
850
993
  };
851
994
  }
852
995
  //#endregion
996
+ //#region src/host/worldbook.ts
997
+ /** 世界书条目 CRUD 服务:promptConfigs world-book 策略配置的增删改查。
998
+ * 与角色卡导入(characters.applyCharacterToPreset)共用同一存储(preset.yml
999
+ * promptConfigs),模型工具(world_book_*)与未来 bridge 端点同源,
1000
+ * 不各自实现 parseDocument 往返。 */
1001
+ /**
1002
+ * 世界书条目结构工厂(能力归一)。
1003
+ * strategy/layer/position 固定值与 params 键集单一权威——ST 导入(sillytavern.ts)
1004
+ * 与模型工具(world_book_upsert)共用,不再各自手写条目结构。
1005
+ */
1006
+ function buildWorldBookEntry(input) {
1007
+ return {
1008
+ ...input.id !== void 0 ? { id: input.id } : {},
1009
+ name: input.name,
1010
+ strategy: "world-book",
1011
+ order: typeof input.order === "number" ? input.order : 100,
1012
+ text: input.text,
1013
+ layer: "pre-step",
1014
+ position: "before-all",
1015
+ ...input.enabled !== void 0 ? { enabled: input.enabled } : {},
1016
+ params: {
1017
+ constant: input.constant,
1018
+ ...input.keys !== void 0 && input.keys.length > 0 ? { keys: input.keys } : {},
1019
+ ...input.secondaryKeys !== void 0 && input.secondaryKeys.length > 0 ? { secondaryKeys: input.secondaryKeys } : {},
1020
+ ...input.caseSensitive === true ? { caseSensitive: true } : {},
1021
+ ...input.wholeWords === true ? { wholeWords: true } : {},
1022
+ ...typeof input.selectiveLogic === "number" ? { selectiveLogic: input.selectiveLogic } : {}
1023
+ }
1024
+ };
1025
+ }
1026
+ const isWorldBook = (config) => config !== null && typeof config === "object" && !Array.isArray(config) && config.strategy === "world-book";
1027
+ /** 当前预设全部世界书条目(保持文件顺序)。 */
1028
+ function listWorldBookEntries(presetDir) {
1029
+ const spec = loadPresetSpec(presetDir);
1030
+ return Array.isArray(spec.promptConfigs) ? spec.promptConfigs.filter(isWorldBook) : [];
1031
+ }
1032
+ /** 新增或更新一条世界书条目(按 id 定位;不存在则追加)。返回写入后条目总数。 */
1033
+ function upsertWorldBookEntry(presetDir, entry) {
1034
+ if (entry === null || typeof entry !== "object" || typeof entry.id !== "string" || entry.id.length === 0) throw new TypeError("世界书条目必须含非空字符串 id");
1035
+ let count = 0;
1036
+ withPresetDoc(presetDir, (doc) => {
1037
+ const current = doc.toJS();
1038
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1039
+ const existing = configs.findIndex((config) => String(config.id ?? "") === entry.id);
1040
+ if (existing >= 0) configs[existing] = entry;
1041
+ else configs.push(entry);
1042
+ doc.setIn(["promptConfigs"], configs);
1043
+ count = configs.filter(isWorldBook).length;
1044
+ });
1045
+ return count;
1046
+ }
1047
+ /** 删除一条世界书条目;不存在抛错。返回删除后条目总数。 */
1048
+ function deleteWorldBookEntry(presetDir, id) {
1049
+ let count = 0;
1050
+ let removed = false;
1051
+ withPresetDoc(presetDir, (doc) => {
1052
+ const current = doc.toJS();
1053
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1054
+ const kept = configs.filter((config) => !(isWorldBook(config) && String(config.id ?? "") === id));
1055
+ removed = kept.length !== configs.length;
1056
+ doc.setIn(["promptConfigs"], kept);
1057
+ count = kept.filter(isWorldBook).length;
1058
+ });
1059
+ if (!removed) throw new Error(`世界书条目 ${id} 不存在`);
1060
+ return count;
1061
+ }
1062
+ //#endregion
853
1063
  //#region src/host/sillytavern.ts
854
1064
  /**
855
1065
  * SillyTavern 预设 → 本项目单文件预设转换引擎(纯函数,无文件 IO)。
@@ -1016,25 +1226,20 @@ function convertStToPreset(card, baseName) {
1016
1226
  const secondaryKeys = Array.isArray(rawSecondary) ? rawSecondary.map(String).filter((key) => key.trim().length > 0) : [];
1017
1227
  const constant = entry.constant === true || entry.add_always === true;
1018
1228
  const enabled = entry.disable !== void 0 ? entry.disable !== true : entry.enabled !== false;
1019
- configs.push({
1229
+ const stOrder = typeof entry.insertion_order === "number" ? entry.insertion_order : typeof entry.order === "number" ? entry.order : 100;
1230
+ configs.push(buildWorldBookEntry({
1020
1231
  id: `lore-${String(entry.id ?? entry.uid ?? index)}`,
1021
1232
  name: comment,
1022
1233
  enabled,
1023
- strategy: "world-book",
1024
- order: typeof entry.insertion_order === "number" ? entry.insertion_order : typeof entry.order === "number" ? entry.order : 100,
1234
+ order: -stOrder,
1025
1235
  text: content,
1026
- layer: "pre-step",
1027
- position: "before-all",
1028
- params: {
1029
- constant,
1030
- ...keys.length > 0 ? { keys } : {},
1031
- ...secondaryKeys.length > 0 ? { secondaryKeys } : {},
1032
- ...(entry.case_sensitive ?? entry.caseSensitive) === true ? { caseSensitive: true } : {},
1033
- ...(entry.match_whole_words ?? entry.matchWholeWords) === true ? { wholeWords: true } : {},
1034
- ...(entry.use_regex ?? entry.useRegex) === true ? { useRegex: true } : {},
1035
- ...typeof entry.selectiveLogic === "number" ? { selectiveLogic: entry.selectiveLogic } : typeof entry.selective_logic === "number" ? { selectiveLogic: entry.selective_logic } : {}
1036
- }
1037
- });
1236
+ constant,
1237
+ keys: keys.length > 0 ? keys : void 0,
1238
+ secondaryKeys: secondaryKeys.length > 0 ? secondaryKeys : void 0,
1239
+ ...(entry.case_sensitive ?? entry.caseSensitive) === true ? { caseSensitive: true } : {},
1240
+ ...(entry.match_whole_words ?? entry.matchWholeWords) === true ? { wholeWords: true } : {},
1241
+ ...typeof entry.selectiveLogic === "number" ? { selectiveLogic: entry.selectiveLogic } : typeof entry.selective_logic === "number" ? { selectiveLogic: entry.selective_logic } : {}
1242
+ }));
1038
1243
  }
1039
1244
  }
1040
1245
  const firstMes = clean(bodyText("first_mes"));
@@ -1188,6 +1393,70 @@ function sortConfigs(configs) {
1188
1393
  return (Number(a.order) || 0) - (Number(b.order) || 0);
1189
1394
  });
1190
1395
  }
1396
+ /** 角色卡记忆条目字段(不含 id;id 由调用方加 chara-<卡>- 前缀)。 */
1397
+ function buildCharacterMemoryEntry(spec, memory) {
1398
+ if (memory.trim().length === 0) return void 0;
1399
+ return buildWorldBookEntry({
1400
+ name: "角色记忆",
1401
+ enabled: true,
1402
+ order: 3e3,
1403
+ text: `【${spec.name} 的关系记忆】\n${memory}`,
1404
+ constant: true
1405
+ });
1406
+ }
1407
+ /** 角色卡记忆变更后同步已导入预设的 chara-<id>-memory 注入条目
1408
+ * (world_book note 写入卡记忆后调用;未导入当前预设的卡返回 synced=false)。 */
1409
+ function syncImportedCharacterMemory(presetRoot, templateName, cardId) {
1410
+ if (!validCardId(cardId)) return {
1411
+ ok: false,
1412
+ message: `非法角色卡 id:${cardId}`
1413
+ };
1414
+ const spec = loadConverted(cardDir(presetRoot, cardId));
1415
+ if (spec === void 0) return {
1416
+ ok: false,
1417
+ message: `角色卡 ${cardId} 不存在或参数损坏`
1418
+ };
1419
+ const memory = readCharacterMemory(presetRoot, cardId);
1420
+ const memoryId = `chara-${cardId}-memory`;
1421
+ let synced = false;
1422
+ try {
1423
+ withPresetDoc(join(presetRoot, templateName), (doc) => {
1424
+ const current = doc.toJS();
1425
+ if (!(Array.isArray(current.meta?.importedCharacters) ? current.meta.importedCharacters.map(String) : []).includes(cardId)) return;
1426
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1427
+ const entry = buildCharacterMemoryEntry(spec, memory);
1428
+ const at = configs.findIndex((config) => config !== null && typeof config === "object" && String(config.id ?? "") === memoryId);
1429
+ if (entry === void 0) {
1430
+ if (at >= 0) {
1431
+ configs.splice(at, 1);
1432
+ synced = true;
1433
+ }
1434
+ } else if (at >= 0) {
1435
+ configs[at] = {
1436
+ ...entry,
1437
+ id: memoryId
1438
+ };
1439
+ synced = true;
1440
+ } else {
1441
+ configs.push({
1442
+ ...entry,
1443
+ id: memoryId
1444
+ });
1445
+ synced = true;
1446
+ }
1447
+ if (synced) doc.setIn(["promptConfigs"], sortConfigs(configs));
1448
+ });
1449
+ return {
1450
+ ok: true,
1451
+ synced
1452
+ };
1453
+ } catch (error) {
1454
+ return {
1455
+ ok: false,
1456
+ message: `同步失败:${error instanceof Error ? error.message : String(error)}`
1457
+ };
1458
+ }
1459
+ }
1191
1460
  /** 角色卡库根:预设根下的点前缀目录(官方 roster 扫描跳过,与 .engine 同机制)。 */
1192
1461
  function charactersDir(presetRoot) {
1193
1462
  return join(presetRoot, ".characters");
@@ -1205,6 +1474,19 @@ function loadConverted(dir) {
1205
1474
  return;
1206
1475
  }
1207
1476
  }
1477
+ /** 追加角色卡本地记忆(.characters/<id>/memory.md,跟随角色卡跨预设)。 */
1478
+ function appendCharacterMemory(presetRoot, cardId, note) {
1479
+ appendMemoryFile(join(cardDir(presetRoot, cardId), "memory.md"), note, "# 角色记忆");
1480
+ }
1481
+ /** 追加记忆文件(时间戳列表格式;header 标注文件类型)。世界书工具与角色卡共用。 */
1482
+ function appendMemoryFile(file, note, header = "# 本地记忆") {
1483
+ const content = note.trim();
1484
+ if (content.length === 0) return;
1485
+ mkdirSync(dirname(file), { recursive: true });
1486
+ const line = `\n- [${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ")}] ${content}\n`;
1487
+ if (existsSync(file)) appendFileSync(file, line, "utf8");
1488
+ else writeFileSync(file, `${header}\n${line}`, "utf8");
1489
+ }
1208
1490
  /** 读角色卡本地记忆文本(无记忆返回空串)。 */
1209
1491
  function readCharacterMemory(presetRoot, cardId) {
1210
1492
  try {
@@ -1303,13 +1585,6 @@ function deleteCharacterCard(presetRoot, id) {
1303
1585
  };
1304
1586
  }
1305
1587
  }
1306
- function withPresetDoc(presetRoot, templateName, mutate) {
1307
- const file = join(presetRoot, templateName, "preset.yml");
1308
- if (!existsSync(file)) throw new Error(`当前预设 ${templateName} 无 preset.yml`);
1309
- const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
1310
- mutate(doc);
1311
- writeFileSync(file, doc.toString(), "utf8");
1312
- }
1313
1588
  /** 角色卡参数导入当前预设:promptConfigs 合并(chara-<id>- 前缀防冲突)、
1314
1589
  * params 合并(角色卡覆盖同键)、meta.importedCharacters 记录来源。 */
1315
1590
  function applyCharacterToPreset(presetRoot, templateName, cardId) {
@@ -1323,9 +1598,31 @@ function applyCharacterToPreset(presetRoot, templateName, cardId) {
1323
1598
  message: `角色卡 ${cardId} 不存在或参数损坏`
1324
1599
  };
1325
1600
  const prefix = `chara-${cardId}-`;
1601
+ const hasSystemSections = (spec.promptConfigs ?? []).some((config) => config !== null && typeof config === "object" && !Array.isArray(config) && config.layer === "system-section");
1602
+ let personaOpened = false;
1326
1603
  try {
1327
- withPresetDoc(presetRoot, templateName, (doc) => {
1604
+ withPresetDoc(join(presetRoot, templateName), (doc) => {
1328
1605
  const current = doc.toJS();
1606
+ if (hasSystemSections) {
1607
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1608
+ const personaIdx = configs.findIndex((config) => {
1609
+ if (config === null || typeof config !== "object" || Array.isArray(config)) return false;
1610
+ const entry = config;
1611
+ const params = entry.params;
1612
+ return entry.id === "persona-main" || params?.sectionName === "deployment:persona";
1613
+ });
1614
+ if (personaIdx >= 0) {
1615
+ if ((configs[personaIdx]?.params)?.complete === true) {
1616
+ doc.setIn([
1617
+ "promptConfigs",
1618
+ personaIdx,
1619
+ "params",
1620
+ "complete"
1621
+ ], false);
1622
+ personaOpened = true;
1623
+ }
1624
+ }
1625
+ }
1329
1626
  const existing = Array.isArray(current.promptConfigs) ? current.promptConfigs.filter((config) => {
1330
1627
  return config !== null && typeof config === "object" && !String(config.id ?? "").startsWith(prefix);
1331
1628
  }) : [];
@@ -1341,31 +1638,36 @@ function applyCharacterToPreset(presetRoot, templateName, cardId) {
1341
1638
  for (const [key, value] of Object.entries(spec.params ?? {})) doc.setIn(["params", key], value);
1342
1639
  const memory = readCharacterMemory(presetRoot, cardId);
1343
1640
  const memoryId = `${prefix}memory`;
1344
- if (memory.length > 0) {
1345
- const memoryEntry = {
1346
- id: memoryId,
1347
- name: "角色记忆",
1348
- strategy: "world-book",
1349
- layer: "pre-step",
1350
- position: "before-all",
1351
- enabled: true,
1352
- order: 1e3,
1353
- text: `【${spec.name} 的关系记忆】\n${memory}`,
1354
- params: { constant: true }
1355
- };
1356
- doc.setIn(["promptConfigs"], sortConfigs([
1357
- ...existing,
1358
- ...added,
1359
- memoryEntry
1360
- ]));
1361
- } else doc.setIn(["promptConfigs"], sortConfigs([...existing, ...added]));
1641
+ const memoryEntry = buildCharacterMemoryEntry(spec, memory);
1642
+ const merged = sortConfigs([
1643
+ ...existing,
1644
+ ...added,
1645
+ ...memoryEntry !== void 0 ? [{
1646
+ ...memoryEntry,
1647
+ id: memoryId
1648
+ }] : []
1649
+ ]);
1650
+ if (hasSystemSections) {
1651
+ const persona = merged.find((config) => {
1652
+ if (config === null || typeof config !== "object" || Array.isArray(config)) return false;
1653
+ const entry = config;
1654
+ const params = entry.params;
1655
+ return entry.id === "persona-main" || params?.sectionName === "deployment:persona";
1656
+ });
1657
+ if ((persona?.params)?.complete === true && persona !== void 0) {
1658
+ persona.params.complete = false;
1659
+ personaOpened = true;
1660
+ }
1661
+ }
1662
+ doc.setIn(["promptConfigs"], merged);
1362
1663
  const list = Array.isArray(current.meta?.importedCharacters) ? current.meta.importedCharacters : [];
1363
1664
  if (!list.map(String).includes(cardId)) list.push(cardId);
1364
1665
  doc.setIn(["meta", "importedCharacters"], list);
1365
1666
  });
1366
1667
  return {
1367
1668
  ok: true,
1368
- count: (spec.promptConfigs ?? []).length
1669
+ count: (spec.promptConfigs ?? []).length,
1670
+ ...personaOpened ? { personaOpened: true } : {}
1369
1671
  };
1370
1672
  } catch (error) {
1371
1673
  return {
@@ -1385,7 +1687,7 @@ function removeCharacterFromPreset(presetRoot, templateName, cardId) {
1385
1687
  const prefix = `chara-${cardId}-`;
1386
1688
  try {
1387
1689
  let removed = 0;
1388
- withPresetDoc(presetRoot, templateName, (doc) => {
1690
+ withPresetDoc(join(presetRoot, templateName), (doc) => {
1389
1691
  const current = doc.toJS();
1390
1692
  const kept = (Array.isArray(current.promptConfigs) ? current.promptConfigs : []).filter((config) => {
1391
1693
  const isCard = config !== null && typeof config === "object" && String(config.id ?? "").startsWith(prefix);
@@ -1412,14 +1714,15 @@ function removeCharacterFromPreset(presetRoot, templateName, cardId) {
1412
1714
  //#region src/shared/bridge-contract.ts
1413
1715
  /**
1414
1716
  * settings bridge 跨端契约(host 注册 / client 消费的唯一来源)。
1415
- * 铁律:11 端点载荷统一成功 `{ ok: true, value }`、失败 `{ ok: false, code?, message? }`;
1717
+ * 铁律:25 端点载荷统一成功 `{ ok: true, value }`、失败 `{ ok: false, code?, message? }`;
1416
1718
  * 端点附加字段只能以 value 旁的可选扩展字段出现(describe)。
1417
1719
  * 改路径或载荷形状必须同步更新 test/shared/bridge-contract.test.mjs。
1418
1720
  */
1419
1721
  const SETTINGS_BRIDGE_PREFIX = "/api/prompt-tool/settings";
1420
- /** 22 个桥端点路径(相对前缀)。新增端点必须同时登记到契约测试。 */
1722
+ /** 桥端点路径(相对前缀)。新增端点必须同时登记到契约测试。 */
1421
1723
  const BRIDGE_ENDPOINTS = {
1422
1724
  meta: "/meta",
1725
+ bootstrap: "/bootstrap",
1423
1726
  describe: "/describe",
1424
1727
  models: "/models",
1425
1728
  mutate: "/mutate",
@@ -1431,6 +1734,7 @@ const BRIDGE_ENDPOINTS = {
1431
1734
  importPreset: "/import-preset",
1432
1735
  paramOverrides: "/param-overrides",
1433
1736
  presetVariables: "/preset-variables",
1737
+ customTools: "/custom-tools",
1434
1738
  importPresetPackage: "/import-preset-package",
1435
1739
  exportPreset: "/export-preset",
1436
1740
  presetDelete: "/preset-delete",
@@ -1523,7 +1827,8 @@ function restorePresetImport(targetDir, backupDir) {
1523
1827
  } catch {}
1524
1828
  }
1525
1829
  /** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
1526
- function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngineStrategyDir, ensureRegistered, afterSkillFix, getPresetConfigsDir, afterPresetImport, afterOverridesChange, afterPresetPackageImport) {
1830
+ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngineStrategyDir, afterSkillFix, getPresetConfigsDir, afterPresetImport, afterOverridesChange, afterPresetPackageImport) {
1831
+ let invalidateCachedDescriptor = () => {};
1527
1832
  ctx.inject(["settings", "webServer"], (sctx) => {
1528
1833
  sctx.effect(() => {
1529
1834
  let cachedDescriptor;
@@ -1540,6 +1845,7 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1540
1845
  const invalidateDescriptor = () => {
1541
1846
  cachedDescriptor = void 0;
1542
1847
  };
1848
+ invalidateCachedDescriptor = invalidateDescriptor;
1543
1849
  const guard = (req, res) => {
1544
1850
  if (!isLoopbackRequest(req)) {
1545
1851
  writeBridgeJson(res, 403, {
@@ -1559,19 +1865,149 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1559
1865
  }
1560
1866
  return true;
1561
1867
  };
1868
+ /** 引擎能力矩阵(meta 端点与 /bootstrap 共用):动态 import 引擎 schema。 */
1869
+ const loadEngineMeta = async () => {
1870
+ const { getEngineMeta } = await import(new URL("../engine/schema.mjs", import.meta.url).href);
1871
+ const meta = getEngineMeta();
1872
+ meta.presets = listPresets();
1873
+ meta.builtinTemplates = listBuiltinTemplates();
1874
+ return meta;
1875
+ };
1876
+ /** describe 运行时事实(describe 端点与 /bootstrap 共用):检测状态、技能快照、
1877
+ * 宿主默认模型、模型目录缓存、激活预设参数。不触网(模型目录只读 10min 缓存)。 */
1878
+ const collectDescribeExtras = () => {
1879
+ const detection = getModelsState();
1880
+ const skillsState = getSkillsState();
1881
+ let hostDefaultModel;
1882
+ try {
1883
+ const selection = sctx.settings.get("agent-default-model");
1884
+ if (selection !== null && typeof selection === "object") {
1885
+ const record = selection;
1886
+ hostDefaultModel = {
1887
+ ...typeof record.provider === "string" && record.provider.length > 0 ? { provider: record.provider } : {},
1888
+ ...typeof record.model === "string" && record.model.length > 0 ? { model: record.model } : {},
1889
+ ...typeof record.reasoningEffort === "string" && record.reasoningEffort.length > 0 ? { reasoningEffort: record.reasoningEffort } : {}
1890
+ };
1891
+ if (Object.keys(hostDefaultModel).length === 0) hostDefaultModel = void 0;
1892
+ }
1893
+ } catch {}
1894
+ const modelCatalog = peekModelCatalog();
1895
+ let presetParams = {};
1896
+ let templatePreStepCount = 0;
1897
+ try {
1898
+ const activeDir = getPresetConfigsDir?.() ?? "";
1899
+ const templateName = activeDir.length > 0 ? basename(activeDir) : "anchored";
1900
+ const spec = loadPresetSpec(activeDir.length > 0 ? activeDir : resolvePresetDir(templateName));
1901
+ presetParams = resolvePresetParams(spec, {});
1902
+ if (Array.isArray(spec.promptConfigs)) presetParams.promptConfigs = spec.promptConfigs;
1903
+ templatePreStepCount = (spec.promptConfigs ?? []).filter((config) => {
1904
+ const layer = config.layer;
1905
+ return layer === void 0 || layer === "pre-step";
1906
+ }).length;
1907
+ } catch {
1908
+ templatePreStepCount = 0;
1909
+ }
1910
+ return {
1911
+ presetParams,
1912
+ hostDefaultModel,
1913
+ templatePreStepCount,
1914
+ modelsAvailable: detection.available,
1915
+ providers: detection.providers,
1916
+ modelCatalog,
1917
+ modelsError: detection.error,
1918
+ activeSkillsDirs: skillsState.activeSkillsDirs,
1919
+ skillsDirExists: Object.fromEntries(skillsState.activeSkillsDirs.map((dir) => [dir, existsSync(dir)])),
1920
+ skillCatalog: skillsState.skillCatalog
1921
+ };
1922
+ };
1923
+ /** 激活预设的引擎参数子集(/param-overrides 读取;preset.yml 按 mtime 缓存)。 */
1924
+ const readParamOverrides = (dir) => {
1925
+ try {
1926
+ const spec = loadPresetSpec(dir);
1927
+ const params = {};
1928
+ for (const key of PARAM_KEYS) {
1929
+ if (key === "promptConfigs") continue;
1930
+ if (spec.params !== null && typeof spec.params === "object" && key in spec.params) params[key] = spec.params[key];
1931
+ }
1932
+ return params;
1933
+ } catch {
1934
+ return {};
1935
+ }
1936
+ };
1937
+ /** 预设级模板变量(/preset-variables 读取)。 */
1938
+ const readPresetVariables = (dir) => {
1939
+ try {
1940
+ const spec = loadPresetSpec(dir);
1941
+ const variables = {};
1942
+ for (const [key, value] of Object.entries(spec.params ?? {})) if (!PARAM_KEYS.has(key) && typeof value === "string") variables[key] = value;
1943
+ for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string") variables[key] = value;
1944
+ return {
1945
+ variables,
1946
+ enabled: spec.variablesEnabled !== false
1947
+ };
1948
+ } catch {
1949
+ return {
1950
+ variables: {},
1951
+ enabled: true
1952
+ };
1953
+ }
1954
+ };
1955
+ /** 生成目录实际生效配置(/prompt-configs 读取)。 */
1956
+ const readPromptConfigs = (dir) => {
1957
+ try {
1958
+ return dir.length > 0 ? loadPromptConfigFiles(join(dir, "prompt-configs")) : [];
1959
+ } catch {
1960
+ return [];
1961
+ }
1962
+ };
1562
1963
  const disposers = [
1964
+ sctx.webServer.register({
1965
+ kind: "exact",
1966
+ path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.bootstrap,
1967
+ handler: async (req, res) => {
1968
+ if (!guard(req, res)) return;
1969
+ const descriptor = findDescriptor();
1970
+ if (descriptor === void 0) {
1971
+ writeBridgeJson(res, 404, {
1972
+ ok: false,
1973
+ code: "settings-not-exposed",
1974
+ message: "prompt-tool settings namespace is not registered"
1975
+ });
1976
+ return;
1977
+ }
1978
+ try {
1979
+ const meta = await loadEngineMeta();
1980
+ const dir = getPresetConfigsDir?.() ?? "";
1981
+ const extras = collectDescribeExtras();
1982
+ writeBridgeJson(res, 200, {
1983
+ ok: true,
1984
+ value: descriptor,
1985
+ meta: { meta },
1986
+ overrides: { overrides: dir.length > 0 ? readParamOverrides(dir) : {} },
1987
+ variables: dir.length > 0 ? readPresetVariables(dir) : {
1988
+ variables: {},
1989
+ enabled: true
1990
+ },
1991
+ promptConfigs: { promptConfigs: readPromptConfigs(dir) },
1992
+ ...extras
1993
+ });
1994
+ } catch (error) {
1995
+ writeBridgeJson(res, 500, {
1996
+ ok: false,
1997
+ code: "bootstrap-failed",
1998
+ message: error instanceof Error ? error.message : String(error)
1999
+ });
2000
+ }
2001
+ }
2002
+ }),
1563
2003
  sctx.webServer.register({
1564
2004
  kind: "exact",
1565
2005
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.meta,
1566
2006
  handler: async (req, res) => {
1567
2007
  if (!guard(req, res)) return;
1568
- const { getEngineMeta } = await import(new URL("../engine/schema.mjs", import.meta.url).href);
1569
- const meta = getEngineMeta();
1570
- meta.presets = listPresets();
1571
- meta.builtinTemplates = listBuiltinTemplates();
1572
2008
  writeBridgeJson(res, 200, {
1573
2009
  ok: true,
1574
- value: { meta }
2010
+ value: { meta: await loadEngineMeta() }
1575
2011
  });
1576
2012
  }
1577
2013
  }),
@@ -1580,7 +2016,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1580
2016
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.describe,
1581
2017
  handler: async (req, res) => {
1582
2018
  if (!guard(req, res)) return;
1583
- ensureRegistered(sctx);
1584
2019
  const descriptor = findDescriptor();
1585
2020
  if (descriptor === void 0) {
1586
2021
  writeBridgeJson(res, 404, {
@@ -1590,50 +2025,10 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1590
2025
  });
1591
2026
  return;
1592
2027
  }
1593
- const detection = getModelsState();
1594
- const skillsState = getSkillsState();
1595
- let hostDefaultModel;
1596
- try {
1597
- const selection = sctx.settings.get(settingsNamespace("agent-default-model"));
1598
- if (selection !== null && typeof selection === "object") {
1599
- const record = selection;
1600
- hostDefaultModel = {
1601
- ...typeof record.provider === "string" && record.provider.length > 0 ? { provider: record.provider } : {},
1602
- ...typeof record.model === "string" && record.model.length > 0 ? { model: record.model } : {},
1603
- ...typeof record.reasoningEffort === "string" && record.reasoningEffort.length > 0 ? { reasoningEffort: record.reasoningEffort } : {}
1604
- };
1605
- if (Object.keys(hostDefaultModel).length === 0) hostDefaultModel = void 0;
1606
- }
1607
- } catch {}
1608
- const modelCatalog = peekModelCatalog();
1609
- let presetParams = {};
1610
- let templatePreStepCount = 0;
1611
- try {
1612
- const settingsValue = descriptor.value !== null && typeof descriptor.value === "object" ? descriptor.value : {};
1613
- const templateName = typeof settingsValue.presetTemplate === "string" && settingsValue.presetTemplate.length > 0 ? settingsValue.presetTemplate : "anchored";
1614
- const spec = loadPresetSpec(resolvePresetDir(templateName));
1615
- presetParams = resolvePresetParams(spec, {});
1616
- if (Array.isArray(spec.promptConfigs)) presetParams.promptConfigs = spec.promptConfigs;
1617
- templatePreStepCount = (spec.promptConfigs ?? []).filter((config) => {
1618
- const layer = config.layer;
1619
- return layer === void 0 || layer === "pre-step";
1620
- }).length;
1621
- } catch {
1622
- templatePreStepCount = 0;
1623
- }
1624
2028
  writeBridgeJson(res, 200, {
1625
2029
  ok: true,
1626
2030
  value: descriptor,
1627
- presetParams,
1628
- hostDefaultModel,
1629
- templatePreStepCount,
1630
- modelsAvailable: detection.available,
1631
- providers: detection.providers,
1632
- modelCatalog,
1633
- modelsError: detection.error,
1634
- activeSkillsDirs: skillsState.activeSkillsDirs,
1635
- skillsDirExists: Object.fromEntries(skillsState.activeSkillsDirs.map((dir) => [dir, existsSync(dir)])),
1636
- skillCatalog: skillsState.skillCatalog
2031
+ ...collectDescribeExtras()
1637
2032
  });
1638
2033
  }
1639
2034
  }),
@@ -1642,7 +2037,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1642
2037
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.models,
1643
2038
  handler: async (req, res) => {
1644
2039
  if (!guard(req, res)) return;
1645
- ensureRegistered(sctx);
1646
2040
  writeBridgeJson(res, 200, {
1647
2041
  ok: true,
1648
2042
  value: { modelCatalog: await listAdvertisedModels(sctx) }
@@ -1654,7 +2048,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1654
2048
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.mutate,
1655
2049
  handler: async (req, res) => {
1656
2050
  if (!guard(req, res)) return;
1657
- ensureRegistered(sctx);
1658
2051
  const { body } = await readBridgeBody(req);
1659
2052
  if (body === null || body === void 0 || typeof body !== "object") {
1660
2053
  writeBridgeJson(res, 400, {
@@ -1716,7 +2109,15 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1716
2109
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.configsValidate,
1717
2110
  handler: async (req, res) => {
1718
2111
  if (!guard(req, res)) return;
1719
- const { body } = await readBridgeBody(req);
2112
+ const { body, tooLarge } = await readBridgeBody(req, PRESET_PACKAGE_MAX_BYTES);
2113
+ if (tooLarge) {
2114
+ writeBridgeJson(res, 413, {
2115
+ ok: false,
2116
+ code: "configs-too-large",
2117
+ message: `配置载荷超过 ${Math.round(PRESET_PACKAGE_MAX_BYTES / 1024 / 1024)}MB 上限`
2118
+ });
2119
+ return;
2120
+ }
1720
2121
  if (body === null || body === void 0 || typeof body !== "object") {
1721
2122
  writeBridgeJson(res, 400, {
1722
2123
  ok: false,
@@ -1736,7 +2137,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1736
2137
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.skillFix,
1737
2138
  handler: async (req, res) => {
1738
2139
  if (!guard(req, res)) return;
1739
- ensureRegistered(sctx);
1740
2140
  const { body } = await readBridgeBody(req);
1741
2141
  if (body === null || body === void 0 || typeof body !== "object") {
1742
2142
  writeBridgeJson(res, 400, {
@@ -1815,7 +2215,10 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1815
2215
  try {
1816
2216
  writeBridgeJson(res, 200, {
1817
2217
  ok: true,
1818
- value: { templates: loadPromptTemplates() }
2218
+ value: {
2219
+ templates: loadPromptTemplates(),
2220
+ toolTemplates: loadToolTemplates()
2221
+ }
1819
2222
  });
1820
2223
  } catch (error) {
1821
2224
  writeBridgeJson(res, 500, {
@@ -1831,18 +2234,11 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1831
2234
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.promptConfigs,
1832
2235
  handler: async (req, res) => {
1833
2236
  if (!guard(req, res)) return;
1834
- try {
1835
- const dir = join(getPresetConfigsDir?.() ?? "", "prompt-configs");
1836
- writeBridgeJson(res, 200, {
1837
- ok: true,
1838
- value: { promptConfigs: dir.length > 0 ? loadPromptConfigFiles(dir) : [] }
1839
- });
1840
- } catch {
1841
- writeBridgeJson(res, 200, {
1842
- ok: true,
1843
- value: { promptConfigs: [] }
1844
- });
1845
- }
2237
+ const dir = getPresetConfigsDir?.() ?? "";
2238
+ writeBridgeJson(res, 200, {
2239
+ ok: true,
2240
+ value: { promptConfigs: readPromptConfigs(dir) }
2241
+ });
1846
2242
  }
1847
2243
  }),
1848
2244
  sctx.webServer.register({
@@ -1871,7 +2267,15 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1871
2267
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.importPreset,
1872
2268
  handler: async (req, res) => {
1873
2269
  if (!guard(req, res)) return;
1874
- const { body } = await readBridgeBody(req);
2270
+ const { body, tooLarge } = await readBridgeBody(req, PRESET_PACKAGE_MAX_BYTES);
2271
+ if (tooLarge) {
2272
+ writeBridgeJson(res, 413, {
2273
+ ok: false,
2274
+ code: "preset-import-too-large",
2275
+ message: `内容资产超过 ${Math.round(PRESET_PACKAGE_MAX_BYTES / 1024 / 1024)}MB 上限`
2276
+ });
2277
+ return;
2278
+ }
1875
2279
  if (body === null || body === void 0 || typeof body !== "object") {
1876
2280
  writeBridgeJson(res, 400, {
1877
2281
  ok: false,
@@ -1881,8 +2285,27 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1881
2285
  return;
1882
2286
  }
1883
2287
  const record = body;
1884
- const scope = record.scope === "agents" ? "agents" : "preset";
1885
- const content = typeof record.content === "string" ? record.content : "";
2288
+ const contents = [];
2289
+ if (Array.isArray(record.contents)) for (const entry of record.contents) {
2290
+ if (entry === null || typeof entry !== "object") continue;
2291
+ const item = entry;
2292
+ contents.push({
2293
+ scope: item.scope === "agents" ? "agents" : "preset",
2294
+ content: typeof item.content === "string" ? item.content : ""
2295
+ });
2296
+ }
2297
+ else contents.push({
2298
+ scope: record.scope === "agents" ? "agents" : "preset",
2299
+ content: typeof record.content === "string" ? record.content : ""
2300
+ });
2301
+ if (contents.length === 0) {
2302
+ writeBridgeJson(res, 400, {
2303
+ ok: false,
2304
+ code: "settings-rejected",
2305
+ message: "unreadable JSON body"
2306
+ });
2307
+ return;
2308
+ }
1886
2309
  const dir = getPresetConfigsDir?.() ?? "";
1887
2310
  if (dir.length === 0) {
1888
2311
  writeBridgeJson(res, 400, {
@@ -1894,11 +2317,11 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1894
2317
  }
1895
2318
  try {
1896
2319
  mkdirSync(dir, { recursive: true });
1897
- writeFileSync(join(dir, scope === "preset" ? "preset.md" : "agents.md"), content, "utf8");
1898
- afterPresetImport?.(scope);
2320
+ for (const entry of contents) writeFileSync(join(dir, entry.scope === "preset" ? "preset.md" : "agents.md"), entry.content, "utf8");
2321
+ afterPresetImport?.(contents.map((entry) => entry.scope));
1899
2322
  writeBridgeJson(res, 200, {
1900
2323
  ok: true,
1901
- value: { scope }
2324
+ value: { scopes: contents.map((entry) => entry.scope) }
1902
2325
  });
1903
2326
  } catch (error) {
1904
2327
  writeBridgeJson(res, 500, {
@@ -1925,31 +2348,47 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1925
2348
  }
1926
2349
  const presetRoot = dirname(dir);
1927
2350
  const templateName = basename(dir);
1928
- const { body } = await readBridgeBody(req);
2351
+ const { body, tooLarge } = await readBridgeBody(req, PRESET_PACKAGE_MAX_BYTES);
2352
+ if (tooLarge) {
2353
+ writeBridgeJson(res, 413, {
2354
+ ok: false,
2355
+ code: "overrides-too-large",
2356
+ message: `参数载荷超过 ${Math.round(PRESET_PACKAGE_MAX_BYTES / 1024 / 1024)}MB 上限`
2357
+ });
2358
+ return;
2359
+ }
1929
2360
  const record = body ?? {};
1930
2361
  if (record.overrides === void 0 && record.promptConfigs === void 0) {
1931
- try {
1932
- const spec = loadPresetSpec(dir);
1933
- const params = {};
1934
- for (const key of PARAM_KEYS) {
1935
- if (key === "promptConfigs") continue;
1936
- if (spec.params !== null && typeof spec.params === "object" && key in spec.params) params[key] = spec.params[key];
1937
- }
1938
- writeBridgeJson(res, 200, {
1939
- ok: true,
1940
- value: { overrides: params }
1941
- });
1942
- } catch {
1943
- writeBridgeJson(res, 200, {
1944
- ok: true,
1945
- value: { overrides: {} }
2362
+ writeBridgeJson(res, 200, {
2363
+ ok: true,
2364
+ value: { overrides: readParamOverrides(dir) }
2365
+ });
2366
+ return;
2367
+ }
2368
+ const rawOverrides = record.overrides !== null && typeof record.overrides === "object" && !Array.isArray(record.overrides) ? record.overrides : void 0;
2369
+ if (rawOverrides !== void 0) {
2370
+ const unknownKeys = Object.keys(rawOverrides).filter((key) => key === "promptConfigs" || !PARAM_KEYS.has(key));
2371
+ if (unknownKeys.length > 0) {
2372
+ writeBridgeJson(res, 400, {
2373
+ ok: false,
2374
+ code: "overrides-unknown-key",
2375
+ message: `未知引擎参数键:${unknownKeys.join(", ")}`
1946
2376
  });
2377
+ return;
1947
2378
  }
2379
+ }
2380
+ const valueErrors = rawOverrides !== void 0 ? validateEngineParamValues(rawOverrides) : [];
2381
+ if (valueErrors.length > 0) {
2382
+ writeBridgeJson(res, 400, {
2383
+ ok: false,
2384
+ code: "overrides-invalid-value",
2385
+ message: valueErrors.map((item) => item.message).join("; ")
2386
+ });
1948
2387
  return;
1949
2388
  }
1950
2389
  try {
1951
- savePresetParams(presetRoot, templateName, record.overrides !== null && typeof record.overrides === "object" && !Array.isArray(record.overrides) ? record.overrides : void 0, Array.isArray(record.promptConfigs) ? record.promptConfigs : void 0);
1952
- afterOverridesChange?.();
2390
+ savePresetParams(presetRoot, templateName, rawOverrides, Array.isArray(record.promptConfigs) ? record.promptConfigs : void 0);
2391
+ if (record.rebuild !== false) afterOverridesChange?.();
1953
2392
  writeBridgeJson(res, 200, {
1954
2393
  ok: true,
1955
2394
  value: {
@@ -1985,41 +2424,83 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1985
2424
  const { body } = await readBridgeBody(req);
1986
2425
  const record = body ?? {};
1987
2426
  if (record.variables === void 0 && record.enabled === void 0) {
2427
+ writeBridgeJson(res, 200, {
2428
+ ok: true,
2429
+ value: readPresetVariables(dir)
2430
+ });
2431
+ return;
2432
+ }
2433
+ try {
2434
+ savePresetParams(presetRoot, templateName, void 0, void 0, record.variables !== null && typeof record.variables === "object" && !Array.isArray(record.variables) ? record.variables : void 0, typeof record.enabled === "boolean" ? record.enabled : void 0);
2435
+ afterOverridesChange?.();
2436
+ writeBridgeJson(res, 200, {
2437
+ ok: true,
2438
+ value: { variables: record.variables }
2439
+ });
2440
+ } catch (error) {
2441
+ writeBridgeJson(res, 409, {
2442
+ ok: false,
2443
+ code: "preset-variables-rejected",
2444
+ message: `模板变量保存失败:${error instanceof Error ? error.message : String(error)}`
2445
+ });
2446
+ }
2447
+ }
2448
+ }),
2449
+ sctx.webServer.register({
2450
+ kind: "exact",
2451
+ path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.customTools,
2452
+ handler: async (req, res) => {
2453
+ if (!guard(req, res)) return;
2454
+ const dir = getPresetConfigsDir?.() ?? "";
2455
+ if (dir.length === 0) {
2456
+ writeBridgeJson(res, 400, {
2457
+ ok: false,
2458
+ code: "preset-dir-unavailable",
2459
+ message: "presetDir 未配置"
2460
+ });
2461
+ return;
2462
+ }
2463
+ const { body } = await readBridgeBody(req);
2464
+ const record = body ?? {};
2465
+ if (record.customTools === void 0) {
1988
2466
  try {
1989
2467
  const spec = loadPresetSpec(dir);
1990
- const variables = {};
1991
- for (const [key, value] of Object.entries(spec.params ?? {})) if (!PARAM_KEYS.has(key) && typeof value === "string") variables[key] = value;
1992
- for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string") variables[key] = value;
1993
2468
  writeBridgeJson(res, 200, {
1994
2469
  ok: true,
1995
- value: {
1996
- variables,
1997
- enabled: spec.variablesEnabled !== false
1998
- }
2470
+ value: { customTools: Array.isArray(spec.customTools) ? spec.customTools : [] }
1999
2471
  });
2000
2472
  } catch {
2001
2473
  writeBridgeJson(res, 200, {
2002
2474
  ok: true,
2003
- value: {
2004
- variables: {},
2005
- enabled: true
2006
- }
2475
+ value: { customTools: [] }
2007
2476
  });
2008
2477
  }
2009
2478
  return;
2010
2479
  }
2011
2480
  try {
2012
- savePresetParams(presetRoot, templateName, void 0, void 0, record.variables !== null && typeof record.variables === "object" && !Array.isArray(record.variables) ? record.variables : void 0, typeof record.enabled === "boolean" ? record.enabled : void 0);
2481
+ const customTools = record.customTools;
2482
+ if (!Array.isArray(customTools)) {
2483
+ writeBridgeJson(res, 400, {
2484
+ ok: false,
2485
+ code: "custom-tools-invalid",
2486
+ message: "customTools 必须是数组"
2487
+ });
2488
+ return;
2489
+ }
2490
+ withPresetDoc(dir, (doc) => {
2491
+ if (customTools.length === 0) doc.deleteIn(["customTools"]);
2492
+ else doc.setIn(["customTools"], customTools);
2493
+ });
2013
2494
  afterOverridesChange?.();
2014
2495
  writeBridgeJson(res, 200, {
2015
2496
  ok: true,
2016
- value: { variables: record.variables }
2497
+ value: { customTools }
2017
2498
  });
2018
2499
  } catch (error) {
2019
2500
  writeBridgeJson(res, 409, {
2020
2501
  ok: false,
2021
- code: "preset-variables-rejected",
2022
- message: `模板变量保存失败:${error instanceof Error ? error.message : String(error)}`
2502
+ code: "custom-tools-rejected",
2503
+ message: `自定义工具保存失败:${error instanceof Error ? error.message : String(error)}`
2023
2504
  });
2024
2505
  }
2025
2506
  }
@@ -2214,7 +2695,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2214
2695
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.presetDelete,
2215
2696
  handler: async (req, res) => {
2216
2697
  if (!guard(req, res)) return;
2217
- ensureRegistered(sctx);
2218
2698
  const { body } = await readBridgeBody(req);
2219
2699
  const record = body ?? {};
2220
2700
  const id = typeof record.id === "string" ? record.id.trim() : "";
@@ -2259,7 +2739,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2259
2739
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.presetClone,
2260
2740
  handler: async (req, res) => {
2261
2741
  if (!guard(req, res)) return;
2262
- ensureRegistered(sctx);
2263
2742
  const { body } = await readBridgeBody(req);
2264
2743
  const record = body ?? {};
2265
2744
  const id = typeof record.id === "string" ? record.id.trim() : "";
@@ -2291,7 +2770,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2291
2770
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.presetDuplicate,
2292
2771
  handler: async (req, res) => {
2293
2772
  if (!guard(req, res)) return;
2294
- ensureRegistered(sctx);
2295
2773
  const { body } = await readBridgeBody(req);
2296
2774
  const record = body ?? {};
2297
2775
  const id = typeof record.id === "string" ? record.id.trim() : "";
@@ -2327,7 +2805,6 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2327
2805
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.presetOpen,
2328
2806
  handler: async (req, res) => {
2329
2807
  if (!guard(req, res)) return;
2330
- ensureRegistered(sctx);
2331
2808
  const { body } = await readBridgeBody(req);
2332
2809
  const record = body ?? {};
2333
2810
  const id = typeof record.id === "string" ? record.id.trim() : "";
@@ -2583,6 +3060,7 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2583
3060
  };
2584
3061
  }, "prompt-tool: settings bridge");
2585
3062
  });
3063
+ return { invalidateDescriptor: () => invalidateCachedDescriptor() };
2586
3064
  }
2587
3065
  //#endregion
2588
3066
  //#region src/runtime/character-tools.ts
@@ -2590,9 +3068,11 @@ const text$2 = (text) => [{
2590
3068
  type: "text",
2591
3069
  text
2592
3070
  }];
3071
+ /** 注册角色卡库模型工具;返回 disposer(预设切换重挂用)。enabled=false 返回空操作。 */
2593
3072
  function registerCharacterTools(ctx, host) {
2594
- ctx.inject(["tools"], (toolsCtx) => {
2595
- toolsCtx.tools.register(defineTool({
3073
+ const fiber = ctx.inject(["tools"], (toolsCtx) => {
3074
+ const disposers = [];
3075
+ disposers.push(toolsCtx.tools.register(defineTool({
2596
3076
  name: "character_list",
2597
3077
  description: "列出 SillyTavern 角色卡库:每张卡(id / 名称 / 描述 / 是否已导入当前预设)。导入角色卡、应用到当前预设或移除前先调用本工具获取 id。",
2598
3078
  parameters: {},
@@ -2627,8 +3107,8 @@ function registerCharacterTools(ctx, host) {
2627
3107
  render: (_args, value) => text$2(JSON.stringify(value.characters))
2628
3108
  },
2629
3109
  execute: async () => ({ characters: listCharacterCards(host.presetRoot(), host.templateName()) })
2630
- }));
2631
- toolsCtx.tools.register(defineTool({
3110
+ })));
3111
+ disposers.push(toolsCtx.tools.register(defineTool({
2632
3112
  name: "character_import",
2633
3113
  description: "导入 SillyTavern 角色卡(chara_card_v2/v3 JSON)到角色卡库:接收角色卡 JSON 文本内容(可先读取文件)。PNG 角色卡请让用户从 UI 角色管理页导入。导入后需调用 character_apply 应用到当前预设。",
2634
3114
  parameters: {
@@ -2671,8 +3151,8 @@ function registerCharacterTools(ctx, host) {
2671
3151
  name: result.name
2672
3152
  };
2673
3153
  }
2674
- }));
2675
- toolsCtx.tools.register(defineTool({
3154
+ })));
3155
+ disposers.push(toolsCtx.tools.register(defineTool({
2676
3156
  name: "character_apply",
2677
3157
  description: "把角色卡库中一张角色卡的参数(角色设定 / 系统提示 / 开场白 / 世界书 / 提示词库 / 采样参数)合并进当前激活预设(promptConfigs 带 chara-<id>- 前缀防冲突,params 合并,meta.importedCharacters 记录),并立即重建生成目录。重复应用幂等。",
2678
3158
  parameters: { id: {
@@ -2706,8 +3186,8 @@ function registerCharacterTools(ctx, host) {
2706
3186
  count: result.count
2707
3187
  };
2708
3188
  }
2709
- }));
2710
- toolsCtx.tools.register(defineTool({
3189
+ })));
3190
+ disposers.push(toolsCtx.tools.register(defineTool({
2711
3191
  name: "character_remove",
2712
3192
  description: "从当前激活预设移除一张已导入角色卡的参数(删 chara-<id>- 前缀配置、该卡声明的 params 键、meta.importedCharacters 除名),并立即重建生成目录。角色卡库条目不受影响。",
2713
3193
  parameters: { id: {
@@ -2741,8 +3221,8 @@ function registerCharacterTools(ctx, host) {
2741
3221
  count: result.count
2742
3222
  };
2743
3223
  }
2744
- }));
2745
- toolsCtx.tools.register(defineTool({
3224
+ })));
3225
+ disposers.push(toolsCtx.tools.register(defineTool({
2746
3226
  name: "character_delete",
2747
3227
  description: "从角色卡库删除一张角色卡(含其转换参数与头像)。已导入当前预设的参数不受影响(如需清理请先调用 character_remove)。",
2748
3228
  parameters: { id: {
@@ -2766,8 +3246,14 @@ function registerCharacterTools(ctx, host) {
2766
3246
  if (!result.ok) throw new Error(result.message);
2767
3247
  return { id: args.id };
2768
3248
  }
2769
- }));
3249
+ })));
3250
+ return () => {
3251
+ for (const dispose of disposers) dispose();
3252
+ };
2770
3253
  });
3254
+ return () => {
3255
+ fiber.dispose();
3256
+ };
2771
3257
  }
2772
3258
  //#endregion
2773
3259
  //#region src/runtime/world-book-tools.ts
@@ -2775,26 +3261,6 @@ const text$1 = (text) => [{
2775
3261
  type: "text",
2776
3262
  text
2777
3263
  }];
2778
- function readConfigs(dir) {
2779
- const file = join(dir, "preset.yml");
2780
- const current = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" }).toJS();
2781
- return Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
2782
- }
2783
- function writeConfigs(dir, configs) {
2784
- const file = join(dir, "preset.yml");
2785
- const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
2786
- doc.setIn(["promptConfigs"], configs);
2787
- writeFileSync(file, doc.toString(), "utf8");
2788
- }
2789
- /** 追加记忆文件(角色卡 memory.md 或预设 memory.md)。 */
2790
- function appendMemory(file, note) {
2791
- const content = note.trim();
2792
- if (content.length === 0) return;
2793
- mkdirSync(join(file, ".."), { recursive: true });
2794
- const line = `\n- [${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ")}] ${content}\n`;
2795
- if (existsSync(file)) appendFileSync(file, line, "utf8");
2796
- else writeFileSync(file, `# 本地记忆\n${line}`, "utf8");
2797
- }
2798
3264
  /** 从条目 id 解析来源角色卡(chara-<cardId>- 前缀,cardId 可含连字符:遍历库目录取最长匹配)。 */
2799
3265
  function sourceCardId(presetRoot, entryId) {
2800
3266
  const root = join(presetRoot, ".characters");
@@ -2808,16 +3274,22 @@ function sourceCardId(presetRoot, entryId) {
2808
3274
  } catch {}
2809
3275
  return best;
2810
3276
  }
3277
+ /** 注册世界书条目级模型工具;返回 disposer(预设切换重挂用)。enabled=false 返回空操作。 */
2811
3278
  function registerWorldBookTools(ctx, host) {
2812
- ctx.inject(["tools"], (toolsCtx) => {
3279
+ const fiber = ctx.inject(["tools"], (toolsCtx) => {
3280
+ const disposers = [];
2813
3281
  /** note 归属写入:角色卡条目 → 卡记忆;其他 → 预设记忆。 */
2814
3282
  const writeNote = (entryId, note) => {
2815
3283
  if (note === void 0 || note.trim().length === 0) return;
2816
3284
  const cardId = entryId !== void 0 ? sourceCardId(host.presetRoot(), entryId) : void 0;
2817
- if (cardId !== void 0) appendMemory(join(host.presetRoot(), ".characters", cardId, "memory.md"), note);
2818
- else appendMemory(join(host.activeDir(), "memory.md"), note);
3285
+ if (cardId !== void 0) {
3286
+ appendMemoryFile(join(host.presetRoot(), ".characters", cardId, "memory.md"), note, "# 角色记忆");
3287
+ try {
3288
+ syncImportedCharacterMemory(host.presetRoot(), basename(host.activeDir()), cardId);
3289
+ } catch {}
3290
+ } else appendMemoryFile(join(host.activeDir(), "memory.md"), note, "# 本地记忆");
2819
3291
  };
2820
- toolsCtx.tools.register(defineTool({
3292
+ disposers.push(toolsCtx.tools.register(defineTool({
2821
3293
  name: "world_book_list",
2822
3294
  description: "列出当前预设的世界书条目(world-book 策略配置:id / 名称 / 关键字 / 常驻 / 启用)。世界书 = 上下文条目:无 keys 的全局条目每次注入,有 keys 条目命中聊天内容才注入。增删改前先调用本工具获取 id。",
2823
3295
  parameters: {},
@@ -2853,7 +3325,7 @@ function registerWorldBookTools(ctx, host) {
2853
3325
  render: (_args, value) => text$1(JSON.stringify(value.entries))
2854
3326
  },
2855
3327
  execute: async () => {
2856
- return { entries: readConfigs(host.activeDir()).filter((config) => config.strategy === "world-book").map((config) => ({
3328
+ return { entries: listWorldBookEntries(host.activeDir()).map((config) => ({
2857
3329
  id: String(config.id ?? ""),
2858
3330
  name: String(config.name ?? config.id ?? ""),
2859
3331
  keys: Array.isArray(config.params && config.params.keys) ? config.params.keys : void 0,
@@ -2861,8 +3333,8 @@ function registerWorldBookTools(ctx, host) {
2861
3333
  enabled: config.enabled !== false
2862
3334
  })) };
2863
3335
  }
2864
- }));
2865
- toolsCtx.tools.register(defineTool({
3336
+ })));
3337
+ disposers.push(toolsCtx.tools.register(defineTool({
2866
3338
  name: "world_book_upsert",
2867
3339
  description: "新增或更新当前预设的一条世界书条目(world-book 策略配置):按 id 更新(不存在则新增,id 自动生成 lore-<n>)。constant=true 常驻注入;否则命中 keys(或 secondaryKeys)任一关键字注入;无 keys 条目按全局每次注入。note 可选:写入来源角色卡的持久记忆(memory.md,条目 id 带 chara-<卡>- 前缀时)或预设记忆——持久记忆跨会话跟随角色卡(与 session_var 会话变量的临时状态不同,适合长期关系记录)。写盘后立即重建生成目录。",
2868
3340
  parameters: {
@@ -2926,37 +3398,29 @@ function registerWorldBookTools(ctx, host) {
2926
3398
  },
2927
3399
  execute: async (args) => {
2928
3400
  const dir = host.activeDir();
2929
- const configs = readConfigs(dir);
2930
3401
  const targetId = args.id !== void 0 && args.id.length > 0 ? args.id : `lore-${Date.now().toString(36)}`;
2931
- const params = {
2932
- constant: args.constant === true,
2933
- ...Array.isArray(args.keys) && args.keys.length > 0 ? { keys: args.keys } : {},
2934
- ...Array.isArray(args.secondaryKeys) && args.secondaryKeys.length > 0 ? { secondaryKeys: args.secondaryKeys } : {}
2935
- };
2936
- const entry = {
2937
- id: targetId,
2938
- name: args.name,
2939
- strategy: "world-book",
2940
- order: typeof args.order === "number" ? args.order : 100,
2941
- text: args.content,
2942
- layer: "pre-step",
2943
- position: "before-all",
2944
- params
2945
- };
2946
- if (args.enabled === false) entry.enabled = false;
2947
- const existing = configs.findIndex((config) => String(config.id ?? "") === targetId);
2948
- if (existing >= 0) configs[existing] = entry;
2949
- else configs.push(entry);
2950
- writeConfigs(dir, configs);
3402
+ const count = upsertWorldBookEntry(dir, {
3403
+ ...buildWorldBookEntry({
3404
+ id: targetId,
3405
+ name: args.name,
3406
+ text: args.content,
3407
+ order: typeof args.order === "number" ? args.order : void 0,
3408
+ enabled: args.enabled === false ? false : void 0,
3409
+ constant: args.constant === true,
3410
+ keys: Array.isArray(args.keys) && args.keys.length > 0 ? args.keys : void 0,
3411
+ secondaryKeys: Array.isArray(args.secondaryKeys) && args.secondaryKeys.length > 0 ? args.secondaryKeys : void 0
3412
+ }),
3413
+ id: targetId
3414
+ });
2951
3415
  writeNote(targetId, typeof args.note === "string" ? args.note : "");
2952
3416
  host.rebuild();
2953
3417
  return {
2954
3418
  id: targetId,
2955
- count: configs.filter((config) => config.strategy === "world-book").length
3419
+ count
2956
3420
  };
2957
3421
  }
2958
- }));
2959
- toolsCtx.tools.register(defineTool({
3422
+ })));
3423
+ disposers.push(toolsCtx.tools.register(defineTool({
2960
3424
  name: "world_book_delete",
2961
3425
  description: "删除当前预设的一条世界书条目(world_book_list 获取 id)。note 可选:写入来源角色卡持久记忆(memory.md,跨会话跟随角色卡)或预设记忆。删除后立即重建生成目录。",
2962
3426
  parameters: {
@@ -2988,20 +3452,22 @@ function registerWorldBookTools(ctx, host) {
2988
3452
  render: (_args, value) => text$1(`世界书条目 ${value.id} 已删除(剩余 ${value.count} 条),生成目录已重建。`)
2989
3453
  },
2990
3454
  execute: async (args) => {
2991
- const dir = host.activeDir();
2992
- const configs = readConfigs(dir);
2993
- const kept = configs.filter((config) => !(config.strategy === "world-book" && String(config.id ?? "") === args.id));
2994
- if (kept.length === configs.length) throw new Error(`世界书条目 ${args.id} 不存在`);
2995
- writeConfigs(dir, kept);
3455
+ const keptCount = deleteWorldBookEntry(host.activeDir(), args.id);
2996
3456
  writeNote(args.id, typeof args.note === "string" ? args.note : "");
2997
3457
  host.rebuild();
2998
3458
  return {
2999
3459
  id: args.id,
3000
- count: kept.filter((config) => config.strategy === "world-book").length
3460
+ count: keptCount
3001
3461
  };
3002
3462
  }
3003
- }));
3463
+ })));
3464
+ return () => {
3465
+ for (const dispose of disposers) dispose();
3466
+ };
3004
3467
  });
3468
+ return () => {
3469
+ fiber.dispose();
3470
+ };
3005
3471
  }
3006
3472
  //#endregion
3007
3473
  //#region src/runtime/session-var-tools.ts
@@ -3009,9 +3475,11 @@ const text = (value) => [{
3009
3475
  type: "text",
3010
3476
  text: value
3011
3477
  }];
3478
+ /** 注册会话变量模型工具;返回 disposer(预设切换重挂用)。enabled=false 返回空操作。 */
3012
3479
  function registerSessionVarTools(ctx) {
3013
- ctx.inject(["tools"], (toolsCtx) => {
3014
- toolsCtx.tools.register(defineTool({
3480
+ const fiber = ctx.inject(["tools"], (toolsCtx) => {
3481
+ const disposers = [];
3482
+ disposers.push(toolsCtx.tools.register(defineTool({
3015
3483
  name: "session_var",
3016
3484
  description: "会话变量管理(SillyTavern setvar/getvar 语义):list 查看当前会话全部变量、get 读取、set 设置、clear 清除。提示词/世界书文本中的 {{变量名}} 会在注入时替换为会话变量值(会话级覆盖预设默认值);适合维护角色状态(如 {{心情}}、{{接受度}} 等)。注意:会话变量仅存于当前会话(结束即失);跨会话长期记忆请用 world_book 工具的 note 参数写入角色卡记忆(持久,跟随角色卡)。",
3017
3485
  parameters: {
@@ -3106,8 +3574,14 @@ function registerSessionVarTools(ctx) {
3106
3574
  error: `未知 action: ${action}`
3107
3575
  };
3108
3576
  }
3109
- }));
3577
+ })));
3578
+ return () => {
3579
+ for (const dispose of disposers) dispose();
3580
+ };
3110
3581
  });
3582
+ return () => {
3583
+ fiber.dispose();
3584
+ };
3111
3585
  }
3112
3586
  //#endregion
3113
3587
  //#region src/runtime/tui.ts
@@ -3121,6 +3595,7 @@ const TUI_GLOBAL_SWITCHES = [
3121
3595
  const TUI_PARAM_SWITCHES = [
3122
3596
  ["injectPrompt", "锚定确认后注入 preset.md"],
3123
3597
  ["firstTurnAnchor", "追加任务引导"],
3598
+ ["guideEnabled", "每轮引导(独立开关;缺省跟随锚定)"],
3124
3599
  ["firstTurnCustom", "使用自定义引导(首句)"],
3125
3600
  ["guideCustom", "使用自定义引导(每轮)"],
3126
3601
  ["usePtcMode", "使用 PTC 模式"]
@@ -3291,18 +3766,56 @@ function parseTuiBoolean(token, current) {
3291
3766
  if (token === "off") return false;
3292
3767
  if (token === "toggle") return !current;
3293
3768
  }
3769
+ function asCommandAction(token) {
3770
+ return token === "on" || token === "off" || token === "toggle" ? token : void 0;
3771
+ }
3772
+ /**
3773
+ * 解析可含空格的标识符与末尾动作。
3774
+ *
3775
+ * 斜杠命令没有 shell 引号语义,因此动作只认“最后一个独立 token”。config 的
3776
+ * 详情形态没有动作;只有去掉末尾动作后的候选 id 已存在时才把末尾 token 视为
3777
+ * 动作,避免把普通 id 误拆。
3778
+ */
3779
+ function parseIdentifierAndAction(tokens, exists) {
3780
+ if (tokens.length === 0) return { id: "" };
3781
+ const last = tokens[tokens.length - 1];
3782
+ const action = asCommandAction(last ?? "");
3783
+ if (action !== void 0 && tokens.length >= 2) {
3784
+ const id = tokens.slice(0, -1).join(" ");
3785
+ if (exists(id)) return {
3786
+ id,
3787
+ action
3788
+ };
3789
+ }
3790
+ return { id: tokens.join(" ") };
3791
+ }
3294
3792
  /** 通过 DSH 命令注册表暴露 /prompt-tool,Web 与 dsh-tui 都能执行。 */
3295
3793
  function registerTuiCommand(ctx, ns, getSource, getModelsState, getModelCatalog, getPresetConfigsDir, savePresetParam) {
3296
3794
  ctx.inject(["settings"], (sctx) => {
3297
3795
  return sctx.commands.register({
3298
3796
  name: "prompt-tool",
3299
3797
  description: "提示词工具:查看或切换本插件开关",
3300
- input: { hint: "status | on/off/toggle <开关> | skill <目录名> on/off | config <id> [on/off/toggle]" },
3798
+ input: { hint: "status | on/off/toggle <开关> | skill <目录名> on/off/toggle | config <id> [on/off/toggle]" },
3301
3799
  handler: async (invocation) => {
3302
3800
  const usage = () => ({
3303
3801
  kind: "error",
3304
- text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|firstTurnAnchor|firstTurnCustom|guideCustom|usePtcMode>\n /prompt-tool skill <技能目录名> on|off|toggle\n /prompt-tool config <id>\n /prompt-tool config <id> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
3802
+ text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|firstTurnAnchor|firstTurnCustom|guideEnabled|guideCustom|usePtcMode>\n /prompt-tool skill <技能目录名> on|off|toggle(目录名可含空格)\n /prompt-tool config <id>(id 可含空格)\n /prompt-tool config <id> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
3305
3803
  });
3804
+ const persistPresetParam = async (key, value) => {
3805
+ if (savePresetParam === void 0) return {
3806
+ kind: "error",
3807
+ text: `无法保存 ${key}:预设参数保存回调不可用`
3808
+ };
3809
+ try {
3810
+ await savePresetParam(key, value);
3811
+ return;
3812
+ } catch (error) {
3813
+ return {
3814
+ kind: "error",
3815
+ text: `保存 ${key} 失败:${error instanceof Error ? error.message : String(error)}`
3816
+ };
3817
+ }
3818
+ };
3306
3819
  const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
3307
3820
  const source = getSource();
3308
3821
  const presetDir = getPresetConfigsDir?.();
@@ -3320,10 +3833,9 @@ function registerTuiCommand(ctx, ns, getSource, getModelsState, getModelCatalog,
3320
3833
  };
3321
3834
  }
3322
3835
  if (tokens[0] === "skill") {
3323
- const folder = tokens[1];
3324
- if (folder === void 0) return usage();
3325
- const current = source.skillSwitches[folder] !== false;
3326
- const next = parseTuiBoolean(tokens[2], current);
3836
+ const { id: folder, action } = parseIdentifierAndAction(tokens.slice(1), () => true);
3837
+ if (folder.length === 0) return usage();
3838
+ const next = parseTuiBoolean(action, source.skillSwitches[folder] !== false);
3327
3839
  if (next === void 0) return usage();
3328
3840
  await sctx.settings.mutate(ns, [{
3329
3841
  op: "set",
@@ -3338,25 +3850,24 @@ ${renderTuiStatus(getSource(), readPresetParams(getPresetConfigsDir?.()), resolv
3338
3850
  };
3339
3851
  }
3340
3852
  if (tokens[0] === "config") {
3341
- const id = tokens[1];
3342
- if (id === void 0) return usage();
3853
+ const { id, action } = parseIdentifierAndAction(tokens.slice(1), (candidate) => promptConfigs.some((config) => config.id === candidate));
3854
+ if (id.length === 0) return usage();
3343
3855
  const current = promptConfigs.find((config) => config.id === id);
3344
3856
  if (current === void 0) return {
3345
3857
  kind: "error",
3346
3858
  text: `未找到提示词配置 ${id};用 /prompt-tool status 查看全部 id。`
3347
3859
  };
3348
- const action = tokens[2];
3349
3860
  if (action === void 0) return {
3350
3861
  kind: "success",
3351
3862
  text: renderConfigDetail(source, id, promptConfigs)
3352
3863
  };
3353
3864
  const next = parseTuiBoolean(action, current.enabled !== false);
3354
3865
  if (next === void 0) return usage();
3355
- const nextConfigs = promptConfigs.map((config) => config.id === id ? {
3866
+ const failure = await persistPresetParam("promptConfigs", promptConfigs.map((config) => config.id === id ? {
3356
3867
  ...config,
3357
3868
  enabled: next
3358
- } : config);
3359
- savePresetParam?.("promptConfigs", nextConfigs);
3869
+ } : config));
3870
+ if (failure !== void 0) return failure;
3360
3871
  return {
3361
3872
  kind: "success",
3362
3873
  text: `已把提示词配置 ${id} 设为 ${next ? "开" : "关"}
@@ -3371,7 +3882,8 @@ ${renderConfigDetail(getSource(), id, resolvePromptConfigs(getPresetConfigsDir?.
3371
3882
  kind: "error",
3372
3883
  text: "bootstrapMaxTokens 需要非负整数:0 关闭封顶,正整数设置首轮 maxTokens。"
3373
3884
  };
3374
- savePresetParam?.("bootstrapMaxTokens", value);
3885
+ const failure = await persistPresetParam("bootstrapMaxTokens", value);
3886
+ if (failure !== void 0) return failure;
3375
3887
  return {
3376
3888
  kind: "success",
3377
3889
  text: `已把 bootstrapMaxTokens 设为 ${value === 0 ? "关闭(不设封顶)" : String(value)}
@@ -3396,7 +3908,10 @@ ${renderTuiStatus(getSource(), readPresetParams(getPresetConfigsDir?.()), resolv
3396
3908
  path: [key],
3397
3909
  value: next
3398
3910
  }]);
3399
- else savePresetParam?.(key, next);
3911
+ else {
3912
+ const failure = await persistPresetParam(key, next);
3913
+ if (failure !== void 0) return failure;
3914
+ }
3400
3915
  return {
3401
3916
  kind: "success",
3402
3917
  text: `已把 ${key} 设为 ${next ? "开" : "关"}
@@ -3408,25 +3923,6 @@ ${renderTuiStatus(getSource(), readPresetParams(getPresetConfigsDir?.()), resolv
3408
3923
  });
3409
3924
  }
3410
3925
  //#endregion
3411
- //#region src/runtime/settings-registration.ts
3412
- /**
3413
- * 确保 `ns` 已注册:已注册(describe 命中)直接返回 true;未注册时补注册。
3414
- * 官方 installSettingsSection 的 inject 回调只在 settings 服务首次可用时跑一次,
3415
- * settings 服务实例被替换(provider fiber reload)后注册丢失且不自动重建;
3416
- * 本函数由 Web bridge 每次请求前调用兜底。
3417
- */
3418
- function ensureSettingsRegistered(sctx, ns, schema, hooks) {
3419
- if (sctx.settings.describe({ redactSecrets: true }).some((entry) => String(entry.ns) === String(ns))) return true;
3420
- try {
3421
- const scope = sctx.settings.register(ns, schema, { base: hooks.base() });
3422
- hooks.onRegistered(scope);
3423
- return true;
3424
- } catch (error) {
3425
- hooks.onError(error instanceof Error ? error.message : String(error));
3426
- return false;
3427
- }
3428
- }
3429
- //#endregion
3430
3926
  //#region src/runtime/agents-file.ts
3431
3927
  /** AGENTS.md 常驻规则的受管块读写(保留文件其余内容)。 */
3432
3928
  const RESIDENT_AGENTS_BEGIN = "# === prompt-tool managed block begin ===";
@@ -3547,8 +4043,9 @@ function runtimeOf(options, prompt) {
3547
4043
  firstTurnText: typeof options.firstTurnText === "string" ? options.firstTurnText : "",
3548
4044
  guideCustom: options.guideCustom === true,
3549
4045
  guideText: typeof options.guideText === "string" ? options.guideText : "",
4046
+ guideEnabled: typeof options.guideEnabled === "boolean" ? options.guideEnabled : void 0,
3550
4047
  injectPrompt: options.injectPrompt !== false,
3551
- usePtcMode: options.usePtcMode !== false,
4048
+ usePtcMode: typeof options.usePtcMode === "boolean" ? options.usePtcMode : void 0,
3552
4049
  bootstrapMaxTokens: Number.isSafeInteger(options.bootstrapMaxTokens) ? options.bootstrapMaxTokens : 0,
3553
4050
  modelProvider: typeof options.modelProvider === "string" && options.modelProvider.length > 0 ? options.modelProvider : "",
3554
4051
  modelName: typeof options.modelName === "string" && options.modelName.length > 0 ? options.modelName : "",
@@ -3560,12 +4057,19 @@ function runtimeOf(options, prompt) {
3560
4057
  subagentReasoningEffort: typeof options.subagentReasoningEffort === "string" && options.subagentReasoningEffort.length > 0 ? options.subagentReasoningEffort : void 0,
3561
4058
  subagentTemperature: typeof options.subagentTemperature === "string" && options.subagentTemperature.length > 0 ? options.subagentTemperature : void 0,
3562
4059
  subagentMaxTokens: typeof options.subagentMaxTokens === "string" && options.subagentMaxTokens.length > 0 ? options.subagentMaxTokens : void 0,
3563
- subagentPersona: typeof options.subagentPersona === "string" && options.subagentPersona.length > 0 ? options.subagentPersona : "",
3564
4060
  toolFilterAllow: options.toolFilterAllow !== void 0 && (Array.isArray(options.toolFilterAllow) ? options.toolFilterAllow.length > 0 : String(options.toolFilterAllow).trim().length > 0) ? options.toolFilterAllow : void 0,
3565
4061
  toolFilterDeny: options.toolFilterDeny !== void 0 && (Array.isArray(options.toolFilterDeny) ? options.toolFilterDeny.length > 0 : String(options.toolFilterDeny).trim().length > 0) ? options.toolFilterDeny : void 0,
3566
4062
  maxDepth: options.maxDepth,
3567
4063
  allowKinds: options.allowKinds,
3568
- firstTurnWord: typeof options.firstTurnWord === "string" && options.firstTurnWord.length > 0 ? options.firstTurnWord : void 0
4064
+ firstTurnWord: typeof options.firstTurnWord === "string" && options.firstTurnWord.length > 0 ? options.firstTurnWord : void 0,
4065
+ anchorTurn: typeof options.anchorTurn === "boolean" ? options.anchorTurn : void 0,
4066
+ anchorTurnText: typeof options.anchorTurnText === "string" ? options.anchorTurnText : void 0,
4067
+ deliberationGate: typeof options.deliberationGate === "boolean" ? options.deliberationGate : void 0,
4068
+ deliberationMinChars: Number.isSafeInteger(options.deliberationMinChars) ? options.deliberationMinChars : void 0,
4069
+ deliberationMaxGatesPerTurn: Number.isSafeInteger(options.deliberationMaxGatesPerTurn) ? options.deliberationMaxGatesPerTurn : void 0,
4070
+ cotDrip: typeof options.cotDrip === "boolean" ? options.cotDrip : void 0,
4071
+ cotDripEvery: Number.isSafeInteger(options.cotDripEvery) ? options.cotDripEvery : void 0,
4072
+ cotDripMaxPerTurn: Number.isSafeInteger(options.cotDripMaxPerTurn) ? options.cotDripMaxPerTurn : void 0
3569
4073
  };
3570
4074
  }
3571
4075
  /** 模型参数(思维程度/温度/输出上限)→ agent-request 提示词配置(官方 LlmCallConfig patch 浅合并)。
@@ -3609,6 +4113,8 @@ function writePreset(prompt, options) {
3609
4113
  const presetDir = options.presetDir.trim().length > 0 ? options.presetDir : DEFAULT_PRESET_DIR;
3610
4114
  const templateName = typeof options.presetTemplate === "string" && options.presetTemplate.trim().length > 0 ? options.presetTemplate.trim() : "anchored";
3611
4115
  if (!/^[a-z0-9][a-z0-9-]*$/.test(templateName)) throw new Error(`invalid presetTemplate ${JSON.stringify(templateName)}: must match official agent-presets id /^[a-z0-9][a-z0-9-]*$/`);
4116
+ const outputId = typeof options.outputId === "string" && options.outputId.trim().length > 0 ? options.outputId.trim() : templateName;
4117
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(outputId)) throw new Error(`invalid outputId ${JSON.stringify(outputId)}: must match official agent-presets id /^[a-z0-9][a-z0-9-]*$/`);
3612
4118
  const templateDir = resolvePresetDir(templateName);
3613
4119
  const spec = loadPresetSpec(templateDir);
3614
4120
  const worldBook = spec.worldBook;
@@ -3638,8 +4144,7 @@ function writePreset(prompt, options) {
3638
4144
  ...keys.length > 0 ? { keys } : {},
3639
4145
  ...secondaryKeys.length > 0 ? { secondaryKeys } : {},
3640
4146
  ...entry.caseSensitive === true ? { caseSensitive: true } : {},
3641
- ...entry.wholeWords === true ? { wholeWords: true } : {},
3642
- ...entry.useRegex === true ? { useRegex: true } : {}
4147
+ ...entry.wholeWords === true ? { wholeWords: true } : {}
3643
4148
  }
3644
4149
  });
3645
4150
  }
@@ -3655,27 +4160,37 @@ function writePreset(prompt, options) {
3655
4160
  }
3656
4161
  const runtime = runtimeOf(options, prompt);
3657
4162
  const params = resolvePresetParams(spec, runtime);
3658
- const targetDir = join(presetDir, templateName);
4163
+ const targetDir = join(presetDir, outputId);
3659
4164
  mkdirSync(presetDir, { recursive: true });
3660
- const tmpDir = mkdtempSync(join(presetDir, `.${templateName}.tmp-`));
4165
+ const tmpDir = mkdtempSync(join(presetDir, `.${outputId}.tmp-`));
3661
4166
  const outDir = tmpDir;
3662
4167
  try {
3663
4168
  const overridesSrc = join(targetDir, "prompt-tool.overrides.yml");
3664
4169
  if (existsSync(overridesSrc)) cpSync(overridesSrc, join(outDir, "prompt-tool.overrides.yml"), { force: true });
3665
4170
  const composition = renderComposition(spec, runtime, templateDir);
3666
4171
  assertCompositionArray(composition, spec);
3667
- const subComposition = composition.replaceAll("./engine/", "../.engine/").replaceAll("../prompt-configs", `../${templateName}/prompt-configs`);
4172
+ const subComposition = composition.replaceAll("./engine/", "../.engine/").replaceAll("../prompt-configs", `../${outputId}/prompt-configs`);
3668
4173
  writeFileSync(join(outDir, "agent.cordis.yml"), subComposition, "utf8");
4174
+ const aliasSuffix = "(旧会话兼容)";
4175
+ const aliasName = options.aliasOf === true ? typeof spec.name === "string" && spec.name.length > 0 ? spec.name : outputId : void 0;
4176
+ const displayName = aliasName !== void 0 && !aliasName.endsWith(aliasSuffix) ? aliasName + aliasSuffix : aliasName;
3669
4177
  const meta = spec.meta !== null && typeof spec.meta === "object" ? spec.meta : {};
3670
- const existingPresetYaml = existsSync(join(targetDir, "preset.yml")) ? readFileSync(join(targetDir, "preset.yml"), "utf8") : void 0;
4178
+ const sourceYamlPath = options.aliasOf === true ? join(presetDir, templateName, "preset.yml") : join(targetDir, "preset.yml");
4179
+ const existingPresetYaml = existsSync(sourceYamlPath) ? readFileSync(sourceYamlPath, "utf8") : void 0;
3671
4180
  if (existingPresetYaml !== void 0 && existingPresetYaml.trim().length > 0) {
3672
4181
  const doc = parseDocument(existingPresetYaml, { logLevel: "silent" });
3673
4182
  doc.setIn(["order"], options.presetOrder);
3674
- if (typeof spec.name === "string" && spec.name.length > 0) doc.setIn(["name"], spec.name);
4183
+ if (typeof spec.name === "string" && spec.name.length > 0) doc.setIn(["name"], displayName ?? spec.name);
4184
+ if (options.aliasOf === true) doc.setIn(["id"], outputId);
3675
4185
  if (typeof spec.description === "string" && spec.description.length > 0) doc.setIn(["description"], spec.description);
3676
4186
  if (Object.keys(meta).length > 0) doc.setIn(["meta"], meta);
3677
4187
  writeFileSync(join(outDir, "preset.yml"), doc.toString(), "utf8");
3678
- } else writeFileSync(join(outDir, "preset.yml"), stringify({
4188
+ } else writeFileSync(join(outDir, "preset.yml"), stringify(displayName !== void 0 ? {
4189
+ ...meta,
4190
+ id: outputId,
4191
+ name: displayName,
4192
+ order: options.presetOrder
4193
+ } : {
3679
4194
  ...meta,
3680
4195
  order: options.presetOrder
3681
4196
  }) + "\n", "utf8");
@@ -3742,7 +4257,7 @@ function writePreset(prompt, options) {
3742
4257
  next.params = {
3743
4258
  ...next.params,
3744
4259
  useCustom: params.firstTurnCustom === true,
3745
- firstTurnText: asString(params.firstTurnText),
4260
+ text: asString(params.firstTurnText),
3746
4261
  buildPattern: asString(params.buildPattern),
3747
4262
  complexPattern: asString(params.complexPattern),
3748
4263
  firstTurnBuild: asString(params.firstTurnBuild),
@@ -3750,14 +4265,15 @@ function writePreset(prompt, options) {
3750
4265
  firstTurnDeep: asString(params.firstTurnDeep)
3751
4266
  };
3752
4267
  } else if (next.id === "router-guide") {
3753
- next.enabled = params.firstTurnAnchor === true;
3754
- const useCustom = params.firstTurnAnchor === true && params.guideCustom === true;
4268
+ const guideEnabled = params.guideEnabled === void 0 ? params.firstTurnAnchor === true : params.guideEnabled === true;
4269
+ next.enabled = guideEnabled;
4270
+ const useCustom = guideEnabled && params.guideCustom === true;
3755
4271
  next.modelScope = useCustom ? "all" : "flash";
3756
4272
  next.params = {
3757
4273
  ...next.params,
3758
4274
  useCustom,
3759
4275
  text: asString(params.guideText),
3760
- guideComplexPattern: asString(params.guideComplexPattern),
4276
+ complexPattern: asString(params.complexPattern) || asString(params.guideComplexPattern),
3761
4277
  guideWeak: asString(params.guideWeak),
3762
4278
  guideDeep: asString(params.guideDeep)
3763
4279
  };
@@ -3769,10 +4285,10 @@ function writePreset(prompt, options) {
3769
4285
  const presetVariables = {};
3770
4286
  for (const [key, value] of Object.entries(spec.params ?? {})) {
3771
4287
  if (PARAM_KEYS.has(key)) continue;
3772
- const text = String(value);
3773
- if (text.length > 0) presetVariables[key] = text;
4288
+ if (value === void 0 || value === null) continue;
4289
+ presetVariables[key] = String(value);
3774
4290
  }
3775
- for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string" && value.length > 0) presetVariables[key] = value;
4291
+ for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string") presetVariables[key] = value;
3776
4292
  const variablesEnabled = spec.variablesEnabled !== false;
3777
4293
  const presetVariableKeys = new Set(Object.keys(presetVariables));
3778
4294
  if (variablesEnabled && presetVariableKeys.size > 0) writeFileSync(join(promptConfigsDir, "variables.yml"), stringify(presetVariables), "utf8");
@@ -3781,15 +4297,30 @@ function writePreset(prompt, options) {
3781
4297
  delete config.text;
3782
4298
  config.texts = [];
3783
4299
  config.enabled = params.injectPrompt !== false && prompt.trim().length > 0;
4300
+ const explicitWord = asString(config.params?.firstTurnWord ?? params.firstTurnWord, "");
4301
+ const signalWord = (text) => {
4302
+ const sentence = /the exact sentence:\s*([A-Za-z]+)/i.exec(text);
4303
+ if (sentence !== null && sentence[1] !== void 0) return sentence[1].toLowerCase();
4304
+ const first = /^\P{L}*([\p{L}]+)/u.exec(text.trim());
4305
+ return first !== null && first[1] !== void 0 ? first[1].toLowerCase() : void 0;
4306
+ };
4307
+ const derivedWords = [...new Set([
4308
+ signalWord(asString(params.firstTurnText)),
4309
+ signalWord(asString(params.firstTurnBuild)),
4310
+ signalWord(asString(params.firstTurnInspect)),
4311
+ signalWord(asString(params.firstTurnDeep))
4312
+ ].filter((word) => word !== void 0))];
4313
+ const anchorWords = explicitWord.length > 0 ? [explicitWord] : derivedWords;
3784
4314
  config.params = {
3785
4315
  ...config.params,
3786
4316
  text: prompt,
3787
- firstTurnWord: asString(config.params?.firstTurnWord ?? params.firstTurnWord, "we")
4317
+ firstTurnWord: explicitWord.length > 0 ? explicitWord : anchorWords[0] ?? "we",
4318
+ anchorWords
3788
4319
  };
3789
4320
  }
3790
4321
  if (config.fill === "instruction-hint") config.params = {
3791
4322
  ...config.params,
3792
- agentsInstructionPath: `../${templateName}/agents-instruction.md`,
4323
+ agentsInstructionPath: `../${outputId}/agents-instruction.md`,
3793
4324
  ...options.injectAgentsPrompt === true ? { text: asString(options.agentsInstructionText) } : {}
3794
4325
  };
3795
4326
  if (!variablesEnabled && presetVariableKeys.size > 0) {
@@ -3803,6 +4334,18 @@ function writePreset(prompt, options) {
3803
4334
  }
3804
4335
  writeFileSync(join(promptConfigsDir, `${String(index * 10).padStart(2, "0")}-${config.id}.yml`), renderPromptConfigYaml(config), "utf8");
3805
4336
  }
4337
+ const customToolsDir = join(outDir, "custom-tools");
4338
+ rmSync(customToolsDir, {
4339
+ recursive: true,
4340
+ force: true
4341
+ });
4342
+ mkdirSync(customToolsDir, { recursive: true });
4343
+ const customTools = Array.isArray(spec.customTools) ? spec.customTools : [];
4344
+ for (const [index, tool] of customTools.entries()) {
4345
+ if (tool === null || typeof tool !== "object" || Array.isArray(tool) || typeof tool.id !== "string" || String(tool.id).length === 0) continue;
4346
+ const toolId = String(tool.id);
4347
+ writeFileSync(join(customToolsDir, `${String(index + 1).padStart(2, "0")}-${toolId}.yml`), stringify(tool, { lineWidth: 0 }), "utf8");
4348
+ }
3806
4349
  for (const legacy of spec.legacyCleanup ?? []) rmSync(join(outDir, legacy), { force: true });
3807
4350
  const agentsInstructionPath = join(outDir, "agents-instruction.md");
3808
4351
  if (typeof options.agentsInstructionText === "string" && options.agentsInstructionText.trim().length > 0) writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
@@ -3855,9 +4398,10 @@ function writePreset(prompt, options) {
3855
4398
  function migrateLegacyLayout(presetRoot, legacyUserPresets) {
3856
4399
  const legacyContainer = join(presetRoot, "prompt-tool");
3857
4400
  const sharedEngine = join(presetRoot, ".engine");
4401
+ const isLegacyContainer = existsSync(legacyContainer) && !existsSync(join(legacyContainer, "preset.yml"));
3858
4402
  let migrated = false;
3859
4403
  const legacyEngine = join(legacyContainer, "engine");
3860
- if (existsSync(legacyEngine) && !existsSync(sharedEngine)) try {
4404
+ if (isLegacyContainer && existsSync(legacyEngine) && !existsSync(sharedEngine)) try {
3861
4405
  mkdirSync(presetRoot, { recursive: true });
3862
4406
  renameSync(legacyEngine, sharedEngine);
3863
4407
  migrated = true;
@@ -3884,7 +4428,7 @@ function migrateLegacyLayout(presetRoot, legacyUserPresets) {
3884
4428
  }
3885
4429
  })();
3886
4430
  const stamp = Date.now().toString(36);
3887
- if (existsSync(sharedEngine) && existsSync(legacyContainer)) try {
4431
+ if (isLegacyContainer && existsSync(sharedEngine) && existsSync(legacyContainer)) try {
3888
4432
  renameSync(legacyContainer, join(presetRoot, `prompt-tool.bak-${stamp}`));
3889
4433
  migrated = true;
3890
4434
  } catch {}
@@ -3900,6 +4444,43 @@ function normalizePresetRootDir(presetDir, presetRoot, legacyContainer) {
3900
4444
  if (presetDir.endsWith("/prompt-tool") || presetDir.endsWith("\\prompt-tool")) return presetRoot;
3901
4445
  return presetDir;
3902
4446
  }
4447
+ /**
4448
+ * 旧版参数覆盖文件(prompt-tool.overrides.yml)→ 激活预设 preset.yml 一次性退役迁移。
4449
+ * 旧写入路径(阶段 1)把引擎参数写进该文件;阶段 2 收敛为 savePresetParams 直接写
4450
+ * preset.yml 并删除覆盖文件。本函数仅处理存量升级场景:
4451
+ * - 键白名单 = PARAM_KEYS(含 guideEnabled/injectPrompt/usePtcMode,补上旧手写列表
4452
+ * 缺失的 3 键;promptConfigs 不走覆盖文件,排除);
4453
+ * - 空值键按 savePresetParams 语义删除(回落模板/引擎默认);
4454
+ * - 成功 = preset.yml 已写入且覆盖文件已删(savePresetParams 删除);失败(preset.yml
4455
+ * 缺失等)抛错且覆盖文件保留,调用方下次重建重试。
4456
+ * 返回 true = 已迁移(调用方需重读参数);false = 无文件/解析失败/无有效键(文件已尽力清理)。
4457
+ */
4458
+ function migrateParamOverridesFile(presetRoot, templateName) {
4459
+ const file = join(presetRoot, templateName, "prompt-tool.overrides.yml");
4460
+ let raw;
4461
+ try {
4462
+ raw = readFileSync(file, "utf8");
4463
+ } catch {
4464
+ return false;
4465
+ }
4466
+ if (raw.trim().length === 0) {
4467
+ rmSync(file, { force: true });
4468
+ return false;
4469
+ }
4470
+ let parsed;
4471
+ try {
4472
+ parsed = parse(raw, { logLevel: "silent" });
4473
+ } catch {
4474
+ return false;
4475
+ }
4476
+ const valid = Object.fromEntries(Object.entries(parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}).filter(([key]) => PARAM_KEYS.has(key) && key !== "promptConfigs"));
4477
+ if (Object.keys(valid).length === 0) {
4478
+ rmSync(file, { force: true });
4479
+ return false;
4480
+ }
4481
+ savePresetParams(presetRoot, templateName, valid, void 0);
4482
+ return true;
4483
+ }
3903
4484
  //#endregion
3904
4485
  //#region src/index.ts
3905
4486
  const name = "prompt-tool";
@@ -3930,6 +4511,7 @@ function warn(ctx, message) {
3930
4511
  } catch {}
3931
4512
  }
3932
4513
  function apply(ctx, configIn) {
4514
+ const hadLegacyContainer = existsSync(join(DEFAULT_PRESET_DIR, "prompt-tool")) && !existsSync(join(DEFAULT_PRESET_DIR, "prompt-tool", "preset.yml"));
3933
4515
  let legacyMigrated = false;
3934
4516
  try {
3935
4517
  legacyMigrated = migrateLegacyLayout(DEFAULT_PRESET_DIR, LEGACY_USER_PRESETS_DIR);
@@ -3972,6 +4554,7 @@ function apply(ctx, configIn) {
3972
4554
  runtime.firstTurnCustom = params.firstTurnCustom === true;
3973
4555
  runtime.guideText = asString(params.guideText);
3974
4556
  runtime.guideCustom = params.guideCustom === true;
4557
+ runtime.guideEnabled = typeof params.guideEnabled === "boolean" ? params.guideEnabled : void 0;
3975
4558
  runtime.modelProvider = asString(params.modelProvider);
3976
4559
  runtime.modelName = asString(params.modelName);
3977
4560
  runtime.subagentModelProvider = asString(params.subagentModelProvider);
@@ -3983,9 +4566,8 @@ function apply(ctx, configIn) {
3983
4566
  runtime.subagentTemperature = asString(params.subagentTemperature);
3984
4567
  runtime.subagentMaxTokens = asString(params.subagentMaxTokens);
3985
4568
  runtime.bootstrapMaxTokens = Number.isSafeInteger(params.bootstrapMaxTokens) && params.bootstrapMaxTokens >= 0 ? params.bootstrapMaxTokens : 0;
3986
- runtime.usePtcMode = params.usePtcMode !== false;
4569
+ runtime.usePtcMode = typeof params.usePtcMode === "boolean" ? params.usePtcMode : void 0;
3987
4570
  runtime.injectPrompt = params.injectPrompt !== false;
3988
- runtime.subagentPersona = asString(params.subagentPersona) || void 0;
3989
4571
  runtime.toolFilterAllow = params.toolFilterAllow;
3990
4572
  runtime.toolFilterDeny = params.toolFilterDeny;
3991
4573
  runtime.maxDepth = params.maxDepth;
@@ -3993,6 +4575,29 @@ function apply(ctx, configIn) {
3993
4575
  runtime.firstTurnWord = asString(params.firstTurnWord) || void 0;
3994
4576
  runtime.promptConfigs = Array.isArray(spec.promptConfigs) ? spec.promptConfigs : [];
3995
4577
  };
4578
+ let legacyAliasHandledThisRun = readPluginState().legacyAliasHandled === true;
4579
+ const ensureLegacyAliasOnce = (presetPrompt, options) => {
4580
+ if (legacyAliasHandledThisRun) return;
4581
+ legacyAliasHandledThisRun = true;
4582
+ const aliasFile = join(runtime.presetDir, "prompt-tool", "preset.yml");
4583
+ try {
4584
+ if (!existsSync(aliasFile) && hadLegacyContainer) {
4585
+ writePreset(presetPrompt, {
4586
+ ...options,
4587
+ outputId: "prompt-tool",
4588
+ aliasOf: true
4589
+ });
4590
+ warn(ctx, "prompt-tool: 已创建一次性旧会话兼容快照 prompt-tool;日常切换不再重建");
4591
+ }
4592
+ writePluginState({
4593
+ ...readPluginState(),
4594
+ legacyAliasHandled: true
4595
+ });
4596
+ } catch (error) {
4597
+ legacyAliasHandledThisRun = false;
4598
+ warn(ctx, `prompt-tool: 创建旧会话兼容快照失败(下次重建重试):${error instanceof Error ? error.message : String(error)}`);
4599
+ }
4600
+ };
3996
4601
  /** 重建生成目录(文本/组合/引擎/提示词配置);writePreset 关闭时移除旧目录。 */
3997
4602
  const rebuildPreset = () => {
3998
4603
  reloadPresetParams();
@@ -4005,6 +4610,7 @@ function apply(ctx, configIn) {
4005
4610
  firstTurnCustom: runtime.firstTurnCustom,
4006
4611
  guideText: runtime.guideText,
4007
4612
  guideCustom: runtime.guideCustom,
4613
+ guideEnabled: runtime.guideEnabled,
4008
4614
  injectPrompt: runtime.injectPrompt,
4009
4615
  modelProvider: runtime.modelProvider,
4010
4616
  modelName: runtime.modelName,
@@ -4016,7 +4622,6 @@ function apply(ctx, configIn) {
4016
4622
  subagentReasoningEffort: runtime.subagentReasoningEffort,
4017
4623
  subagentTemperature: runtime.subagentTemperature,
4018
4624
  subagentMaxTokens: runtime.subagentMaxTokens,
4019
- subagentPersona: runtime.subagentPersona,
4020
4625
  toolFilterAllow: runtime.toolFilterAllow,
4021
4626
  toolFilterDeny: runtime.toolFilterDeny,
4022
4627
  maxDepth: runtime.maxDepth,
@@ -4047,6 +4652,7 @@ function apply(ctx, configIn) {
4047
4652
  }
4048
4653
  }
4049
4654
  writePreset(presetPrompt, options);
4655
+ ensureLegacyAliasOnce(presetPrompt, options);
4050
4656
  } else {
4051
4657
  let cleaned = 0;
4052
4658
  for (const preset of listPresets()) {
@@ -4054,6 +4660,7 @@ function apply(ctx, configIn) {
4054
4660
  for (const name of [
4055
4661
  "agent.cordis.yml",
4056
4662
  "prompt-configs",
4663
+ "custom-tools",
4057
4664
  "preset.md",
4058
4665
  "agents.md",
4059
4666
  "agents-instruction.md",
@@ -4082,49 +4689,15 @@ function apply(ctx, configIn) {
4082
4689
  return true;
4083
4690
  }
4084
4691
  };
4085
- /** 用户参数覆盖(生成目录 prompt-tool.overrides.yml;settings 不再承载参数)。 */
4692
+ /** 旧版参数覆盖文件(prompt-tool.overrides.yml)一次性退役迁移:白名单
4693
+ * (PARAM_KEYS,含 guideEnabled/injectPrompt/usePtcMode)过滤后并入 preset.yml。
4694
+ * 实现收敛到 migrateParamOverridesFile(migration.ts 单一权威)。 */
4086
4695
  const applyParamOverrides = () => {
4087
- const raw = readGeneratedContent(activePresetDir(), "prompt-tool.overrides.yml");
4088
- if (raw.length === 0) return;
4089
- let overrides;
4090
4696
  try {
4091
- const parsed = parse(raw, { logLevel: "silent" });
4092
- overrides = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4697
+ if (migrateParamOverridesFile(runtime.presetDir, runtime.presetTemplate)) reloadPresetParams();
4093
4698
  } catch {
4094
- return;
4699
+ warn(ctx, "prompt-tool: prompt-tool.overrides.yml 迁移到 preset.yml 失败(下次重建重试)");
4095
4700
  }
4096
- if (typeof overrides.firstTurnAnchor === "boolean") runtime.firstTurnAnchor = overrides.firstTurnAnchor;
4097
- if (typeof overrides.firstTurnCustom === "boolean") runtime.firstTurnCustom = overrides.firstTurnCustom;
4098
- if (typeof overrides.firstTurnText === "string") runtime.firstTurnText = overrides.firstTurnText;
4099
- if (typeof overrides.guideCustom === "boolean") runtime.guideCustom = overrides.guideCustom;
4100
- if (typeof overrides.guideText === "string") runtime.guideText = overrides.guideText;
4101
- if (typeof overrides.modelProvider === "string") runtime.modelProvider = overrides.modelProvider;
4102
- if (typeof overrides.modelName === "string") runtime.modelName = overrides.modelName;
4103
- if (typeof overrides.subagentModelProvider === "string") runtime.subagentModelProvider = overrides.subagentModelProvider;
4104
- if (typeof overrides.subagentModelName === "string") runtime.subagentModelName = overrides.subagentModelName;
4105
- if (typeof overrides.modelReasoningEffort === "string") runtime.modelReasoningEffort = overrides.modelReasoningEffort;
4106
- if (typeof overrides.modelTemperature === "string") runtime.modelTemperature = overrides.modelTemperature;
4107
- if (typeof overrides.modelMaxTokens === "string") runtime.modelMaxTokens = overrides.modelMaxTokens;
4108
- if (typeof overrides.subagentReasoningEffort === "string") runtime.subagentReasoningEffort = overrides.subagentReasoningEffort;
4109
- if (typeof overrides.subagentTemperature === "string") runtime.subagentTemperature = overrides.subagentTemperature;
4110
- if (typeof overrides.subagentMaxTokens === "string") runtime.subagentMaxTokens = overrides.subagentMaxTokens;
4111
- if (typeof overrides.subagentPersona === "string") runtime.subagentPersona = overrides.subagentPersona;
4112
- if (typeof overrides.firstTurnWord === "string") runtime.firstTurnWord = overrides.firstTurnWord;
4113
- if (typeof overrides.bootstrapMaxTokens === "number") runtime.bootstrapMaxTokens = overrides.bootstrapMaxTokens;
4114
- const listOf = (value) => {
4115
- if (typeof value === "string") return value;
4116
- if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
4117
- };
4118
- const tfAllow = listOf(overrides.toolFilterAllow);
4119
- if (tfAllow !== void 0) runtime.toolFilterAllow = tfAllow;
4120
- const tfDeny = listOf(overrides.toolFilterDeny);
4121
- if (tfDeny !== void 0) runtime.toolFilterDeny = tfDeny;
4122
- const kinds = listOf(overrides.allowKinds);
4123
- if (kinds !== void 0 && (typeof kinds === "string" ? kinds.trim().length > 0 : kinds.length > 0)) runtime.allowKinds = kinds;
4124
- const depth = overrides.maxDepth;
4125
- if (depth === "provider-managed") runtime.maxDepth = "provider-managed";
4126
- else if (typeof depth === "number" && Number.isSafeInteger(depth) && depth >= 0) runtime.maxDepth = depth;
4127
- else if (typeof depth === "string" && depth.length > 0) runtime.maxDepth = depth;
4128
4701
  };
4129
4702
  const legacySkillsDir = typeof config.skillsDir === "string" && config.skillsDir.length > 0 && config.skillsDir !== DEFAULT_SKILLS_DIR ? config.skillsDir : "";
4130
4703
  const userSkillsDirs = Array.isArray(config.skillsDirs) && config.skillsDirs.length > 0 ? config.skillsDirs.filter((dir) => typeof dir === "string" && dir.trim().length > 0) : legacySkillsDir.length > 0 ? [legacySkillsDir] : [];
@@ -4238,15 +4811,15 @@ function apply(ctx, configIn) {
4238
4811
  }
4239
4812
  };
4240
4813
  });
4241
- registerSettingsBridge(ctx, NS, getModelsState, () => ({
4814
+ const settingsBridge = registerSettingsBridge(ctx, NS, getModelsState, () => ({
4242
4815
  activeSkillsDirs,
4243
4816
  skillCatalog
4244
- }), () => "", (sctx) => ensureRegistered(sctx), () => {
4817
+ }), () => "", () => {
4245
4818
  skillCatalog = catalogOf(readAllSkillsChecked());
4246
4819
  cachedSkills.invalidate();
4247
4820
  invalidateSkills?.();
4248
- }, () => activePresetDir(), (scope) => {
4249
- if (scope === "preset") current = readGeneratedContent(activePresetDir(), "preset.md");
4821
+ }, () => activePresetDir(), (scopes) => {
4822
+ for (const scope of scopes) if (scope === "preset") current = readGeneratedContent(activePresetDir(), "preset.md");
4250
4823
  else currentAgents = readGeneratedContent(activePresetDir(), "agents.md");
4251
4824
  try {
4252
4825
  rebuildPreset();
@@ -4283,6 +4856,7 @@ function apply(ctx, configIn) {
4283
4856
  firstTurnCustom: initialParams.firstTurnCustom === true,
4284
4857
  guideText: asString(initialParams.guideText),
4285
4858
  guideCustom: initialParams.guideCustom === true,
4859
+ guideEnabled: typeof initialParams.guideEnabled === "boolean" ? initialParams.guideEnabled : void 0,
4286
4860
  injectPrompt: initialParams.injectPrompt !== false,
4287
4861
  modelProvider: asString(initialParams.modelProvider),
4288
4862
  modelName: asString(initialParams.modelName),
@@ -4295,8 +4869,7 @@ function apply(ctx, configIn) {
4295
4869
  subagentTemperature: asString(initialParams.subagentTemperature),
4296
4870
  subagentMaxTokens: asString(initialParams.subagentMaxTokens),
4297
4871
  bootstrapMaxTokens: Number.isSafeInteger(initialParams.bootstrapMaxTokens) && initialParams.bootstrapMaxTokens >= 0 ? initialParams.bootstrapMaxTokens : 0,
4298
- usePtcMode: initialParams.usePtcMode !== false,
4299
- subagentPersona: asString(initialParams.subagentPersona) || void 0,
4872
+ usePtcMode: typeof initialParams.usePtcMode === "boolean" ? initialParams.usePtcMode : void 0,
4300
4873
  toolFilterAllow: initialParams.toolFilterAllow,
4301
4874
  toolFilterDeny: initialParams.toolFilterDeny,
4302
4875
  maxDepth: initialParams.maxDepth,
@@ -4309,9 +4882,21 @@ function apply(ctx, configIn) {
4309
4882
  fallbackText: config.fallbackText,
4310
4883
  promptConfigs: Array.isArray(initialSpec?.promptConfigs) ? initialSpec.promptConfigs : []
4311
4884
  };
4885
+ const agentPresetsNs = "agent-presets";
4312
4886
  let hostSettingsService;
4313
- let lastSyncedHostDefault;
4314
- const syncHostDefault = (reason) => {
4887
+ const readHostDefault = (value) => {
4888
+ if (value === null || typeof value !== "object") return void 0;
4889
+ const candidate = value.default;
4890
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
4891
+ };
4892
+ const managedPresetExists = (id) => {
4893
+ try {
4894
+ return listPresets().some((preset) => preset.id === id);
4895
+ } catch {
4896
+ return false;
4897
+ }
4898
+ };
4899
+ const syncHostDefault = (_reason) => {
4315
4900
  const s = hostSettingsService;
4316
4901
  if (s === void 0) return;
4317
4902
  const template = runtime.presetTemplate;
@@ -4319,17 +4904,31 @@ function apply(ctx, configIn) {
4319
4904
  warn(ctx, `prompt-tool: 预设 id ${JSON.stringify(template)} 不符合官方 agent-presets 命名(^[a-z0-9][a-z0-9-]*$),跳过宿主 default 同步;请将预设目录改名为合法 id`);
4320
4905
  return;
4321
4906
  }
4322
- if (reason === "switch" && lastSyncedHostDefault === template) return;
4323
- lastSyncedHostDefault = template;
4324
- try {
4325
- s.mutate(settingsNamespace("agent-presets"), [{
4326
- op: "set",
4327
- path: ["default"],
4328
- value: template
4329
- }]);
4330
- } catch (error) {
4907
+ if (readHostDefault(s.get(agentPresetsNs)) === template) return;
4908
+ s.mutate(agentPresetsNs, [{
4909
+ op: "set",
4910
+ path: ["default"],
4911
+ value: template
4912
+ }]).catch((error) => {
4331
4913
  warn(ctx, `prompt-tool: 同步宿主 agent-presets default 失败:${error instanceof Error ? error.message : String(error)}`);
4914
+ });
4915
+ };
4916
+ const syncTemplateFromHostDefault = (value) => {
4917
+ const s = hostSettingsService;
4918
+ if (s === void 0) return;
4919
+ const template = readHostDefault(value ?? s.get(agentPresetsNs));
4920
+ if (template === void 0 || template === runtime.presetTemplate) return;
4921
+ if (!managedPresetExists(template)) {
4922
+ warn(ctx, `prompt-tool: 官方 agent-presets default 已切换为 ${JSON.stringify(template)},但该预设不在提示词工具管理目录中,跳过反向同步`);
4923
+ return;
4332
4924
  }
4925
+ s.mutate(NS, [{
4926
+ op: "set",
4927
+ path: ["presetTemplate"],
4928
+ value: template
4929
+ }]).catch((error) => {
4930
+ warn(ctx, `prompt-tool: 跟随官方 agent-presets default 失败:${error instanceof Error ? error.message : String(error)}`);
4931
+ });
4333
4932
  };
4334
4933
  installSubagentModelRoute(ctx, () => runtime.subagentModelProvider.length > 0 && runtime.subagentModelName.length > 0, () => runtime.subagentModelProvider, () => runtime.subagentModelName);
4335
4934
  const applyDefaultModel = installDefaultModelRoute(ctx, () => runtime.modelProvider.length > 0 && runtime.modelName.length > 0, () => runtime.modelProvider, () => runtime.modelName, () => runtime.modelReasoningEffort);
@@ -4352,14 +4951,10 @@ function apply(ctx, configIn) {
4352
4951
  presetTemplate: runtime.presetTemplate
4353
4952
  });
4354
4953
  registerTuiCommand(ctx, NS, () => currentSource(), getModelsState, () => listAdvertisedModels(ctx), () => activePresetDir(), (key, value) => {
4355
- try {
4356
- if (key === "promptConfigs") savePresetParams(runtime.presetDir, runtime.presetTemplate, void 0, Array.isArray(value) ? value : void 0);
4357
- else savePresetParams(runtime.presetDir, runtime.presetTemplate, { [key]: value }, void 0);
4358
- reloadPresetParams();
4359
- rebuildPreset();
4360
- } catch (error) {
4361
- warn(ctx, `prompt-tool: TUI 参数保存失败:${error instanceof Error ? error.message : String(error)}`);
4362
- }
4954
+ if (key === "promptConfigs") savePresetParams(runtime.presetDir, runtime.presetTemplate, void 0, Array.isArray(value) ? value : void 0);
4955
+ else savePresetParams(runtime.presetDir, runtime.presetTemplate, { [key]: value }, void 0);
4956
+ reloadPresetParams();
4957
+ rebuildPreset();
4363
4958
  });
4364
4959
  /**
4365
4960
  * 物化导入的预设:用其自身 preset.yml(spec 参数 + modules + promptConfigs)渲染
@@ -4381,7 +4976,6 @@ function apply(ctx, configIn) {
4381
4976
  modelName: "",
4382
4977
  subagentModelProvider: "",
4383
4978
  subagentModelName: "",
4384
- subagentPersona: "",
4385
4979
  injectAgentsPrompt: false,
4386
4980
  bootstrapMaxTokens: 0,
4387
4981
  usePtcMode: false,
@@ -4454,51 +5048,44 @@ function apply(ctx, configIn) {
4454
5048
  if (!residentAgentsWritten) warn(ctx, `prompt-tool: failed to remove resident rules block from ${runtime.residentAgentsPath}`);
4455
5049
  }
4456
5050
  rebuildPreset();
4457
- if (presetTemplateChanged) syncHostDefault("switch");
4458
- };
4459
- registerCharacterTools(ctx, {
4460
- presetRoot: () => dirname(activePresetDir()),
4461
- templateName: () => basename(activePresetDir()),
4462
- rebuild: () => {
4463
- try {
4464
- rebuildPreset();
4465
- } catch (error) {
4466
- warn(ctx, `prompt-tool: character tool rebuild failed: ${String(error)}`);
4467
- }
5051
+ if (presetTemplateChanged) {
5052
+ syncHostDefault("switch");
5053
+ applyBuiltinTools();
4468
5054
  }
4469
- });
4470
- registerWorldBookTools(ctx, {
4471
- activeDir: () => activePresetDir(),
4472
- presetRoot: () => dirname(activePresetDir()),
4473
- rebuild: () => {
4474
- try {
4475
- rebuildPreset();
4476
- } catch (error) {
4477
- warn(ctx, `prompt-tool: world-book tool rebuild failed: ${String(error)}`);
5055
+ };
5056
+ let disposeBuiltinTools = [];
5057
+ const applyBuiltinTools = () => {
5058
+ for (const dispose of disposeBuiltinTools) dispose();
5059
+ disposeBuiltinTools = [];
5060
+ const push = (dispose) => {
5061
+ if (typeof dispose === "function") disposeBuiltinTools.push(dispose);
5062
+ };
5063
+ push(registerCharacterTools(ctx, {
5064
+ presetRoot: () => dirname(activePresetDir()),
5065
+ templateName: () => basename(activePresetDir()),
5066
+ rebuild: () => {
5067
+ try {
5068
+ rebuildPreset();
5069
+ } catch (error) {
5070
+ warn(ctx, `prompt-tool: character tool rebuild failed: ${String(error)}`);
5071
+ }
4478
5072
  }
4479
- }
4480
- });
4481
- registerSessionVarTools(ctx);
4482
- const settingsEntry = currentSource();
4483
- const ensureRegistered = (sctx) => ensureSettingsRegistered(sctx, NS, PromptSettingsSchema, {
4484
- base: () => settingsEntry,
4485
- onRegistered: (scope) => {
4486
- currentSource = () => scope.get();
4487
- scope.watch(() => {
5073
+ }));
5074
+ push(registerWorldBookTools(ctx, {
5075
+ activeDir: () => activePresetDir(),
5076
+ presetRoot: () => dirname(activePresetDir()),
5077
+ rebuild: () => {
4488
5078
  try {
4489
- applyState();
5079
+ rebuildPreset();
4490
5080
  } catch (error) {
4491
- warn(ctx, `prompt-tool: applyState failed: ${error instanceof Error ? error.message : String(error)}`);
5081
+ warn(ctx, `prompt-tool: world-book tool rebuild failed: ${String(error)}`);
4492
5082
  }
4493
- });
4494
- try {
4495
- applyState();
4496
- } catch (error) {
4497
- warn(ctx, `prompt-tool: applyState failed: ${error instanceof Error ? error.message : String(error)}`);
4498
5083
  }
4499
- },
4500
- onError: (message) => warn(ctx, `prompt-tool: settings register failed: ${message}`)
4501
- });
5084
+ }));
5085
+ push(registerSessionVarTools(ctx));
5086
+ };
5087
+ applyBuiltinTools();
5088
+ const settingsEntry = currentSource();
4502
5089
  /** 阶段 2 迁移:旧全局 settings 引擎参数 → 激活预设 preset.yml(一次性,state.paramsMigrated 后跳过)。
4503
5090
  * 兼容旧版预设根内 .pt-params-migrated 标记:存在即视为已迁移,并迁入状态文件后删除。 */
4504
5091
  const migrateSettingsParamsToPreset = (sctx) => {
@@ -4560,20 +5147,32 @@ function apply(ctx, configIn) {
4560
5147
  warn(ctx, `prompt-tool: settings 参数迁移失败(下次启动重试):${error instanceof Error ? error.message : String(error)}`);
4561
5148
  }
4562
5149
  };
5150
+ ctx.inject(["settings", "agentPresets"], () => {
5151
+ syncTemplateFromHostDefault();
5152
+ });
4563
5153
  ctx.inject(["settings"], (sctx) => {
4564
5154
  hostSettingsService = sctx.settings;
4565
- ensureRegistered(sctx);
4566
- if (legacyMigrated) syncHostDefault("migrate");
4567
- migrateSettingsParamsToPreset(sctx);
4568
- sctx.effect(() => () => {
4569
- currentSource = () => settingsEntry;
4570
- try {
4571
- applyState();
4572
- } catch (error) {
4573
- warn(ctx, `prompt-tool: applyState failed: ${error instanceof Error ? error.message : String(error)}`);
5155
+ sctx.effect(() => sctx.on("settings/updated", (ns, next) => {
5156
+ if (String(ns) !== String(agentPresetsNs)) return;
5157
+ syncTemplateFromHostDefault(next);
5158
+ }), "prompt-tool: follow agent-presets default");
5159
+ sctx.settings.installSection(ctx, NS, PromptSettingsSchema, settingsEntry, {
5160
+ setSource: (current) => {
5161
+ currentSource = current;
5162
+ },
5163
+ onChange: () => {
5164
+ settingsBridge.invalidateDescriptor();
5165
+ try {
5166
+ applyState();
5167
+ } catch (error) {
5168
+ warn(ctx, `prompt-tool: applyState failed: ${error instanceof Error ? error.message : String(error)}`);
5169
+ }
4574
5170
  }
4575
5171
  });
5172
+ if (legacyMigrated) syncHostDefault("migrate");
5173
+ else syncTemplateFromHostDefault();
5174
+ migrateSettingsParamsToPreset(sctx);
4576
5175
  });
4577
5176
  }
4578
5177
  //#endregion
4579
- export { BRIDGE_ENDPOINTS, Config, PARAM_KEYS, PromptSettingsSchema, SETTINGS_BRIDGE_PREFIX, SKILL_NAME_RE, apply, applyModuleConfigs, cloneBuiltinPreset, createCachedSkillsReader, detectModels, ensurePresetSeed, ensureSettingsRegistered, ensureWebSurface, fixSkillEntry, inject, installDefaultModelRoute, isValidSkill, listAdvertisedModels, listBuiltinTemplates, listPresets, listSkillFolders, loadPresetSpec, loadPromptTemplates, mergeSkillDirs, migrateLegacyLayout, name, normalizePresetRootDir, parseFrontmatter, readPluginState, readSkills, registerCharacterTools, registerSettingsBridge, registerTuiCommand, registerWorldBookTools, removeUserPreset, resolvePresetDir, resolvePresetParams, resolveProfileDir, resolveProfileSkillsDir, savePresetParams, stPresetId, toKebabName, userPresetsDir, validSkills, validatePromptConfigs, writePluginState, writePreset };
5178
+ export { BRIDGE_ENDPOINTS, Config, ENGINE_PARAM_KEYS, MODEL_SEGMENT_MAP, PARAM_KEYS, PromptSettingsSchema, SETTINGS_BRIDGE_PREFIX, SKILL_NAME_RE, WRITER_PARAM_KEYS, appendCharacterMemory, appendMemoryFile, apply, applyCharacterToPreset, applyModuleConfigs, buildModuleConfigsFromParams, buildWorldBookEntry, cloneBuiltinPreset, convertStToPreset, createCachedSkillsReader, deleteCharacterCard, deleteWorldBookEntry, detectModels, ensurePresetSeed, ensureWebSurface, fixSkillEntry, importCharacterCard, inject, installDefaultModelRoute, isValidSkill, listAdvertisedModels, listBuiltinTemplates, listCharacterCards, listPresets, listSkillFolders, listWorldBookEntries, loadPresetSpec, loadPromptTemplates, loadToolTemplates, mergeSkillDirs, mergeStPresets, migrateLegacyLayout, name, normalizePresetRootDir, parseFrontmatter, processStText, readCharacterMemory, readPluginState, readSkills, registerCharacterTools, registerSettingsBridge, registerTuiCommand, registerWorldBookTools, removeCharacterFromPreset, removeUserPreset, renderComposition, resolvePresetDir, resolvePresetParams, resolveProfileDir, resolveProfileSkillsDir, savePresetParams, stPresetId, syncImportedCharacterMemory, toKebabName, upsertWorldBookEntry, userPresetsDir, validSkills, validatePromptConfigs, writePluginState, writePreset };