dsh-plugin-prompt-tool 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +185 -79
  54. package/lib/index.mjs +1000 -356
  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,9 @@
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";
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";
2
2
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
- import { parse, parseDocument, stringify } from "yaml";
5
4
  import { basename, dirname, join, resolve, sep } from "node:path";
6
5
  import { fileURLToPath } from "node:url";
6
+ import { parse, parseDocument, stringify } from "yaml";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
  import { createHash, randomBytes } from "node:crypto";
9
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -551,23 +551,20 @@ function installSubagentModelRoute(ctx, isEnabled, provider, model) {
551
551
  });
552
552
  }
553
553
  //#endregion
554
- //#region src/shared/param-keys.ts
554
+ //#region src/shared/engine-params.ts
555
555
  /**
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 共用,单一来源。
556
+ * 引擎行为参数键唯一权威(数组字面量 = 运行时事实)。
557
+ * 与 EngineParams 接口双向相等断言:新增参数必须同时改接口与列表,漏改任一侧
558
+ * typecheck 失败(防历史事故:PARAM_KEYS 漏 25 键混入 variables.yml 污染注入)。
564
559
  */
565
- const PARAM_KEYS = /* @__PURE__ */ new Set([
560
+ const ENGINE_PARAM_KEYS = [
566
561
  "firstTurnAnchor",
567
562
  "firstTurnText",
568
563
  "firstTurnCustom",
569
564
  "guideText",
570
565
  "guideCustom",
566
+ "guideEnabled",
567
+ "injectPrompt",
571
568
  "modelProvider",
572
569
  "modelName",
573
570
  "subagentModelProvider",
@@ -578,7 +575,6 @@ const PARAM_KEYS = /* @__PURE__ */ new Set([
578
575
  "subagentReasoningEffort",
579
576
  "subagentTemperature",
580
577
  "subagentMaxTokens",
581
- "subagentPersona",
582
578
  "toolFilterAllow",
583
579
  "toolFilterDeny",
584
580
  "maxDepth",
@@ -586,9 +582,143 @@ const PARAM_KEYS = /* @__PURE__ */ new Set([
586
582
  "firstTurnWord",
587
583
  "bootstrapMaxTokens",
588
584
  "usePtcMode",
585
+ "promoteGate",
586
+ "promoteAfterFirstResponse",
587
+ "maxPromoteSteps",
588
+ "bootstrapTools",
589
+ "compactionTools",
590
+ "personaSectionsOnly",
591
+ "workspaceLine",
592
+ "phase1FirstCallInstruction",
593
+ "messageSources",
594
+ "deferredSources",
595
+ "deferredGraceSteps",
596
+ "instructionHint",
597
+ "stages",
598
+ "stagePreUnlock",
599
+ "stageAdvanceTool",
600
+ "stageAdvanceDescription",
601
+ "stageSectionTemplate",
602
+ "toolFilterSubagents",
603
+ "strReplaceEditorMaxOutputChars",
604
+ "anchorTurn",
605
+ "anchorTurnText",
606
+ "deliberationGate",
607
+ "deliberationMinChars",
608
+ "deliberationMaxGatesPerTurn",
609
+ "cotDrip",
610
+ "cotDripEvery",
611
+ "cotDripMaxPerTurn"
612
+ ];
613
+ /** writePreset.runtimeOf 实际透传键(与 PresetWriterParams 双向相等断言,防 Pick 漏键)。 */
614
+ const WRITER_PARAM_KEYS = [
615
+ "firstTurnAnchor",
616
+ "firstTurnText",
617
+ "firstTurnCustom",
618
+ "guideText",
619
+ "guideCustom",
620
+ "guideEnabled",
589
621
  "injectPrompt",
622
+ "modelProvider",
623
+ "modelName",
624
+ "subagentModelProvider",
625
+ "subagentModelName",
626
+ "modelReasoningEffort",
627
+ "modelTemperature",
628
+ "modelMaxTokens",
629
+ "subagentReasoningEffort",
630
+ "subagentTemperature",
631
+ "subagentMaxTokens",
632
+ "toolFilterAllow",
633
+ "toolFilterDeny",
634
+ "maxDepth",
635
+ "allowKinds",
636
+ "firstTurnWord",
637
+ "bootstrapMaxTokens",
638
+ "usePtcMode",
639
+ "anchorTurn",
640
+ "anchorTurnText",
641
+ "deliberationGate",
642
+ "deliberationMinChars",
643
+ "deliberationMaxGatesPerTurn",
644
+ "cotDrip",
645
+ "cotDripEvery",
646
+ "cotDripMaxPerTurn"
647
+ ];
648
+ const NUMERIC_PARAM_RULES = {
649
+ modelTemperature: validateTemperature,
650
+ subagentTemperature: validateTemperature,
651
+ modelMaxTokens: validateMaxTokens,
652
+ subagentMaxTokens: validateMaxTokens
653
+ };
654
+ function validateTemperature(value) {
655
+ return Number.isFinite(value) ? void 0 : "必须是有限数字";
656
+ }
657
+ function validateMaxTokens(value) {
658
+ return Number.isSafeInteger(value) && value > 0 ? void 0 : "必须是正整数";
659
+ }
660
+ /**
661
+ * 校验参数键值(键集合合法性由调用方白名单先行校验)。返回逐字段错误;空数组 = 通过。
662
+ * 类型收窄:只接受 number(preset.yml 手写数字)与 string(UI 字符串标量);
663
+ * '' 是合法删键值,其余空白串/非数字串/越界值在保存层响亮失败。
664
+ */
665
+ function validateEngineParamValues(overrides) {
666
+ const errors = [];
667
+ for (const [key, value] of Object.entries(overrides)) {
668
+ const rule = NUMERIC_PARAM_RULES[key];
669
+ if (rule === void 0) continue;
670
+ if (value === void 0 || value === null || value === "") continue;
671
+ if (typeof value !== "number" && typeof value !== "string") {
672
+ errors.push({
673
+ key,
674
+ message: `${key}: 必须是数字或数字字符串(留空 = 不设置)`
675
+ });
676
+ continue;
677
+ }
678
+ const text = typeof value === "string" ? value.trim() : String(value);
679
+ if (text.length === 0) {
680
+ errors.push({
681
+ key,
682
+ message: `${key}: 必须是数字(留空 = 不设置)`
683
+ });
684
+ continue;
685
+ }
686
+ const numeric = Number(text);
687
+ const reason = Number.isNaN(numeric) ? "必须是数字" : rule(numeric);
688
+ if (reason !== void 0) errors.push({
689
+ key,
690
+ message: `${key}: ${reason}(留空 = 不设置)`
691
+ });
692
+ }
693
+ return errors;
694
+ }
695
+ //#endregion
696
+ //#region src/shared/param-keys.ts
697
+ /**
698
+ * 引擎行为参数键(按预设存储:激活预设 preset.yml 的 params + promptConfigs)。
699
+ * 不进 Config schema、不进 settings namespace——每预设一份,随预设走(官方范式:
700
+ * Config = 部署轴,引擎行为在预设文件)。
701
+ *
702
+ * 派生自 ENGINE_PARAM_KEYS(唯一权威)+ 少量附加键:
703
+ * - 锚定/引导内容键:writePreset 映射进 near-anchor/router-guide 的 promptConfig
704
+ * params(策略消费),须排除出 variables.yml(避免同一键双落盘为模板变量);
705
+ * - promptConfigs:settings 提示词配置数组键。
706
+ * 注意:variables.yml 的占位键(spec.variables 空值登记,供世界书条目 {{key}} 动态
707
+ * 引用)走另一套通道,不属于 PARAM_KEYS——两套体系不互串。
708
+ * 放 shared:host(write-preset)与 config/settings-bridge 共用,单一来源。
709
+ */
710
+ const EXTRA_PARAM_KEYS = [
711
+ "buildPattern",
712
+ "complexPattern",
713
+ "firstTurnBuild",
714
+ "firstTurnInspect",
715
+ "firstTurnDeep",
716
+ "guideComplexPattern",
717
+ "guideWeak",
718
+ "guideDeep",
590
719
  "promptConfigs"
591
- ]);
720
+ ];
721
+ const PARAM_KEYS = /* @__PURE__ */ new Set([...ENGINE_PARAM_KEYS, ...EXTRA_PARAM_KEYS]);
592
722
  //#endregion
593
723
  //#region src/config.ts
594
724
  /** 插件配置、settings 数据模型与默认常量(settings 接口层)。 */
@@ -730,6 +860,20 @@ function loadPromptTemplates() {
730
860
  };
731
861
  });
732
862
  }
863
+ /** 工具模板库扫描(templates/tools/*.yml;损坏 fail loud——包内模板属于发布 bug)。 */
864
+ function loadToolTemplates() {
865
+ const dir = fileURLToPath(new URL("../templates/tools", import.meta.url));
866
+ 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) => {
867
+ const content = readFileSync(join(dir, entry.name), "utf8");
868
+ const parsed = parse(content, { logLevel: "silent" });
869
+ 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`);
870
+ return {
871
+ file: entry.name,
872
+ content,
873
+ spec: parsed
874
+ };
875
+ });
876
+ }
733
877
  //#endregion
734
878
  //#region src/runtime/skill-fix.ts
735
879
  /**
@@ -850,6 +994,73 @@ function fixSkillEntry(skillsDir, folder) {
850
994
  };
851
995
  }
852
996
  //#endregion
997
+ //#region src/host/worldbook.ts
998
+ /** 世界书条目 CRUD 服务:promptConfigs world-book 策略配置的增删改查。
999
+ * 与角色卡导入(characters.applyCharacterToPreset)共用同一存储(preset.yml
1000
+ * promptConfigs),模型工具(world_book_*)与未来 bridge 端点同源,
1001
+ * 不各自实现 parseDocument 往返。 */
1002
+ /**
1003
+ * 世界书条目结构工厂(能力归一)。
1004
+ * strategy/layer/position 固定值与 params 键集单一权威——ST 导入(sillytavern.ts)
1005
+ * 与模型工具(world_book_upsert)共用,不再各自手写条目结构。
1006
+ */
1007
+ function buildWorldBookEntry(input) {
1008
+ return {
1009
+ ...input.id !== void 0 ? { id: input.id } : {},
1010
+ name: input.name,
1011
+ strategy: "world-book",
1012
+ order: typeof input.order === "number" ? input.order : 100,
1013
+ text: input.text,
1014
+ layer: "pre-step",
1015
+ position: "before-all",
1016
+ ...input.enabled !== void 0 ? { enabled: input.enabled } : {},
1017
+ params: {
1018
+ constant: input.constant,
1019
+ ...input.keys !== void 0 && input.keys.length > 0 ? { keys: input.keys } : {},
1020
+ ...input.secondaryKeys !== void 0 && input.secondaryKeys.length > 0 ? { secondaryKeys: input.secondaryKeys } : {},
1021
+ ...input.caseSensitive === true ? { caseSensitive: true } : {},
1022
+ ...input.wholeWords === true ? { wholeWords: true } : {},
1023
+ ...typeof input.selectiveLogic === "number" ? { selectiveLogic: input.selectiveLogic } : {}
1024
+ }
1025
+ };
1026
+ }
1027
+ const isWorldBook = (config) => config !== null && typeof config === "object" && !Array.isArray(config) && config.strategy === "world-book";
1028
+ /** 当前预设全部世界书条目(保持文件顺序)。 */
1029
+ function listWorldBookEntries(presetDir) {
1030
+ const spec = loadPresetSpec(presetDir);
1031
+ return Array.isArray(spec.promptConfigs) ? spec.promptConfigs.filter(isWorldBook) : [];
1032
+ }
1033
+ /** 新增或更新一条世界书条目(按 id 定位;不存在则追加)。返回写入后条目总数。 */
1034
+ function upsertWorldBookEntry(presetDir, entry) {
1035
+ if (entry === null || typeof entry !== "object" || typeof entry.id !== "string" || entry.id.length === 0) throw new TypeError("世界书条目必须含非空字符串 id");
1036
+ let count = 0;
1037
+ withPresetDoc(presetDir, (doc) => {
1038
+ const current = doc.toJS();
1039
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1040
+ const existing = configs.findIndex((config) => String(config.id ?? "") === entry.id);
1041
+ if (existing >= 0) configs[existing] = entry;
1042
+ else configs.push(entry);
1043
+ doc.setIn(["promptConfigs"], configs);
1044
+ count = configs.filter(isWorldBook).length;
1045
+ });
1046
+ return count;
1047
+ }
1048
+ /** 删除一条世界书条目;不存在抛错。返回删除后条目总数。 */
1049
+ function deleteWorldBookEntry(presetDir, id) {
1050
+ let count = 0;
1051
+ let removed = false;
1052
+ withPresetDoc(presetDir, (doc) => {
1053
+ const current = doc.toJS();
1054
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1055
+ const kept = configs.filter((config) => !(isWorldBook(config) && String(config.id ?? "") === id));
1056
+ removed = kept.length !== configs.length;
1057
+ doc.setIn(["promptConfigs"], kept);
1058
+ count = kept.filter(isWorldBook).length;
1059
+ });
1060
+ if (!removed) throw new Error(`世界书条目 ${id} 不存在`);
1061
+ return count;
1062
+ }
1063
+ //#endregion
853
1064
  //#region src/host/sillytavern.ts
854
1065
  /**
855
1066
  * SillyTavern 预设 → 本项目单文件预设转换引擎(纯函数,无文件 IO)。
@@ -1016,25 +1227,20 @@ function convertStToPreset(card, baseName) {
1016
1227
  const secondaryKeys = Array.isArray(rawSecondary) ? rawSecondary.map(String).filter((key) => key.trim().length > 0) : [];
1017
1228
  const constant = entry.constant === true || entry.add_always === true;
1018
1229
  const enabled = entry.disable !== void 0 ? entry.disable !== true : entry.enabled !== false;
1019
- configs.push({
1230
+ const stOrder = typeof entry.insertion_order === "number" ? entry.insertion_order : typeof entry.order === "number" ? entry.order : 100;
1231
+ configs.push(buildWorldBookEntry({
1020
1232
  id: `lore-${String(entry.id ?? entry.uid ?? index)}`,
1021
1233
  name: comment,
1022
1234
  enabled,
1023
- strategy: "world-book",
1024
- order: typeof entry.insertion_order === "number" ? entry.insertion_order : typeof entry.order === "number" ? entry.order : 100,
1235
+ order: -stOrder,
1025
1236
  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
- });
1237
+ constant,
1238
+ keys: keys.length > 0 ? keys : void 0,
1239
+ secondaryKeys: secondaryKeys.length > 0 ? secondaryKeys : void 0,
1240
+ ...(entry.case_sensitive ?? entry.caseSensitive) === true ? { caseSensitive: true } : {},
1241
+ ...(entry.match_whole_words ?? entry.matchWholeWords) === true ? { wholeWords: true } : {},
1242
+ ...typeof entry.selectiveLogic === "number" ? { selectiveLogic: entry.selectiveLogic } : typeof entry.selective_logic === "number" ? { selectiveLogic: entry.selective_logic } : {}
1243
+ }));
1038
1244
  }
1039
1245
  }
1040
1246
  const firstMes = clean(bodyText("first_mes"));
@@ -1188,6 +1394,70 @@ function sortConfigs(configs) {
1188
1394
  return (Number(a.order) || 0) - (Number(b.order) || 0);
1189
1395
  });
1190
1396
  }
1397
+ /** 角色卡记忆条目字段(不含 id;id 由调用方加 chara-<卡>- 前缀)。 */
1398
+ function buildCharacterMemoryEntry(spec, memory) {
1399
+ if (memory.trim().length === 0) return void 0;
1400
+ return buildWorldBookEntry({
1401
+ name: "角色记忆",
1402
+ enabled: true,
1403
+ order: 3e3,
1404
+ text: `【${spec.name} 的关系记忆】\n${memory}`,
1405
+ constant: true
1406
+ });
1407
+ }
1408
+ /** 角色卡记忆变更后同步已导入预设的 chara-<id>-memory 注入条目
1409
+ * (world_book note 写入卡记忆后调用;未导入当前预设的卡返回 synced=false)。 */
1410
+ function syncImportedCharacterMemory(presetRoot, templateName, cardId) {
1411
+ if (!validCardId(cardId)) return {
1412
+ ok: false,
1413
+ message: `非法角色卡 id:${cardId}`
1414
+ };
1415
+ const spec = loadConverted(cardDir(presetRoot, cardId));
1416
+ if (spec === void 0) return {
1417
+ ok: false,
1418
+ message: `角色卡 ${cardId} 不存在或参数损坏`
1419
+ };
1420
+ const memory = readCharacterMemory(presetRoot, cardId);
1421
+ const memoryId = `chara-${cardId}-memory`;
1422
+ let synced = false;
1423
+ try {
1424
+ withPresetDoc(join(presetRoot, templateName), (doc) => {
1425
+ const current = doc.toJS();
1426
+ if (!(Array.isArray(current.meta?.importedCharacters) ? current.meta.importedCharacters.map(String) : []).includes(cardId)) return;
1427
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1428
+ const entry = buildCharacterMemoryEntry(spec, memory);
1429
+ const at = configs.findIndex((config) => config !== null && typeof config === "object" && String(config.id ?? "") === memoryId);
1430
+ if (entry === void 0) {
1431
+ if (at >= 0) {
1432
+ configs.splice(at, 1);
1433
+ synced = true;
1434
+ }
1435
+ } else if (at >= 0) {
1436
+ configs[at] = {
1437
+ ...entry,
1438
+ id: memoryId
1439
+ };
1440
+ synced = true;
1441
+ } else {
1442
+ configs.push({
1443
+ ...entry,
1444
+ id: memoryId
1445
+ });
1446
+ synced = true;
1447
+ }
1448
+ if (synced) doc.setIn(["promptConfigs"], sortConfigs(configs));
1449
+ });
1450
+ return {
1451
+ ok: true,
1452
+ synced
1453
+ };
1454
+ } catch (error) {
1455
+ return {
1456
+ ok: false,
1457
+ message: `同步失败:${error instanceof Error ? error.message : String(error)}`
1458
+ };
1459
+ }
1460
+ }
1191
1461
  /** 角色卡库根:预设根下的点前缀目录(官方 roster 扫描跳过,与 .engine 同机制)。 */
1192
1462
  function charactersDir(presetRoot) {
1193
1463
  return join(presetRoot, ".characters");
@@ -1205,6 +1475,19 @@ function loadConverted(dir) {
1205
1475
  return;
1206
1476
  }
1207
1477
  }
1478
+ /** 追加角色卡本地记忆(.characters/<id>/memory.md,跟随角色卡跨预设)。 */
1479
+ function appendCharacterMemory(presetRoot, cardId, note) {
1480
+ appendMemoryFile(join(cardDir(presetRoot, cardId), "memory.md"), note, "# 角色记忆");
1481
+ }
1482
+ /** 追加记忆文件(时间戳列表格式;header 标注文件类型)。世界书工具与角色卡共用。 */
1483
+ function appendMemoryFile(file, note, header = "# 本地记忆") {
1484
+ const content = note.trim();
1485
+ if (content.length === 0) return;
1486
+ mkdirSync(dirname(file), { recursive: true });
1487
+ const line = `\n- [${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ")}] ${content}\n`;
1488
+ if (existsSync(file)) appendFileSync(file, line, "utf8");
1489
+ else writeFileSync(file, `${header}\n${line}`, "utf8");
1490
+ }
1208
1491
  /** 读角色卡本地记忆文本(无记忆返回空串)。 */
1209
1492
  function readCharacterMemory(presetRoot, cardId) {
1210
1493
  try {
@@ -1303,13 +1586,6 @@ function deleteCharacterCard(presetRoot, id) {
1303
1586
  };
1304
1587
  }
1305
1588
  }
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
1589
  /** 角色卡参数导入当前预设:promptConfigs 合并(chara-<id>- 前缀防冲突)、
1314
1590
  * params 合并(角色卡覆盖同键)、meta.importedCharacters 记录来源。 */
1315
1591
  function applyCharacterToPreset(presetRoot, templateName, cardId) {
@@ -1323,9 +1599,31 @@ function applyCharacterToPreset(presetRoot, templateName, cardId) {
1323
1599
  message: `角色卡 ${cardId} 不存在或参数损坏`
1324
1600
  };
1325
1601
  const prefix = `chara-${cardId}-`;
1602
+ const hasSystemSections = (spec.promptConfigs ?? []).some((config) => config !== null && typeof config === "object" && !Array.isArray(config) && config.layer === "system-section");
1603
+ let personaOpened = false;
1326
1604
  try {
1327
- withPresetDoc(presetRoot, templateName, (doc) => {
1605
+ withPresetDoc(join(presetRoot, templateName), (doc) => {
1328
1606
  const current = doc.toJS();
1607
+ if (hasSystemSections) {
1608
+ const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
1609
+ const personaIdx = configs.findIndex((config) => {
1610
+ if (config === null || typeof config !== "object" || Array.isArray(config)) return false;
1611
+ const entry = config;
1612
+ const params = entry.params;
1613
+ return entry.id === "persona-main" || params?.sectionName === "deployment:persona";
1614
+ });
1615
+ if (personaIdx >= 0) {
1616
+ if ((configs[personaIdx]?.params)?.complete === true) {
1617
+ doc.setIn([
1618
+ "promptConfigs",
1619
+ personaIdx,
1620
+ "params",
1621
+ "complete"
1622
+ ], false);
1623
+ personaOpened = true;
1624
+ }
1625
+ }
1626
+ }
1329
1627
  const existing = Array.isArray(current.promptConfigs) ? current.promptConfigs.filter((config) => {
1330
1628
  return config !== null && typeof config === "object" && !String(config.id ?? "").startsWith(prefix);
1331
1629
  }) : [];
@@ -1341,31 +1639,36 @@ function applyCharacterToPreset(presetRoot, templateName, cardId) {
1341
1639
  for (const [key, value] of Object.entries(spec.params ?? {})) doc.setIn(["params", key], value);
1342
1640
  const memory = readCharacterMemory(presetRoot, cardId);
1343
1641
  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]));
1642
+ const memoryEntry = buildCharacterMemoryEntry(spec, memory);
1643
+ const merged = sortConfigs([
1644
+ ...existing,
1645
+ ...added,
1646
+ ...memoryEntry !== void 0 ? [{
1647
+ ...memoryEntry,
1648
+ id: memoryId
1649
+ }] : []
1650
+ ]);
1651
+ if (hasSystemSections) {
1652
+ const persona = merged.find((config) => {
1653
+ if (config === null || typeof config !== "object" || Array.isArray(config)) return false;
1654
+ const entry = config;
1655
+ const params = entry.params;
1656
+ return entry.id === "persona-main" || params?.sectionName === "deployment:persona";
1657
+ });
1658
+ if ((persona?.params)?.complete === true && persona !== void 0) {
1659
+ persona.params.complete = false;
1660
+ personaOpened = true;
1661
+ }
1662
+ }
1663
+ doc.setIn(["promptConfigs"], merged);
1362
1664
  const list = Array.isArray(current.meta?.importedCharacters) ? current.meta.importedCharacters : [];
1363
1665
  if (!list.map(String).includes(cardId)) list.push(cardId);
1364
1666
  doc.setIn(["meta", "importedCharacters"], list);
1365
1667
  });
1366
1668
  return {
1367
1669
  ok: true,
1368
- count: (spec.promptConfigs ?? []).length
1670
+ count: (spec.promptConfigs ?? []).length,
1671
+ ...personaOpened ? { personaOpened: true } : {}
1369
1672
  };
1370
1673
  } catch (error) {
1371
1674
  return {
@@ -1385,7 +1688,7 @@ function removeCharacterFromPreset(presetRoot, templateName, cardId) {
1385
1688
  const prefix = `chara-${cardId}-`;
1386
1689
  try {
1387
1690
  let removed = 0;
1388
- withPresetDoc(presetRoot, templateName, (doc) => {
1691
+ withPresetDoc(join(presetRoot, templateName), (doc) => {
1389
1692
  const current = doc.toJS();
1390
1693
  const kept = (Array.isArray(current.promptConfigs) ? current.promptConfigs : []).filter((config) => {
1391
1694
  const isCard = config !== null && typeof config === "object" && String(config.id ?? "").startsWith(prefix);
@@ -1412,14 +1715,15 @@ function removeCharacterFromPreset(presetRoot, templateName, cardId) {
1412
1715
  //#region src/shared/bridge-contract.ts
1413
1716
  /**
1414
1717
  * settings bridge 跨端契约(host 注册 / client 消费的唯一来源)。
1415
- * 铁律:11 端点载荷统一成功 `{ ok: true, value }`、失败 `{ ok: false, code?, message? }`;
1718
+ * 铁律:25 端点载荷统一成功 `{ ok: true, value }`、失败 `{ ok: false, code?, message? }`;
1416
1719
  * 端点附加字段只能以 value 旁的可选扩展字段出现(describe)。
1417
1720
  * 改路径或载荷形状必须同步更新 test/shared/bridge-contract.test.mjs。
1418
1721
  */
1419
1722
  const SETTINGS_BRIDGE_PREFIX = "/api/prompt-tool/settings";
1420
- /** 22 个桥端点路径(相对前缀)。新增端点必须同时登记到契约测试。 */
1723
+ /** 桥端点路径(相对前缀)。新增端点必须同时登记到契约测试。 */
1421
1724
  const BRIDGE_ENDPOINTS = {
1422
1725
  meta: "/meta",
1726
+ bootstrap: "/bootstrap",
1423
1727
  describe: "/describe",
1424
1728
  models: "/models",
1425
1729
  mutate: "/mutate",
@@ -1431,6 +1735,7 @@ const BRIDGE_ENDPOINTS = {
1431
1735
  importPreset: "/import-preset",
1432
1736
  paramOverrides: "/param-overrides",
1433
1737
  presetVariables: "/preset-variables",
1738
+ customTools: "/custom-tools",
1434
1739
  importPresetPackage: "/import-preset-package",
1435
1740
  exportPreset: "/export-preset",
1436
1741
  presetDelete: "/preset-delete",
@@ -1524,6 +1829,7 @@ function restorePresetImport(targetDir, backupDir) {
1524
1829
  }
1525
1830
  /** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
1526
1831
  function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngineStrategyDir, ensureRegistered, afterSkillFix, getPresetConfigsDir, afterPresetImport, afterOverridesChange, afterPresetPackageImport) {
1832
+ let invalidateCachedDescriptor = () => {};
1527
1833
  ctx.inject(["settings", "webServer"], (sctx) => {
1528
1834
  sctx.effect(() => {
1529
1835
  let cachedDescriptor;
@@ -1540,6 +1846,7 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1540
1846
  const invalidateDescriptor = () => {
1541
1847
  cachedDescriptor = void 0;
1542
1848
  };
1849
+ invalidateCachedDescriptor = invalidateDescriptor;
1543
1850
  const guard = (req, res) => {
1544
1851
  if (!isLoopbackRequest(req)) {
1545
1852
  writeBridgeJson(res, 403, {
@@ -1559,19 +1866,150 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1559
1866
  }
1560
1867
  return true;
1561
1868
  };
1869
+ /** 引擎能力矩阵(meta 端点与 /bootstrap 共用):动态 import 引擎 schema。 */
1870
+ const loadEngineMeta = async () => {
1871
+ const { getEngineMeta } = await import(new URL("../engine/schema.mjs", import.meta.url).href);
1872
+ const meta = getEngineMeta();
1873
+ meta.presets = listPresets();
1874
+ meta.builtinTemplates = listBuiltinTemplates();
1875
+ return meta;
1876
+ };
1877
+ /** describe 运行时事实(describe 端点与 /bootstrap 共用):检测状态、技能快照、
1878
+ * 宿主默认模型、模型目录缓存、激活预设参数。不触网(模型目录只读 10min 缓存)。 */
1879
+ const collectDescribeExtras = () => {
1880
+ const detection = getModelsState();
1881
+ const skillsState = getSkillsState();
1882
+ let hostDefaultModel;
1883
+ try {
1884
+ const selection = sctx.settings.get(settingsNamespace("agent-default-model"));
1885
+ if (selection !== null && typeof selection === "object") {
1886
+ const record = selection;
1887
+ hostDefaultModel = {
1888
+ ...typeof record.provider === "string" && record.provider.length > 0 ? { provider: record.provider } : {},
1889
+ ...typeof record.model === "string" && record.model.length > 0 ? { model: record.model } : {},
1890
+ ...typeof record.reasoningEffort === "string" && record.reasoningEffort.length > 0 ? { reasoningEffort: record.reasoningEffort } : {}
1891
+ };
1892
+ if (Object.keys(hostDefaultModel).length === 0) hostDefaultModel = void 0;
1893
+ }
1894
+ } catch {}
1895
+ const modelCatalog = peekModelCatalog();
1896
+ let presetParams = {};
1897
+ let templatePreStepCount = 0;
1898
+ try {
1899
+ const activeDir = getPresetConfigsDir?.() ?? "";
1900
+ const templateName = activeDir.length > 0 ? basename(activeDir) : "anchored";
1901
+ const spec = loadPresetSpec(activeDir.length > 0 ? activeDir : resolvePresetDir(templateName));
1902
+ presetParams = resolvePresetParams(spec, {});
1903
+ if (Array.isArray(spec.promptConfigs)) presetParams.promptConfigs = spec.promptConfigs;
1904
+ templatePreStepCount = (spec.promptConfigs ?? []).filter((config) => {
1905
+ const layer = config.layer;
1906
+ return layer === void 0 || layer === "pre-step";
1907
+ }).length;
1908
+ } catch {
1909
+ templatePreStepCount = 0;
1910
+ }
1911
+ return {
1912
+ presetParams,
1913
+ hostDefaultModel,
1914
+ templatePreStepCount,
1915
+ modelsAvailable: detection.available,
1916
+ providers: detection.providers,
1917
+ modelCatalog,
1918
+ modelsError: detection.error,
1919
+ activeSkillsDirs: skillsState.activeSkillsDirs,
1920
+ skillsDirExists: Object.fromEntries(skillsState.activeSkillsDirs.map((dir) => [dir, existsSync(dir)])),
1921
+ skillCatalog: skillsState.skillCatalog
1922
+ };
1923
+ };
1924
+ /** 激活预设的引擎参数子集(/param-overrides 读取;preset.yml 按 mtime 缓存)。 */
1925
+ const readParamOverrides = (dir) => {
1926
+ try {
1927
+ const spec = loadPresetSpec(dir);
1928
+ const params = {};
1929
+ for (const key of PARAM_KEYS) {
1930
+ if (key === "promptConfigs") continue;
1931
+ if (spec.params !== null && typeof spec.params === "object" && key in spec.params) params[key] = spec.params[key];
1932
+ }
1933
+ return params;
1934
+ } catch {
1935
+ return {};
1936
+ }
1937
+ };
1938
+ /** 预设级模板变量(/preset-variables 读取)。 */
1939
+ const readPresetVariables = (dir) => {
1940
+ try {
1941
+ const spec = loadPresetSpec(dir);
1942
+ const variables = {};
1943
+ for (const [key, value] of Object.entries(spec.params ?? {})) if (!PARAM_KEYS.has(key) && typeof value === "string") variables[key] = value;
1944
+ for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string") variables[key] = value;
1945
+ return {
1946
+ variables,
1947
+ enabled: spec.variablesEnabled !== false
1948
+ };
1949
+ } catch {
1950
+ return {
1951
+ variables: {},
1952
+ enabled: true
1953
+ };
1954
+ }
1955
+ };
1956
+ /** 生成目录实际生效配置(/prompt-configs 读取)。 */
1957
+ const readPromptConfigs = (dir) => {
1958
+ try {
1959
+ return dir.length > 0 ? loadPromptConfigFiles(join(dir, "prompt-configs")) : [];
1960
+ } catch {
1961
+ return [];
1962
+ }
1963
+ };
1562
1964
  const disposers = [
1965
+ sctx.webServer.register({
1966
+ kind: "exact",
1967
+ path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.bootstrap,
1968
+ handler: async (req, res) => {
1969
+ if (!guard(req, res)) return;
1970
+ ensureRegistered(sctx);
1971
+ const descriptor = findDescriptor();
1972
+ if (descriptor === void 0) {
1973
+ writeBridgeJson(res, 404, {
1974
+ ok: false,
1975
+ code: "settings-not-exposed",
1976
+ message: "prompt-tool settings namespace is not registered"
1977
+ });
1978
+ return;
1979
+ }
1980
+ try {
1981
+ const meta = await loadEngineMeta();
1982
+ const dir = getPresetConfigsDir?.() ?? "";
1983
+ const extras = collectDescribeExtras();
1984
+ writeBridgeJson(res, 200, {
1985
+ ok: true,
1986
+ value: descriptor,
1987
+ meta: { meta },
1988
+ overrides: { overrides: dir.length > 0 ? readParamOverrides(dir) : {} },
1989
+ variables: dir.length > 0 ? readPresetVariables(dir) : {
1990
+ variables: {},
1991
+ enabled: true
1992
+ },
1993
+ promptConfigs: { promptConfigs: readPromptConfigs(dir) },
1994
+ ...extras
1995
+ });
1996
+ } catch (error) {
1997
+ writeBridgeJson(res, 500, {
1998
+ ok: false,
1999
+ code: "bootstrap-failed",
2000
+ message: error instanceof Error ? error.message : String(error)
2001
+ });
2002
+ }
2003
+ }
2004
+ }),
1563
2005
  sctx.webServer.register({
1564
2006
  kind: "exact",
1565
2007
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.meta,
1566
2008
  handler: async (req, res) => {
1567
2009
  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
2010
  writeBridgeJson(res, 200, {
1573
2011
  ok: true,
1574
- value: { meta }
2012
+ value: { meta: await loadEngineMeta() }
1575
2013
  });
1576
2014
  }
1577
2015
  }),
@@ -1590,50 +2028,10 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1590
2028
  });
1591
2029
  return;
1592
2030
  }
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
2031
  writeBridgeJson(res, 200, {
1625
2032
  ok: true,
1626
2033
  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
2034
+ ...collectDescribeExtras()
1637
2035
  });
1638
2036
  }
1639
2037
  }),
@@ -1716,7 +2114,15 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1716
2114
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.configsValidate,
1717
2115
  handler: async (req, res) => {
1718
2116
  if (!guard(req, res)) return;
1719
- const { body } = await readBridgeBody(req);
2117
+ const { body, tooLarge } = await readBridgeBody(req, PRESET_PACKAGE_MAX_BYTES);
2118
+ if (tooLarge) {
2119
+ writeBridgeJson(res, 413, {
2120
+ ok: false,
2121
+ code: "configs-too-large",
2122
+ message: `配置载荷超过 ${Math.round(PRESET_PACKAGE_MAX_BYTES / 1024 / 1024)}MB 上限`
2123
+ });
2124
+ return;
2125
+ }
1720
2126
  if (body === null || body === void 0 || typeof body !== "object") {
1721
2127
  writeBridgeJson(res, 400, {
1722
2128
  ok: false,
@@ -1815,7 +2221,10 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1815
2221
  try {
1816
2222
  writeBridgeJson(res, 200, {
1817
2223
  ok: true,
1818
- value: { templates: loadPromptTemplates() }
2224
+ value: {
2225
+ templates: loadPromptTemplates(),
2226
+ toolTemplates: loadToolTemplates()
2227
+ }
1819
2228
  });
1820
2229
  } catch (error) {
1821
2230
  writeBridgeJson(res, 500, {
@@ -1831,18 +2240,11 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1831
2240
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.promptConfigs,
1832
2241
  handler: async (req, res) => {
1833
2242
  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
- }
2243
+ const dir = getPresetConfigsDir?.() ?? "";
2244
+ writeBridgeJson(res, 200, {
2245
+ ok: true,
2246
+ value: { promptConfigs: readPromptConfigs(dir) }
2247
+ });
1846
2248
  }
1847
2249
  }),
1848
2250
  sctx.webServer.register({
@@ -1871,7 +2273,15 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1871
2273
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.importPreset,
1872
2274
  handler: async (req, res) => {
1873
2275
  if (!guard(req, res)) return;
1874
- const { body } = await readBridgeBody(req);
2276
+ const { body, tooLarge } = await readBridgeBody(req, PRESET_PACKAGE_MAX_BYTES);
2277
+ if (tooLarge) {
2278
+ writeBridgeJson(res, 413, {
2279
+ ok: false,
2280
+ code: "preset-import-too-large",
2281
+ message: `内容资产超过 ${Math.round(PRESET_PACKAGE_MAX_BYTES / 1024 / 1024)}MB 上限`
2282
+ });
2283
+ return;
2284
+ }
1875
2285
  if (body === null || body === void 0 || typeof body !== "object") {
1876
2286
  writeBridgeJson(res, 400, {
1877
2287
  ok: false,
@@ -1881,8 +2291,27 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1881
2291
  return;
1882
2292
  }
1883
2293
  const record = body;
1884
- const scope = record.scope === "agents" ? "agents" : "preset";
1885
- const content = typeof record.content === "string" ? record.content : "";
2294
+ const contents = [];
2295
+ if (Array.isArray(record.contents)) for (const entry of record.contents) {
2296
+ if (entry === null || typeof entry !== "object") continue;
2297
+ const item = entry;
2298
+ contents.push({
2299
+ scope: item.scope === "agents" ? "agents" : "preset",
2300
+ content: typeof item.content === "string" ? item.content : ""
2301
+ });
2302
+ }
2303
+ else contents.push({
2304
+ scope: record.scope === "agents" ? "agents" : "preset",
2305
+ content: typeof record.content === "string" ? record.content : ""
2306
+ });
2307
+ if (contents.length === 0) {
2308
+ writeBridgeJson(res, 400, {
2309
+ ok: false,
2310
+ code: "settings-rejected",
2311
+ message: "unreadable JSON body"
2312
+ });
2313
+ return;
2314
+ }
1886
2315
  const dir = getPresetConfigsDir?.() ?? "";
1887
2316
  if (dir.length === 0) {
1888
2317
  writeBridgeJson(res, 400, {
@@ -1894,11 +2323,11 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1894
2323
  }
1895
2324
  try {
1896
2325
  mkdirSync(dir, { recursive: true });
1897
- writeFileSync(join(dir, scope === "preset" ? "preset.md" : "agents.md"), content, "utf8");
1898
- afterPresetImport?.(scope);
2326
+ for (const entry of contents) writeFileSync(join(dir, entry.scope === "preset" ? "preset.md" : "agents.md"), entry.content, "utf8");
2327
+ afterPresetImport?.(contents.map((entry) => entry.scope));
1899
2328
  writeBridgeJson(res, 200, {
1900
2329
  ok: true,
1901
- value: { scope }
2330
+ value: { scopes: contents.map((entry) => entry.scope) }
1902
2331
  });
1903
2332
  } catch (error) {
1904
2333
  writeBridgeJson(res, 500, {
@@ -1925,31 +2354,47 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1925
2354
  }
1926
2355
  const presetRoot = dirname(dir);
1927
2356
  const templateName = basename(dir);
1928
- const { body } = await readBridgeBody(req);
2357
+ const { body, tooLarge } = await readBridgeBody(req, PRESET_PACKAGE_MAX_BYTES);
2358
+ if (tooLarge) {
2359
+ writeBridgeJson(res, 413, {
2360
+ ok: false,
2361
+ code: "overrides-too-large",
2362
+ message: `参数载荷超过 ${Math.round(PRESET_PACKAGE_MAX_BYTES / 1024 / 1024)}MB 上限`
2363
+ });
2364
+ return;
2365
+ }
1929
2366
  const record = body ?? {};
1930
2367
  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: {} }
2368
+ writeBridgeJson(res, 200, {
2369
+ ok: true,
2370
+ value: { overrides: readParamOverrides(dir) }
2371
+ });
2372
+ return;
2373
+ }
2374
+ const rawOverrides = record.overrides !== null && typeof record.overrides === "object" && !Array.isArray(record.overrides) ? record.overrides : void 0;
2375
+ if (rawOverrides !== void 0) {
2376
+ const unknownKeys = Object.keys(rawOverrides).filter((key) => key === "promptConfigs" || !PARAM_KEYS.has(key));
2377
+ if (unknownKeys.length > 0) {
2378
+ writeBridgeJson(res, 400, {
2379
+ ok: false,
2380
+ code: "overrides-unknown-key",
2381
+ message: `未知引擎参数键:${unknownKeys.join(", ")}`
1946
2382
  });
2383
+ return;
1947
2384
  }
2385
+ }
2386
+ const valueErrors = rawOverrides !== void 0 ? validateEngineParamValues(rawOverrides) : [];
2387
+ if (valueErrors.length > 0) {
2388
+ writeBridgeJson(res, 400, {
2389
+ ok: false,
2390
+ code: "overrides-invalid-value",
2391
+ message: valueErrors.map((item) => item.message).join("; ")
2392
+ });
1948
2393
  return;
1949
2394
  }
1950
2395
  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?.();
2396
+ savePresetParams(presetRoot, templateName, rawOverrides, Array.isArray(record.promptConfigs) ? record.promptConfigs : void 0);
2397
+ if (record.rebuild !== false) afterOverridesChange?.();
1953
2398
  writeBridgeJson(res, 200, {
1954
2399
  ok: true,
1955
2400
  value: {
@@ -1985,41 +2430,83 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1985
2430
  const { body } = await readBridgeBody(req);
1986
2431
  const record = body ?? {};
1987
2432
  if (record.variables === void 0 && record.enabled === void 0) {
2433
+ writeBridgeJson(res, 200, {
2434
+ ok: true,
2435
+ value: readPresetVariables(dir)
2436
+ });
2437
+ return;
2438
+ }
2439
+ try {
2440
+ 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);
2441
+ afterOverridesChange?.();
2442
+ writeBridgeJson(res, 200, {
2443
+ ok: true,
2444
+ value: { variables: record.variables }
2445
+ });
2446
+ } catch (error) {
2447
+ writeBridgeJson(res, 409, {
2448
+ ok: false,
2449
+ code: "preset-variables-rejected",
2450
+ message: `模板变量保存失败:${error instanceof Error ? error.message : String(error)}`
2451
+ });
2452
+ }
2453
+ }
2454
+ }),
2455
+ sctx.webServer.register({
2456
+ kind: "exact",
2457
+ path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.customTools,
2458
+ handler: async (req, res) => {
2459
+ if (!guard(req, res)) return;
2460
+ const dir = getPresetConfigsDir?.() ?? "";
2461
+ if (dir.length === 0) {
2462
+ writeBridgeJson(res, 400, {
2463
+ ok: false,
2464
+ code: "preset-dir-unavailable",
2465
+ message: "presetDir 未配置"
2466
+ });
2467
+ return;
2468
+ }
2469
+ const { body } = await readBridgeBody(req);
2470
+ const record = body ?? {};
2471
+ if (record.customTools === void 0) {
1988
2472
  try {
1989
2473
  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
2474
  writeBridgeJson(res, 200, {
1994
2475
  ok: true,
1995
- value: {
1996
- variables,
1997
- enabled: spec.variablesEnabled !== false
1998
- }
2476
+ value: { customTools: Array.isArray(spec.customTools) ? spec.customTools : [] }
1999
2477
  });
2000
2478
  } catch {
2001
2479
  writeBridgeJson(res, 200, {
2002
2480
  ok: true,
2003
- value: {
2004
- variables: {},
2005
- enabled: true
2006
- }
2481
+ value: { customTools: [] }
2007
2482
  });
2008
2483
  }
2009
2484
  return;
2010
2485
  }
2011
2486
  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);
2487
+ const customTools = record.customTools;
2488
+ if (!Array.isArray(customTools)) {
2489
+ writeBridgeJson(res, 400, {
2490
+ ok: false,
2491
+ code: "custom-tools-invalid",
2492
+ message: "customTools 必须是数组"
2493
+ });
2494
+ return;
2495
+ }
2496
+ withPresetDoc(dir, (doc) => {
2497
+ if (customTools.length === 0) doc.deleteIn(["customTools"]);
2498
+ else doc.setIn(["customTools"], customTools);
2499
+ });
2013
2500
  afterOverridesChange?.();
2014
2501
  writeBridgeJson(res, 200, {
2015
2502
  ok: true,
2016
- value: { variables: record.variables }
2503
+ value: { customTools }
2017
2504
  });
2018
2505
  } catch (error) {
2019
2506
  writeBridgeJson(res, 409, {
2020
2507
  ok: false,
2021
- code: "preset-variables-rejected",
2022
- message: `模板变量保存失败:${error instanceof Error ? error.message : String(error)}`
2508
+ code: "custom-tools-rejected",
2509
+ message: `自定义工具保存失败:${error instanceof Error ? error.message : String(error)}`
2023
2510
  });
2024
2511
  }
2025
2512
  }
@@ -2583,6 +3070,7 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2583
3070
  };
2584
3071
  }, "prompt-tool: settings bridge");
2585
3072
  });
3073
+ return { invalidateDescriptor: () => invalidateCachedDescriptor() };
2586
3074
  }
2587
3075
  //#endregion
2588
3076
  //#region src/runtime/character-tools.ts
@@ -2590,9 +3078,11 @@ const text$2 = (text) => [{
2590
3078
  type: "text",
2591
3079
  text
2592
3080
  }];
3081
+ /** 注册角色卡库模型工具;返回 disposer(预设切换重挂用)。enabled=false 返回空操作。 */
2593
3082
  function registerCharacterTools(ctx, host) {
2594
- ctx.inject(["tools"], (toolsCtx) => {
2595
- toolsCtx.tools.register(defineTool({
3083
+ const fiber = ctx.inject(["tools"], (toolsCtx) => {
3084
+ const disposers = [];
3085
+ disposers.push(toolsCtx.tools.register(defineTool({
2596
3086
  name: "character_list",
2597
3087
  description: "列出 SillyTavern 角色卡库:每张卡(id / 名称 / 描述 / 是否已导入当前预设)。导入角色卡、应用到当前预设或移除前先调用本工具获取 id。",
2598
3088
  parameters: {},
@@ -2627,8 +3117,8 @@ function registerCharacterTools(ctx, host) {
2627
3117
  render: (_args, value) => text$2(JSON.stringify(value.characters))
2628
3118
  },
2629
3119
  execute: async () => ({ characters: listCharacterCards(host.presetRoot(), host.templateName()) })
2630
- }));
2631
- toolsCtx.tools.register(defineTool({
3120
+ })));
3121
+ disposers.push(toolsCtx.tools.register(defineTool({
2632
3122
  name: "character_import",
2633
3123
  description: "导入 SillyTavern 角色卡(chara_card_v2/v3 JSON)到角色卡库:接收角色卡 JSON 文本内容(可先读取文件)。PNG 角色卡请让用户从 UI 角色管理页导入。导入后需调用 character_apply 应用到当前预设。",
2634
3124
  parameters: {
@@ -2671,8 +3161,8 @@ function registerCharacterTools(ctx, host) {
2671
3161
  name: result.name
2672
3162
  };
2673
3163
  }
2674
- }));
2675
- toolsCtx.tools.register(defineTool({
3164
+ })));
3165
+ disposers.push(toolsCtx.tools.register(defineTool({
2676
3166
  name: "character_apply",
2677
3167
  description: "把角色卡库中一张角色卡的参数(角色设定 / 系统提示 / 开场白 / 世界书 / 提示词库 / 采样参数)合并进当前激活预设(promptConfigs 带 chara-<id>- 前缀防冲突,params 合并,meta.importedCharacters 记录),并立即重建生成目录。重复应用幂等。",
2678
3168
  parameters: { id: {
@@ -2706,8 +3196,8 @@ function registerCharacterTools(ctx, host) {
2706
3196
  count: result.count
2707
3197
  };
2708
3198
  }
2709
- }));
2710
- toolsCtx.tools.register(defineTool({
3199
+ })));
3200
+ disposers.push(toolsCtx.tools.register(defineTool({
2711
3201
  name: "character_remove",
2712
3202
  description: "从当前激活预设移除一张已导入角色卡的参数(删 chara-<id>- 前缀配置、该卡声明的 params 键、meta.importedCharacters 除名),并立即重建生成目录。角色卡库条目不受影响。",
2713
3203
  parameters: { id: {
@@ -2741,8 +3231,8 @@ function registerCharacterTools(ctx, host) {
2741
3231
  count: result.count
2742
3232
  };
2743
3233
  }
2744
- }));
2745
- toolsCtx.tools.register(defineTool({
3234
+ })));
3235
+ disposers.push(toolsCtx.tools.register(defineTool({
2746
3236
  name: "character_delete",
2747
3237
  description: "从角色卡库删除一张角色卡(含其转换参数与头像)。已导入当前预设的参数不受影响(如需清理请先调用 character_remove)。",
2748
3238
  parameters: { id: {
@@ -2766,8 +3256,14 @@ function registerCharacterTools(ctx, host) {
2766
3256
  if (!result.ok) throw new Error(result.message);
2767
3257
  return { id: args.id };
2768
3258
  }
2769
- }));
3259
+ })));
3260
+ return () => {
3261
+ for (const dispose of disposers) dispose();
3262
+ };
2770
3263
  });
3264
+ return () => {
3265
+ fiber.dispose();
3266
+ };
2771
3267
  }
2772
3268
  //#endregion
2773
3269
  //#region src/runtime/world-book-tools.ts
@@ -2775,26 +3271,6 @@ const text$1 = (text) => [{
2775
3271
  type: "text",
2776
3272
  text
2777
3273
  }];
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
3274
  /** 从条目 id 解析来源角色卡(chara-<cardId>- 前缀,cardId 可含连字符:遍历库目录取最长匹配)。 */
2799
3275
  function sourceCardId(presetRoot, entryId) {
2800
3276
  const root = join(presetRoot, ".characters");
@@ -2808,16 +3284,22 @@ function sourceCardId(presetRoot, entryId) {
2808
3284
  } catch {}
2809
3285
  return best;
2810
3286
  }
3287
+ /** 注册世界书条目级模型工具;返回 disposer(预设切换重挂用)。enabled=false 返回空操作。 */
2811
3288
  function registerWorldBookTools(ctx, host) {
2812
- ctx.inject(["tools"], (toolsCtx) => {
3289
+ const fiber = ctx.inject(["tools"], (toolsCtx) => {
3290
+ const disposers = [];
2813
3291
  /** note 归属写入:角色卡条目 → 卡记忆;其他 → 预设记忆。 */
2814
3292
  const writeNote = (entryId, note) => {
2815
3293
  if (note === void 0 || note.trim().length === 0) return;
2816
3294
  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);
3295
+ if (cardId !== void 0) {
3296
+ appendMemoryFile(join(host.presetRoot(), ".characters", cardId, "memory.md"), note, "# 角色记忆");
3297
+ try {
3298
+ syncImportedCharacterMemory(host.presetRoot(), basename(host.activeDir()), cardId);
3299
+ } catch {}
3300
+ } else appendMemoryFile(join(host.activeDir(), "memory.md"), note, "# 本地记忆");
2819
3301
  };
2820
- toolsCtx.tools.register(defineTool({
3302
+ disposers.push(toolsCtx.tools.register(defineTool({
2821
3303
  name: "world_book_list",
2822
3304
  description: "列出当前预设的世界书条目(world-book 策略配置:id / 名称 / 关键字 / 常驻 / 启用)。世界书 = 上下文条目:无 keys 的全局条目每次注入,有 keys 条目命中聊天内容才注入。增删改前先调用本工具获取 id。",
2823
3305
  parameters: {},
@@ -2853,7 +3335,7 @@ function registerWorldBookTools(ctx, host) {
2853
3335
  render: (_args, value) => text$1(JSON.stringify(value.entries))
2854
3336
  },
2855
3337
  execute: async () => {
2856
- return { entries: readConfigs(host.activeDir()).filter((config) => config.strategy === "world-book").map((config) => ({
3338
+ return { entries: listWorldBookEntries(host.activeDir()).map((config) => ({
2857
3339
  id: String(config.id ?? ""),
2858
3340
  name: String(config.name ?? config.id ?? ""),
2859
3341
  keys: Array.isArray(config.params && config.params.keys) ? config.params.keys : void 0,
@@ -2861,8 +3343,8 @@ function registerWorldBookTools(ctx, host) {
2861
3343
  enabled: config.enabled !== false
2862
3344
  })) };
2863
3345
  }
2864
- }));
2865
- toolsCtx.tools.register(defineTool({
3346
+ })));
3347
+ disposers.push(toolsCtx.tools.register(defineTool({
2866
3348
  name: "world_book_upsert",
2867
3349
  description: "新增或更新当前预设的一条世界书条目(world-book 策略配置):按 id 更新(不存在则新增,id 自动生成 lore-<n>)。constant=true 常驻注入;否则命中 keys(或 secondaryKeys)任一关键字注入;无 keys 条目按全局每次注入。note 可选:写入来源角色卡的持久记忆(memory.md,条目 id 带 chara-<卡>- 前缀时)或预设记忆——持久记忆跨会话跟随角色卡(与 session_var 会话变量的临时状态不同,适合长期关系记录)。写盘后立即重建生成目录。",
2868
3350
  parameters: {
@@ -2926,37 +3408,29 @@ function registerWorldBookTools(ctx, host) {
2926
3408
  },
2927
3409
  execute: async (args) => {
2928
3410
  const dir = host.activeDir();
2929
- const configs = readConfigs(dir);
2930
3411
  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);
3412
+ const count = upsertWorldBookEntry(dir, {
3413
+ ...buildWorldBookEntry({
3414
+ id: targetId,
3415
+ name: args.name,
3416
+ text: args.content,
3417
+ order: typeof args.order === "number" ? args.order : void 0,
3418
+ enabled: args.enabled === false ? false : void 0,
3419
+ constant: args.constant === true,
3420
+ keys: Array.isArray(args.keys) && args.keys.length > 0 ? args.keys : void 0,
3421
+ secondaryKeys: Array.isArray(args.secondaryKeys) && args.secondaryKeys.length > 0 ? args.secondaryKeys : void 0
3422
+ }),
3423
+ id: targetId
3424
+ });
2951
3425
  writeNote(targetId, typeof args.note === "string" ? args.note : "");
2952
3426
  host.rebuild();
2953
3427
  return {
2954
3428
  id: targetId,
2955
- count: configs.filter((config) => config.strategy === "world-book").length
3429
+ count
2956
3430
  };
2957
3431
  }
2958
- }));
2959
- toolsCtx.tools.register(defineTool({
3432
+ })));
3433
+ disposers.push(toolsCtx.tools.register(defineTool({
2960
3434
  name: "world_book_delete",
2961
3435
  description: "删除当前预设的一条世界书条目(world_book_list 获取 id)。note 可选:写入来源角色卡持久记忆(memory.md,跨会话跟随角色卡)或预设记忆。删除后立即重建生成目录。",
2962
3436
  parameters: {
@@ -2988,20 +3462,22 @@ function registerWorldBookTools(ctx, host) {
2988
3462
  render: (_args, value) => text$1(`世界书条目 ${value.id} 已删除(剩余 ${value.count} 条),生成目录已重建。`)
2989
3463
  },
2990
3464
  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);
3465
+ const keptCount = deleteWorldBookEntry(host.activeDir(), args.id);
2996
3466
  writeNote(args.id, typeof args.note === "string" ? args.note : "");
2997
3467
  host.rebuild();
2998
3468
  return {
2999
3469
  id: args.id,
3000
- count: kept.filter((config) => config.strategy === "world-book").length
3470
+ count: keptCount
3001
3471
  };
3002
3472
  }
3003
- }));
3473
+ })));
3474
+ return () => {
3475
+ for (const dispose of disposers) dispose();
3476
+ };
3004
3477
  });
3478
+ return () => {
3479
+ fiber.dispose();
3480
+ };
3005
3481
  }
3006
3482
  //#endregion
3007
3483
  //#region src/runtime/session-var-tools.ts
@@ -3009,9 +3485,11 @@ const text = (value) => [{
3009
3485
  type: "text",
3010
3486
  text: value
3011
3487
  }];
3488
+ /** 注册会话变量模型工具;返回 disposer(预设切换重挂用)。enabled=false 返回空操作。 */
3012
3489
  function registerSessionVarTools(ctx) {
3013
- ctx.inject(["tools"], (toolsCtx) => {
3014
- toolsCtx.tools.register(defineTool({
3490
+ const fiber = ctx.inject(["tools"], (toolsCtx) => {
3491
+ const disposers = [];
3492
+ disposers.push(toolsCtx.tools.register(defineTool({
3015
3493
  name: "session_var",
3016
3494
  description: "会话变量管理(SillyTavern setvar/getvar 语义):list 查看当前会话全部变量、get 读取、set 设置、clear 清除。提示词/世界书文本中的 {{变量名}} 会在注入时替换为会话变量值(会话级覆盖预设默认值);适合维护角色状态(如 {{心情}}、{{接受度}} 等)。注意:会话变量仅存于当前会话(结束即失);跨会话长期记忆请用 world_book 工具的 note 参数写入角色卡记忆(持久,跟随角色卡)。",
3017
3495
  parameters: {
@@ -3106,8 +3584,14 @@ function registerSessionVarTools(ctx) {
3106
3584
  error: `未知 action: ${action}`
3107
3585
  };
3108
3586
  }
3109
- }));
3587
+ })));
3588
+ return () => {
3589
+ for (const dispose of disposers) dispose();
3590
+ };
3110
3591
  });
3592
+ return () => {
3593
+ fiber.dispose();
3594
+ };
3111
3595
  }
3112
3596
  //#endregion
3113
3597
  //#region src/runtime/tui.ts
@@ -3121,6 +3605,7 @@ const TUI_GLOBAL_SWITCHES = [
3121
3605
  const TUI_PARAM_SWITCHES = [
3122
3606
  ["injectPrompt", "锚定确认后注入 preset.md"],
3123
3607
  ["firstTurnAnchor", "追加任务引导"],
3608
+ ["guideEnabled", "每轮引导(独立开关;缺省跟随锚定)"],
3124
3609
  ["firstTurnCustom", "使用自定义引导(首句)"],
3125
3610
  ["guideCustom", "使用自定义引导(每轮)"],
3126
3611
  ["usePtcMode", "使用 PTC 模式"]
@@ -3291,18 +3776,56 @@ function parseTuiBoolean(token, current) {
3291
3776
  if (token === "off") return false;
3292
3777
  if (token === "toggle") return !current;
3293
3778
  }
3779
+ function asCommandAction(token) {
3780
+ return token === "on" || token === "off" || token === "toggle" ? token : void 0;
3781
+ }
3782
+ /**
3783
+ * 解析可含空格的标识符与末尾动作。
3784
+ *
3785
+ * 斜杠命令没有 shell 引号语义,因此动作只认“最后一个独立 token”。config 的
3786
+ * 详情形态没有动作;只有去掉末尾动作后的候选 id 已存在时才把末尾 token 视为
3787
+ * 动作,避免把普通 id 误拆。
3788
+ */
3789
+ function parseIdentifierAndAction(tokens, exists) {
3790
+ if (tokens.length === 0) return { id: "" };
3791
+ const last = tokens[tokens.length - 1];
3792
+ const action = asCommandAction(last ?? "");
3793
+ if (action !== void 0 && tokens.length >= 2) {
3794
+ const id = tokens.slice(0, -1).join(" ");
3795
+ if (exists(id)) return {
3796
+ id,
3797
+ action
3798
+ };
3799
+ }
3800
+ return { id: tokens.join(" ") };
3801
+ }
3294
3802
  /** 通过 DSH 命令注册表暴露 /prompt-tool,Web 与 dsh-tui 都能执行。 */
3295
3803
  function registerTuiCommand(ctx, ns, getSource, getModelsState, getModelCatalog, getPresetConfigsDir, savePresetParam) {
3296
3804
  ctx.inject(["settings"], (sctx) => {
3297
3805
  return sctx.commands.register({
3298
3806
  name: "prompt-tool",
3299
3807
  description: "提示词工具:查看或切换本插件开关",
3300
- input: { hint: "status | on/off/toggle <开关> | skill <目录名> on/off | config <id> [on/off/toggle]" },
3808
+ input: { hint: "status | on/off/toggle <开关> | skill <目录名> on/off/toggle | config <id> [on/off/toggle]" },
3301
3809
  handler: async (invocation) => {
3302
3810
  const usage = () => ({
3303
3811
  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(关闭)>"
3812
+ 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
3813
  });
3814
+ const persistPresetParam = async (key, value) => {
3815
+ if (savePresetParam === void 0) return {
3816
+ kind: "error",
3817
+ text: `无法保存 ${key}:预设参数保存回调不可用`
3818
+ };
3819
+ try {
3820
+ await savePresetParam(key, value);
3821
+ return;
3822
+ } catch (error) {
3823
+ return {
3824
+ kind: "error",
3825
+ text: `保存 ${key} 失败:${error instanceof Error ? error.message : String(error)}`
3826
+ };
3827
+ }
3828
+ };
3306
3829
  const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
3307
3830
  const source = getSource();
3308
3831
  const presetDir = getPresetConfigsDir?.();
@@ -3320,10 +3843,9 @@ function registerTuiCommand(ctx, ns, getSource, getModelsState, getModelCatalog,
3320
3843
  };
3321
3844
  }
3322
3845
  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);
3846
+ const { id: folder, action } = parseIdentifierAndAction(tokens.slice(1), () => true);
3847
+ if (folder.length === 0) return usage();
3848
+ const next = parseTuiBoolean(action, source.skillSwitches[folder] !== false);
3327
3849
  if (next === void 0) return usage();
3328
3850
  await sctx.settings.mutate(ns, [{
3329
3851
  op: "set",
@@ -3338,25 +3860,24 @@ ${renderTuiStatus(getSource(), readPresetParams(getPresetConfigsDir?.()), resolv
3338
3860
  };
3339
3861
  }
3340
3862
  if (tokens[0] === "config") {
3341
- const id = tokens[1];
3342
- if (id === void 0) return usage();
3863
+ const { id, action } = parseIdentifierAndAction(tokens.slice(1), (candidate) => promptConfigs.some((config) => config.id === candidate));
3864
+ if (id.length === 0) return usage();
3343
3865
  const current = promptConfigs.find((config) => config.id === id);
3344
3866
  if (current === void 0) return {
3345
3867
  kind: "error",
3346
3868
  text: `未找到提示词配置 ${id};用 /prompt-tool status 查看全部 id。`
3347
3869
  };
3348
- const action = tokens[2];
3349
3870
  if (action === void 0) return {
3350
3871
  kind: "success",
3351
3872
  text: renderConfigDetail(source, id, promptConfigs)
3352
3873
  };
3353
3874
  const next = parseTuiBoolean(action, current.enabled !== false);
3354
3875
  if (next === void 0) return usage();
3355
- const nextConfigs = promptConfigs.map((config) => config.id === id ? {
3876
+ const failure = await persistPresetParam("promptConfigs", promptConfigs.map((config) => config.id === id ? {
3356
3877
  ...config,
3357
3878
  enabled: next
3358
- } : config);
3359
- savePresetParam?.("promptConfigs", nextConfigs);
3879
+ } : config));
3880
+ if (failure !== void 0) return failure;
3360
3881
  return {
3361
3882
  kind: "success",
3362
3883
  text: `已把提示词配置 ${id} 设为 ${next ? "开" : "关"}
@@ -3371,7 +3892,8 @@ ${renderConfigDetail(getSource(), id, resolvePromptConfigs(getPresetConfigsDir?.
3371
3892
  kind: "error",
3372
3893
  text: "bootstrapMaxTokens 需要非负整数:0 关闭封顶,正整数设置首轮 maxTokens。"
3373
3894
  };
3374
- savePresetParam?.("bootstrapMaxTokens", value);
3895
+ const failure = await persistPresetParam("bootstrapMaxTokens", value);
3896
+ if (failure !== void 0) return failure;
3375
3897
  return {
3376
3898
  kind: "success",
3377
3899
  text: `已把 bootstrapMaxTokens 设为 ${value === 0 ? "关闭(不设封顶)" : String(value)}
@@ -3396,7 +3918,10 @@ ${renderTuiStatus(getSource(), readPresetParams(getPresetConfigsDir?.()), resolv
3396
3918
  path: [key],
3397
3919
  value: next
3398
3920
  }]);
3399
- else savePresetParam?.(key, next);
3921
+ else {
3922
+ const failure = await persistPresetParam(key, next);
3923
+ if (failure !== void 0) return failure;
3924
+ }
3400
3925
  return {
3401
3926
  kind: "success",
3402
3927
  text: `已把 ${key} 设为 ${next ? "开" : "关"}
@@ -3547,8 +4072,9 @@ function runtimeOf(options, prompt) {
3547
4072
  firstTurnText: typeof options.firstTurnText === "string" ? options.firstTurnText : "",
3548
4073
  guideCustom: options.guideCustom === true,
3549
4074
  guideText: typeof options.guideText === "string" ? options.guideText : "",
4075
+ guideEnabled: typeof options.guideEnabled === "boolean" ? options.guideEnabled : void 0,
3550
4076
  injectPrompt: options.injectPrompt !== false,
3551
- usePtcMode: options.usePtcMode !== false,
4077
+ usePtcMode: typeof options.usePtcMode === "boolean" ? options.usePtcMode : void 0,
3552
4078
  bootstrapMaxTokens: Number.isSafeInteger(options.bootstrapMaxTokens) ? options.bootstrapMaxTokens : 0,
3553
4079
  modelProvider: typeof options.modelProvider === "string" && options.modelProvider.length > 0 ? options.modelProvider : "",
3554
4080
  modelName: typeof options.modelName === "string" && options.modelName.length > 0 ? options.modelName : "",
@@ -3560,12 +4086,19 @@ function runtimeOf(options, prompt) {
3560
4086
  subagentReasoningEffort: typeof options.subagentReasoningEffort === "string" && options.subagentReasoningEffort.length > 0 ? options.subagentReasoningEffort : void 0,
3561
4087
  subagentTemperature: typeof options.subagentTemperature === "string" && options.subagentTemperature.length > 0 ? options.subagentTemperature : void 0,
3562
4088
  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
4089
  toolFilterAllow: options.toolFilterAllow !== void 0 && (Array.isArray(options.toolFilterAllow) ? options.toolFilterAllow.length > 0 : String(options.toolFilterAllow).trim().length > 0) ? options.toolFilterAllow : void 0,
3565
4090
  toolFilterDeny: options.toolFilterDeny !== void 0 && (Array.isArray(options.toolFilterDeny) ? options.toolFilterDeny.length > 0 : String(options.toolFilterDeny).trim().length > 0) ? options.toolFilterDeny : void 0,
3566
4091
  maxDepth: options.maxDepth,
3567
4092
  allowKinds: options.allowKinds,
3568
- firstTurnWord: typeof options.firstTurnWord === "string" && options.firstTurnWord.length > 0 ? options.firstTurnWord : void 0
4093
+ firstTurnWord: typeof options.firstTurnWord === "string" && options.firstTurnWord.length > 0 ? options.firstTurnWord : void 0,
4094
+ anchorTurn: typeof options.anchorTurn === "boolean" ? options.anchorTurn : void 0,
4095
+ anchorTurnText: typeof options.anchorTurnText === "string" ? options.anchorTurnText : void 0,
4096
+ deliberationGate: typeof options.deliberationGate === "boolean" ? options.deliberationGate : void 0,
4097
+ deliberationMinChars: Number.isSafeInteger(options.deliberationMinChars) ? options.deliberationMinChars : void 0,
4098
+ deliberationMaxGatesPerTurn: Number.isSafeInteger(options.deliberationMaxGatesPerTurn) ? options.deliberationMaxGatesPerTurn : void 0,
4099
+ cotDrip: typeof options.cotDrip === "boolean" ? options.cotDrip : void 0,
4100
+ cotDripEvery: Number.isSafeInteger(options.cotDripEvery) ? options.cotDripEvery : void 0,
4101
+ cotDripMaxPerTurn: Number.isSafeInteger(options.cotDripMaxPerTurn) ? options.cotDripMaxPerTurn : void 0
3569
4102
  };
3570
4103
  }
3571
4104
  /** 模型参数(思维程度/温度/输出上限)→ agent-request 提示词配置(官方 LlmCallConfig patch 浅合并)。
@@ -3609,6 +4142,8 @@ function writePreset(prompt, options) {
3609
4142
  const presetDir = options.presetDir.trim().length > 0 ? options.presetDir : DEFAULT_PRESET_DIR;
3610
4143
  const templateName = typeof options.presetTemplate === "string" && options.presetTemplate.trim().length > 0 ? options.presetTemplate.trim() : "anchored";
3611
4144
  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-]*$/`);
4145
+ const outputId = typeof options.outputId === "string" && options.outputId.trim().length > 0 ? options.outputId.trim() : templateName;
4146
+ 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
4147
  const templateDir = resolvePresetDir(templateName);
3613
4148
  const spec = loadPresetSpec(templateDir);
3614
4149
  const worldBook = spec.worldBook;
@@ -3638,8 +4173,7 @@ function writePreset(prompt, options) {
3638
4173
  ...keys.length > 0 ? { keys } : {},
3639
4174
  ...secondaryKeys.length > 0 ? { secondaryKeys } : {},
3640
4175
  ...entry.caseSensitive === true ? { caseSensitive: true } : {},
3641
- ...entry.wholeWords === true ? { wholeWords: true } : {},
3642
- ...entry.useRegex === true ? { useRegex: true } : {}
4176
+ ...entry.wholeWords === true ? { wholeWords: true } : {}
3643
4177
  }
3644
4178
  });
3645
4179
  }
@@ -3655,27 +4189,37 @@ function writePreset(prompt, options) {
3655
4189
  }
3656
4190
  const runtime = runtimeOf(options, prompt);
3657
4191
  const params = resolvePresetParams(spec, runtime);
3658
- const targetDir = join(presetDir, templateName);
4192
+ const targetDir = join(presetDir, outputId);
3659
4193
  mkdirSync(presetDir, { recursive: true });
3660
- const tmpDir = mkdtempSync(join(presetDir, `.${templateName}.tmp-`));
4194
+ const tmpDir = mkdtempSync(join(presetDir, `.${outputId}.tmp-`));
3661
4195
  const outDir = tmpDir;
3662
4196
  try {
3663
4197
  const overridesSrc = join(targetDir, "prompt-tool.overrides.yml");
3664
4198
  if (existsSync(overridesSrc)) cpSync(overridesSrc, join(outDir, "prompt-tool.overrides.yml"), { force: true });
3665
4199
  const composition = renderComposition(spec, runtime, templateDir);
3666
4200
  assertCompositionArray(composition, spec);
3667
- const subComposition = composition.replaceAll("./engine/", "../.engine/").replaceAll("../prompt-configs", `../${templateName}/prompt-configs`);
4201
+ const subComposition = composition.replaceAll("./engine/", "../.engine/").replaceAll("../prompt-configs", `../${outputId}/prompt-configs`);
3668
4202
  writeFileSync(join(outDir, "agent.cordis.yml"), subComposition, "utf8");
4203
+ const aliasSuffix = "(旧会话兼容)";
4204
+ const aliasName = options.aliasOf === true ? typeof spec.name === "string" && spec.name.length > 0 ? spec.name : outputId : void 0;
4205
+ const displayName = aliasName !== void 0 && !aliasName.endsWith(aliasSuffix) ? aliasName + aliasSuffix : aliasName;
3669
4206
  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;
4207
+ const sourceYamlPath = options.aliasOf === true ? join(presetDir, templateName, "preset.yml") : join(targetDir, "preset.yml");
4208
+ const existingPresetYaml = existsSync(sourceYamlPath) ? readFileSync(sourceYamlPath, "utf8") : void 0;
3671
4209
  if (existingPresetYaml !== void 0 && existingPresetYaml.trim().length > 0) {
3672
4210
  const doc = parseDocument(existingPresetYaml, { logLevel: "silent" });
3673
4211
  doc.setIn(["order"], options.presetOrder);
3674
- if (typeof spec.name === "string" && spec.name.length > 0) doc.setIn(["name"], spec.name);
4212
+ if (typeof spec.name === "string" && spec.name.length > 0) doc.setIn(["name"], displayName ?? spec.name);
4213
+ if (options.aliasOf === true) doc.setIn(["id"], outputId);
3675
4214
  if (typeof spec.description === "string" && spec.description.length > 0) doc.setIn(["description"], spec.description);
3676
4215
  if (Object.keys(meta).length > 0) doc.setIn(["meta"], meta);
3677
4216
  writeFileSync(join(outDir, "preset.yml"), doc.toString(), "utf8");
3678
- } else writeFileSync(join(outDir, "preset.yml"), stringify({
4217
+ } else writeFileSync(join(outDir, "preset.yml"), stringify(displayName !== void 0 ? {
4218
+ ...meta,
4219
+ id: outputId,
4220
+ name: displayName,
4221
+ order: options.presetOrder
4222
+ } : {
3679
4223
  ...meta,
3680
4224
  order: options.presetOrder
3681
4225
  }) + "\n", "utf8");
@@ -3742,7 +4286,7 @@ function writePreset(prompt, options) {
3742
4286
  next.params = {
3743
4287
  ...next.params,
3744
4288
  useCustom: params.firstTurnCustom === true,
3745
- firstTurnText: asString(params.firstTurnText),
4289
+ text: asString(params.firstTurnText),
3746
4290
  buildPattern: asString(params.buildPattern),
3747
4291
  complexPattern: asString(params.complexPattern),
3748
4292
  firstTurnBuild: asString(params.firstTurnBuild),
@@ -3750,14 +4294,15 @@ function writePreset(prompt, options) {
3750
4294
  firstTurnDeep: asString(params.firstTurnDeep)
3751
4295
  };
3752
4296
  } else if (next.id === "router-guide") {
3753
- next.enabled = params.firstTurnAnchor === true;
3754
- const useCustom = params.firstTurnAnchor === true && params.guideCustom === true;
4297
+ const guideEnabled = params.guideEnabled === void 0 ? params.firstTurnAnchor === true : params.guideEnabled === true;
4298
+ next.enabled = guideEnabled;
4299
+ const useCustom = guideEnabled && params.guideCustom === true;
3755
4300
  next.modelScope = useCustom ? "all" : "flash";
3756
4301
  next.params = {
3757
4302
  ...next.params,
3758
4303
  useCustom,
3759
4304
  text: asString(params.guideText),
3760
- guideComplexPattern: asString(params.guideComplexPattern),
4305
+ complexPattern: asString(params.complexPattern) || asString(params.guideComplexPattern),
3761
4306
  guideWeak: asString(params.guideWeak),
3762
4307
  guideDeep: asString(params.guideDeep)
3763
4308
  };
@@ -3769,10 +4314,10 @@ function writePreset(prompt, options) {
3769
4314
  const presetVariables = {};
3770
4315
  for (const [key, value] of Object.entries(spec.params ?? {})) {
3771
4316
  if (PARAM_KEYS.has(key)) continue;
3772
- const text = String(value);
3773
- if (text.length > 0) presetVariables[key] = text;
4317
+ if (value === void 0 || value === null) continue;
4318
+ presetVariables[key] = String(value);
3774
4319
  }
3775
- for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string" && value.length > 0) presetVariables[key] = value;
4320
+ for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string") presetVariables[key] = value;
3776
4321
  const variablesEnabled = spec.variablesEnabled !== false;
3777
4322
  const presetVariableKeys = new Set(Object.keys(presetVariables));
3778
4323
  if (variablesEnabled && presetVariableKeys.size > 0) writeFileSync(join(promptConfigsDir, "variables.yml"), stringify(presetVariables), "utf8");
@@ -3781,15 +4326,30 @@ function writePreset(prompt, options) {
3781
4326
  delete config.text;
3782
4327
  config.texts = [];
3783
4328
  config.enabled = params.injectPrompt !== false && prompt.trim().length > 0;
4329
+ const explicitWord = asString(config.params?.firstTurnWord ?? params.firstTurnWord, "");
4330
+ const signalWord = (text) => {
4331
+ const sentence = /the exact sentence:\s*([A-Za-z]+)/i.exec(text);
4332
+ if (sentence !== null && sentence[1] !== void 0) return sentence[1].toLowerCase();
4333
+ const first = /^\P{L}*([\p{L}]+)/u.exec(text.trim());
4334
+ return first !== null && first[1] !== void 0 ? first[1].toLowerCase() : void 0;
4335
+ };
4336
+ const derivedWords = [...new Set([
4337
+ signalWord(asString(params.firstTurnText)),
4338
+ signalWord(asString(params.firstTurnBuild)),
4339
+ signalWord(asString(params.firstTurnInspect)),
4340
+ signalWord(asString(params.firstTurnDeep))
4341
+ ].filter((word) => word !== void 0))];
4342
+ const anchorWords = explicitWord.length > 0 ? [explicitWord] : derivedWords;
3784
4343
  config.params = {
3785
4344
  ...config.params,
3786
4345
  text: prompt,
3787
- firstTurnWord: asString(config.params?.firstTurnWord ?? params.firstTurnWord, "we")
4346
+ firstTurnWord: explicitWord.length > 0 ? explicitWord : anchorWords[0] ?? "we",
4347
+ anchorWords
3788
4348
  };
3789
4349
  }
3790
4350
  if (config.fill === "instruction-hint") config.params = {
3791
4351
  ...config.params,
3792
- agentsInstructionPath: `../${templateName}/agents-instruction.md`,
4352
+ agentsInstructionPath: `../${outputId}/agents-instruction.md`,
3793
4353
  ...options.injectAgentsPrompt === true ? { text: asString(options.agentsInstructionText) } : {}
3794
4354
  };
3795
4355
  if (!variablesEnabled && presetVariableKeys.size > 0) {
@@ -3803,6 +4363,18 @@ function writePreset(prompt, options) {
3803
4363
  }
3804
4364
  writeFileSync(join(promptConfigsDir, `${String(index * 10).padStart(2, "0")}-${config.id}.yml`), renderPromptConfigYaml(config), "utf8");
3805
4365
  }
4366
+ const customToolsDir = join(outDir, "custom-tools");
4367
+ rmSync(customToolsDir, {
4368
+ recursive: true,
4369
+ force: true
4370
+ });
4371
+ mkdirSync(customToolsDir, { recursive: true });
4372
+ const customTools = Array.isArray(spec.customTools) ? spec.customTools : [];
4373
+ for (const [index, tool] of customTools.entries()) {
4374
+ if (tool === null || typeof tool !== "object" || Array.isArray(tool) || typeof tool.id !== "string" || String(tool.id).length === 0) continue;
4375
+ const toolId = String(tool.id);
4376
+ writeFileSync(join(customToolsDir, `${String(index + 1).padStart(2, "0")}-${toolId}.yml`), stringify(tool, { lineWidth: 0 }), "utf8");
4377
+ }
3806
4378
  for (const legacy of spec.legacyCleanup ?? []) rmSync(join(outDir, legacy), { force: true });
3807
4379
  const agentsInstructionPath = join(outDir, "agents-instruction.md");
3808
4380
  if (typeof options.agentsInstructionText === "string" && options.agentsInstructionText.trim().length > 0) writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
@@ -3855,9 +4427,10 @@ function writePreset(prompt, options) {
3855
4427
  function migrateLegacyLayout(presetRoot, legacyUserPresets) {
3856
4428
  const legacyContainer = join(presetRoot, "prompt-tool");
3857
4429
  const sharedEngine = join(presetRoot, ".engine");
4430
+ const isLegacyContainer = existsSync(legacyContainer) && !existsSync(join(legacyContainer, "preset.yml"));
3858
4431
  let migrated = false;
3859
4432
  const legacyEngine = join(legacyContainer, "engine");
3860
- if (existsSync(legacyEngine) && !existsSync(sharedEngine)) try {
4433
+ if (isLegacyContainer && existsSync(legacyEngine) && !existsSync(sharedEngine)) try {
3861
4434
  mkdirSync(presetRoot, { recursive: true });
3862
4435
  renameSync(legacyEngine, sharedEngine);
3863
4436
  migrated = true;
@@ -3884,7 +4457,7 @@ function migrateLegacyLayout(presetRoot, legacyUserPresets) {
3884
4457
  }
3885
4458
  })();
3886
4459
  const stamp = Date.now().toString(36);
3887
- if (existsSync(sharedEngine) && existsSync(legacyContainer)) try {
4460
+ if (isLegacyContainer && existsSync(sharedEngine) && existsSync(legacyContainer)) try {
3888
4461
  renameSync(legacyContainer, join(presetRoot, `prompt-tool.bak-${stamp}`));
3889
4462
  migrated = true;
3890
4463
  } catch {}
@@ -3900,6 +4473,43 @@ function normalizePresetRootDir(presetDir, presetRoot, legacyContainer) {
3900
4473
  if (presetDir.endsWith("/prompt-tool") || presetDir.endsWith("\\prompt-tool")) return presetRoot;
3901
4474
  return presetDir;
3902
4475
  }
4476
+ /**
4477
+ * 旧版参数覆盖文件(prompt-tool.overrides.yml)→ 激活预设 preset.yml 一次性退役迁移。
4478
+ * 旧写入路径(阶段 1)把引擎参数写进该文件;阶段 2 收敛为 savePresetParams 直接写
4479
+ * preset.yml 并删除覆盖文件。本函数仅处理存量升级场景:
4480
+ * - 键白名单 = PARAM_KEYS(含 guideEnabled/injectPrompt/usePtcMode,补上旧手写列表
4481
+ * 缺失的 3 键;promptConfigs 不走覆盖文件,排除);
4482
+ * - 空值键按 savePresetParams 语义删除(回落模板/引擎默认);
4483
+ * - 成功 = preset.yml 已写入且覆盖文件已删(savePresetParams 删除);失败(preset.yml
4484
+ * 缺失等)抛错且覆盖文件保留,调用方下次重建重试。
4485
+ * 返回 true = 已迁移(调用方需重读参数);false = 无文件/解析失败/无有效键(文件已尽力清理)。
4486
+ */
4487
+ function migrateParamOverridesFile(presetRoot, templateName) {
4488
+ const file = join(presetRoot, templateName, "prompt-tool.overrides.yml");
4489
+ let raw;
4490
+ try {
4491
+ raw = readFileSync(file, "utf8");
4492
+ } catch {
4493
+ return false;
4494
+ }
4495
+ if (raw.trim().length === 0) {
4496
+ rmSync(file, { force: true });
4497
+ return false;
4498
+ }
4499
+ let parsed;
4500
+ try {
4501
+ parsed = parse(raw, { logLevel: "silent" });
4502
+ } catch {
4503
+ return false;
4504
+ }
4505
+ const valid = Object.fromEntries(Object.entries(parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}).filter(([key]) => PARAM_KEYS.has(key) && key !== "promptConfigs"));
4506
+ if (Object.keys(valid).length === 0) {
4507
+ rmSync(file, { force: true });
4508
+ return false;
4509
+ }
4510
+ savePresetParams(presetRoot, templateName, valid, void 0);
4511
+ return true;
4512
+ }
3903
4513
  //#endregion
3904
4514
  //#region src/index.ts
3905
4515
  const name = "prompt-tool";
@@ -3930,6 +4540,7 @@ function warn(ctx, message) {
3930
4540
  } catch {}
3931
4541
  }
3932
4542
  function apply(ctx, configIn) {
4543
+ const hadLegacyContainer = existsSync(join(DEFAULT_PRESET_DIR, "prompt-tool")) && !existsSync(join(DEFAULT_PRESET_DIR, "prompt-tool", "preset.yml"));
3933
4544
  let legacyMigrated = false;
3934
4545
  try {
3935
4546
  legacyMigrated = migrateLegacyLayout(DEFAULT_PRESET_DIR, LEGACY_USER_PRESETS_DIR);
@@ -3972,6 +4583,7 @@ function apply(ctx, configIn) {
3972
4583
  runtime.firstTurnCustom = params.firstTurnCustom === true;
3973
4584
  runtime.guideText = asString(params.guideText);
3974
4585
  runtime.guideCustom = params.guideCustom === true;
4586
+ runtime.guideEnabled = typeof params.guideEnabled === "boolean" ? params.guideEnabled : void 0;
3975
4587
  runtime.modelProvider = asString(params.modelProvider);
3976
4588
  runtime.modelName = asString(params.modelName);
3977
4589
  runtime.subagentModelProvider = asString(params.subagentModelProvider);
@@ -3983,9 +4595,8 @@ function apply(ctx, configIn) {
3983
4595
  runtime.subagentTemperature = asString(params.subagentTemperature);
3984
4596
  runtime.subagentMaxTokens = asString(params.subagentMaxTokens);
3985
4597
  runtime.bootstrapMaxTokens = Number.isSafeInteger(params.bootstrapMaxTokens) && params.bootstrapMaxTokens >= 0 ? params.bootstrapMaxTokens : 0;
3986
- runtime.usePtcMode = params.usePtcMode !== false;
4598
+ runtime.usePtcMode = typeof params.usePtcMode === "boolean" ? params.usePtcMode : void 0;
3987
4599
  runtime.injectPrompt = params.injectPrompt !== false;
3988
- runtime.subagentPersona = asString(params.subagentPersona) || void 0;
3989
4600
  runtime.toolFilterAllow = params.toolFilterAllow;
3990
4601
  runtime.toolFilterDeny = params.toolFilterDeny;
3991
4602
  runtime.maxDepth = params.maxDepth;
@@ -3993,6 +4604,29 @@ function apply(ctx, configIn) {
3993
4604
  runtime.firstTurnWord = asString(params.firstTurnWord) || void 0;
3994
4605
  runtime.promptConfigs = Array.isArray(spec.promptConfigs) ? spec.promptConfigs : [];
3995
4606
  };
4607
+ let legacyAliasHandledThisRun = readPluginState().legacyAliasHandled === true;
4608
+ const ensureLegacyAliasOnce = (presetPrompt, options) => {
4609
+ if (legacyAliasHandledThisRun) return;
4610
+ legacyAliasHandledThisRun = true;
4611
+ const aliasFile = join(runtime.presetDir, "prompt-tool", "preset.yml");
4612
+ try {
4613
+ if (!existsSync(aliasFile) && hadLegacyContainer) {
4614
+ writePreset(presetPrompt, {
4615
+ ...options,
4616
+ outputId: "prompt-tool",
4617
+ aliasOf: true
4618
+ });
4619
+ warn(ctx, "prompt-tool: 已创建一次性旧会话兼容快照 prompt-tool;日常切换不再重建");
4620
+ }
4621
+ writePluginState({
4622
+ ...readPluginState(),
4623
+ legacyAliasHandled: true
4624
+ });
4625
+ } catch (error) {
4626
+ legacyAliasHandledThisRun = false;
4627
+ warn(ctx, `prompt-tool: 创建旧会话兼容快照失败(下次重建重试):${error instanceof Error ? error.message : String(error)}`);
4628
+ }
4629
+ };
3996
4630
  /** 重建生成目录(文本/组合/引擎/提示词配置);writePreset 关闭时移除旧目录。 */
3997
4631
  const rebuildPreset = () => {
3998
4632
  reloadPresetParams();
@@ -4005,6 +4639,7 @@ function apply(ctx, configIn) {
4005
4639
  firstTurnCustom: runtime.firstTurnCustom,
4006
4640
  guideText: runtime.guideText,
4007
4641
  guideCustom: runtime.guideCustom,
4642
+ guideEnabled: runtime.guideEnabled,
4008
4643
  injectPrompt: runtime.injectPrompt,
4009
4644
  modelProvider: runtime.modelProvider,
4010
4645
  modelName: runtime.modelName,
@@ -4016,7 +4651,6 @@ function apply(ctx, configIn) {
4016
4651
  subagentReasoningEffort: runtime.subagentReasoningEffort,
4017
4652
  subagentTemperature: runtime.subagentTemperature,
4018
4653
  subagentMaxTokens: runtime.subagentMaxTokens,
4019
- subagentPersona: runtime.subagentPersona,
4020
4654
  toolFilterAllow: runtime.toolFilterAllow,
4021
4655
  toolFilterDeny: runtime.toolFilterDeny,
4022
4656
  maxDepth: runtime.maxDepth,
@@ -4047,6 +4681,7 @@ function apply(ctx, configIn) {
4047
4681
  }
4048
4682
  }
4049
4683
  writePreset(presetPrompt, options);
4684
+ ensureLegacyAliasOnce(presetPrompt, options);
4050
4685
  } else {
4051
4686
  let cleaned = 0;
4052
4687
  for (const preset of listPresets()) {
@@ -4054,6 +4689,7 @@ function apply(ctx, configIn) {
4054
4689
  for (const name of [
4055
4690
  "agent.cordis.yml",
4056
4691
  "prompt-configs",
4692
+ "custom-tools",
4057
4693
  "preset.md",
4058
4694
  "agents.md",
4059
4695
  "agents-instruction.md",
@@ -4082,49 +4718,15 @@ function apply(ctx, configIn) {
4082
4718
  return true;
4083
4719
  }
4084
4720
  };
4085
- /** 用户参数覆盖(生成目录 prompt-tool.overrides.yml;settings 不再承载参数)。 */
4721
+ /** 旧版参数覆盖文件(prompt-tool.overrides.yml)一次性退役迁移:白名单
4722
+ * (PARAM_KEYS,含 guideEnabled/injectPrompt/usePtcMode)过滤后并入 preset.yml。
4723
+ * 实现收敛到 migrateParamOverridesFile(migration.ts 单一权威)。 */
4086
4724
  const applyParamOverrides = () => {
4087
- const raw = readGeneratedContent(activePresetDir(), "prompt-tool.overrides.yml");
4088
- if (raw.length === 0) return;
4089
- let overrides;
4090
4725
  try {
4091
- const parsed = parse(raw, { logLevel: "silent" });
4092
- overrides = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4726
+ if (migrateParamOverridesFile(runtime.presetDir, runtime.presetTemplate)) reloadPresetParams();
4093
4727
  } catch {
4094
- return;
4728
+ warn(ctx, "prompt-tool: prompt-tool.overrides.yml 迁移到 preset.yml 失败(下次重建重试)");
4095
4729
  }
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
4730
  };
4129
4731
  const legacySkillsDir = typeof config.skillsDir === "string" && config.skillsDir.length > 0 && config.skillsDir !== DEFAULT_SKILLS_DIR ? config.skillsDir : "";
4130
4732
  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 +4840,15 @@ function apply(ctx, configIn) {
4238
4840
  }
4239
4841
  };
4240
4842
  });
4241
- registerSettingsBridge(ctx, NS, getModelsState, () => ({
4843
+ const settingsBridge = registerSettingsBridge(ctx, NS, getModelsState, () => ({
4242
4844
  activeSkillsDirs,
4243
4845
  skillCatalog
4244
4846
  }), () => "", (sctx) => ensureRegistered(sctx), () => {
4245
4847
  skillCatalog = catalogOf(readAllSkillsChecked());
4246
4848
  cachedSkills.invalidate();
4247
4849
  invalidateSkills?.();
4248
- }, () => activePresetDir(), (scope) => {
4249
- if (scope === "preset") current = readGeneratedContent(activePresetDir(), "preset.md");
4850
+ }, () => activePresetDir(), (scopes) => {
4851
+ for (const scope of scopes) if (scope === "preset") current = readGeneratedContent(activePresetDir(), "preset.md");
4250
4852
  else currentAgents = readGeneratedContent(activePresetDir(), "agents.md");
4251
4853
  try {
4252
4854
  rebuildPreset();
@@ -4283,6 +4885,7 @@ function apply(ctx, configIn) {
4283
4885
  firstTurnCustom: initialParams.firstTurnCustom === true,
4284
4886
  guideText: asString(initialParams.guideText),
4285
4887
  guideCustom: initialParams.guideCustom === true,
4888
+ guideEnabled: typeof initialParams.guideEnabled === "boolean" ? initialParams.guideEnabled : void 0,
4286
4889
  injectPrompt: initialParams.injectPrompt !== false,
4287
4890
  modelProvider: asString(initialParams.modelProvider),
4288
4891
  modelName: asString(initialParams.modelName),
@@ -4295,8 +4898,7 @@ function apply(ctx, configIn) {
4295
4898
  subagentTemperature: asString(initialParams.subagentTemperature),
4296
4899
  subagentMaxTokens: asString(initialParams.subagentMaxTokens),
4297
4900
  bootstrapMaxTokens: Number.isSafeInteger(initialParams.bootstrapMaxTokens) && initialParams.bootstrapMaxTokens >= 0 ? initialParams.bootstrapMaxTokens : 0,
4298
- usePtcMode: initialParams.usePtcMode !== false,
4299
- subagentPersona: asString(initialParams.subagentPersona) || void 0,
4901
+ usePtcMode: typeof initialParams.usePtcMode === "boolean" ? initialParams.usePtcMode : void 0,
4300
4902
  toolFilterAllow: initialParams.toolFilterAllow,
4301
4903
  toolFilterDeny: initialParams.toolFilterDeny,
4302
4904
  maxDepth: initialParams.maxDepth,
@@ -4309,9 +4911,21 @@ function apply(ctx, configIn) {
4309
4911
  fallbackText: config.fallbackText,
4310
4912
  promptConfigs: Array.isArray(initialSpec?.promptConfigs) ? initialSpec.promptConfigs : []
4311
4913
  };
4914
+ const agentPresetsNs = settingsNamespace("agent-presets");
4312
4915
  let hostSettingsService;
4313
- let lastSyncedHostDefault;
4314
- const syncHostDefault = (reason) => {
4916
+ const readHostDefault = (value) => {
4917
+ if (value === null || typeof value !== "object") return void 0;
4918
+ const candidate = value.default;
4919
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
4920
+ };
4921
+ const managedPresetExists = (id) => {
4922
+ try {
4923
+ return listPresets().some((preset) => preset.id === id);
4924
+ } catch {
4925
+ return false;
4926
+ }
4927
+ };
4928
+ const syncHostDefault = (_reason) => {
4315
4929
  const s = hostSettingsService;
4316
4930
  if (s === void 0) return;
4317
4931
  const template = runtime.presetTemplate;
@@ -4319,17 +4933,31 @@ function apply(ctx, configIn) {
4319
4933
  warn(ctx, `prompt-tool: 预设 id ${JSON.stringify(template)} 不符合官方 agent-presets 命名(^[a-z0-9][a-z0-9-]*$),跳过宿主 default 同步;请将预设目录改名为合法 id`);
4320
4934
  return;
4321
4935
  }
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) {
4936
+ if (readHostDefault(s.get(agentPresetsNs)) === template) return;
4937
+ s.mutate(agentPresetsNs, [{
4938
+ op: "set",
4939
+ path: ["default"],
4940
+ value: template
4941
+ }]).catch((error) => {
4331
4942
  warn(ctx, `prompt-tool: 同步宿主 agent-presets default 失败:${error instanceof Error ? error.message : String(error)}`);
4943
+ });
4944
+ };
4945
+ const syncTemplateFromHostDefault = (value) => {
4946
+ const s = hostSettingsService;
4947
+ if (s === void 0) return;
4948
+ const template = readHostDefault(value ?? s.get(agentPresetsNs));
4949
+ if (template === void 0 || template === runtime.presetTemplate) return;
4950
+ if (!managedPresetExists(template)) {
4951
+ warn(ctx, `prompt-tool: 官方 agent-presets default 已切换为 ${JSON.stringify(template)},但该预设不在提示词工具管理目录中,跳过反向同步`);
4952
+ return;
4332
4953
  }
4954
+ s.mutate(NS, [{
4955
+ op: "set",
4956
+ path: ["presetTemplate"],
4957
+ value: template
4958
+ }]).catch((error) => {
4959
+ warn(ctx, `prompt-tool: 跟随官方 agent-presets default 失败:${error instanceof Error ? error.message : String(error)}`);
4960
+ });
4333
4961
  };
4334
4962
  installSubagentModelRoute(ctx, () => runtime.subagentModelProvider.length > 0 && runtime.subagentModelName.length > 0, () => runtime.subagentModelProvider, () => runtime.subagentModelName);
4335
4963
  const applyDefaultModel = installDefaultModelRoute(ctx, () => runtime.modelProvider.length > 0 && runtime.modelName.length > 0, () => runtime.modelProvider, () => runtime.modelName, () => runtime.modelReasoningEffort);
@@ -4352,14 +4980,10 @@ function apply(ctx, configIn) {
4352
4980
  presetTemplate: runtime.presetTemplate
4353
4981
  });
4354
4982
  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
- }
4983
+ if (key === "promptConfigs") savePresetParams(runtime.presetDir, runtime.presetTemplate, void 0, Array.isArray(value) ? value : void 0);
4984
+ else savePresetParams(runtime.presetDir, runtime.presetTemplate, { [key]: value }, void 0);
4985
+ reloadPresetParams();
4986
+ rebuildPreset();
4363
4987
  });
4364
4988
  /**
4365
4989
  * 物化导入的预设:用其自身 preset.yml(spec 参数 + modules + promptConfigs)渲染
@@ -4381,7 +5005,6 @@ function apply(ctx, configIn) {
4381
5005
  modelName: "",
4382
5006
  subagentModelProvider: "",
4383
5007
  subagentModelName: "",
4384
- subagentPersona: "",
4385
5008
  injectAgentsPrompt: false,
4386
5009
  bootstrapMaxTokens: 0,
4387
5010
  usePtcMode: false,
@@ -4454,37 +5077,50 @@ function apply(ctx, configIn) {
4454
5077
  if (!residentAgentsWritten) warn(ctx, `prompt-tool: failed to remove resident rules block from ${runtime.residentAgentsPath}`);
4455
5078
  }
4456
5079
  rebuildPreset();
4457
- if (presetTemplateChanged) syncHostDefault("switch");
5080
+ if (presetTemplateChanged) {
5081
+ syncHostDefault("switch");
5082
+ applyBuiltinTools();
5083
+ }
4458
5084
  };
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)}`);
5085
+ let disposeBuiltinTools = [];
5086
+ const applyBuiltinTools = () => {
5087
+ for (const dispose of disposeBuiltinTools) dispose();
5088
+ disposeBuiltinTools = [];
5089
+ const push = (dispose) => {
5090
+ if (typeof dispose === "function") disposeBuiltinTools.push(dispose);
5091
+ };
5092
+ push(registerCharacterTools(ctx, {
5093
+ presetRoot: () => dirname(activePresetDir()),
5094
+ templateName: () => basename(activePresetDir()),
5095
+ rebuild: () => {
5096
+ try {
5097
+ rebuildPreset();
5098
+ } catch (error) {
5099
+ warn(ctx, `prompt-tool: character tool rebuild failed: ${String(error)}`);
5100
+ }
4467
5101
  }
4468
- }
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)}`);
5102
+ }));
5103
+ push(registerWorldBookTools(ctx, {
5104
+ activeDir: () => activePresetDir(),
5105
+ presetRoot: () => dirname(activePresetDir()),
5106
+ rebuild: () => {
5107
+ try {
5108
+ rebuildPreset();
5109
+ } catch (error) {
5110
+ warn(ctx, `prompt-tool: world-book tool rebuild failed: ${String(error)}`);
5111
+ }
4478
5112
  }
4479
- }
4480
- });
4481
- registerSessionVarTools(ctx);
5113
+ }));
5114
+ push(registerSessionVarTools(ctx));
5115
+ };
5116
+ applyBuiltinTools();
4482
5117
  const settingsEntry = currentSource();
4483
5118
  const ensureRegistered = (sctx) => ensureSettingsRegistered(sctx, NS, PromptSettingsSchema, {
4484
5119
  base: () => settingsEntry,
4485
5120
  onRegistered: (scope) => {
4486
5121
  currentSource = () => scope.get();
4487
5122
  scope.watch(() => {
5123
+ settingsBridge.invalidateDescriptor();
4488
5124
  try {
4489
5125
  applyState();
4490
5126
  } catch (error) {
@@ -4560,10 +5196,18 @@ function apply(ctx, configIn) {
4560
5196
  warn(ctx, `prompt-tool: settings 参数迁移失败(下次启动重试):${error instanceof Error ? error.message : String(error)}`);
4561
5197
  }
4562
5198
  };
5199
+ ctx.inject(["settings", "agentPresets"], () => {
5200
+ syncTemplateFromHostDefault();
5201
+ });
4563
5202
  ctx.inject(["settings"], (sctx) => {
4564
5203
  hostSettingsService = sctx.settings;
5204
+ sctx.effect(() => sctx.on("settings/updated", (ns, next) => {
5205
+ if (String(ns) !== String(agentPresetsNs)) return;
5206
+ syncTemplateFromHostDefault(next);
5207
+ }), "prompt-tool: follow agent-presets default");
4565
5208
  ensureRegistered(sctx);
4566
5209
  if (legacyMigrated) syncHostDefault("migrate");
5210
+ else syncTemplateFromHostDefault();
4567
5211
  migrateSettingsParamsToPreset(sctx);
4568
5212
  sctx.effect(() => () => {
4569
5213
  currentSource = () => settingsEntry;
@@ -4576,4 +5220,4 @@ function apply(ctx, configIn) {
4576
5220
  });
4577
5221
  }
4578
5222
  //#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 };
5223
+ 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, ensureSettingsRegistered, 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 };