dsh-plugin-prompt-tool 0.5.0 → 0.6.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 (37) hide show
  1. package/README.md +18 -12
  2. package/engine/anchor-match.mjs +117 -0
  3. package/engine/compositions/library/context-gate.yml +2 -2
  4. package/engine/compositions/library/delegation.yml +2 -2
  5. package/engine/compositions/library/str-replace-editor.yml +1 -1
  6. package/engine/compositions/library/tool-bootstrap.yml +2 -2
  7. package/engine/compositions/library/tool-filter.yml +3 -3
  8. package/engine/compositions/source/local/context-gate.yml +2 -2
  9. package/engine/compositions/source/local/tool-bootstrap.yml +3 -3
  10. package/engine/executor.mjs +9 -2
  11. package/engine/interpolate.mjs +66 -0
  12. package/engine/layers.mjs +26 -22
  13. package/engine/schema.mjs +18 -0
  14. package/engine/session-vars.mjs +52 -0
  15. package/engine/shared.mjs +0 -19
  16. package/engine/strategies.mjs +28 -38
  17. package/lib/client.js +471 -162
  18. package/lib/client.js.map +1 -1
  19. package/lib/index.d.mts +35 -9
  20. package/lib/index.mjs +376 -92
  21. package/lib/preset-core.d.mts +2 -2
  22. package/lib/preset-core.mjs +3 -6
  23. package/lib/{prompt-configs-BcP6miu9.d.mts → prompt-configs-B4vH09wx.d.mts} +1 -3
  24. package/lib/{prompt-configs-k7wuZs0C.mjs → prompt-configs-ThS4iXPg.mjs} +85 -33
  25. package/package.json +105 -104
  26. package/preset/anchored/preset.yml +40 -42
  27. package/preset/creative/preset.yml +33 -30
  28. package/preset/creative/skills/cordis-plugin-development/SKILL.md +420 -0
  29. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +165 -0
  30. package/preset/liangshen/preset.yml +24 -18
  31. package/preset/minimal/preset.yml +27 -24
  32. package/preset/ptc/preset.yml +23 -17
  33. package/preset/standard/preset.yml +23 -17
  34. package/templates/20-system-section.yml +5 -0
  35. package/engine/compositions/library/router-first-turn.yml +0 -9
  36. package/engine/compositions/source/local/router-first-turn.yml +0 -5
  37. package/engine/router-first-turn.mjs +0 -85
package/lib/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
- import { C as DEFAULT_PRESET_DIR, D as LEGACY_USER_PRESETS_DIR, E as LEGACY_CONTAINER_DIR, S as userPresetsDir, T as DEFAULT_SKILLS_DIR, _ as removeUserPreset, a as asString, b as resolvePresetParams, c as duplicateUserPreset, d as listPresets, f as loadPresetContent, g as parseImportedPresetId, h as packageEngineDir, i as applyModuleConfigs, 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 renderComposition, w as DEFAULT_RESIDENT_AGENTS_PATH, x as savePresetParams, y as resolvePresetDir } from "./prompt-configs-k7wuZs0C.mjs";
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-ThS4iXPg.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
4
  import { parse, parseDocument, stringify } from "yaml";
5
5
  import { basename, dirname, join, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import z from "@deepseek-ai/schemastery";
8
- import { randomBytes } from "node:crypto";
8
+ import { createHash, randomBytes } from "node:crypto";
9
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
10
10
  //#region src/runtime/skills-watcher.ts
11
11
  /** 技能目录文件 watcher(多目录):任一目录变化防抖 300ms 后触发 onRefresh;单目录不可 watch 时跳过。 */
@@ -551,13 +551,16 @@ function installSubagentModelRoute(ctx, isEnabled, provider, model) {
551
551
  });
552
552
  }
553
553
  //#endregion
554
- //#region src/config.ts
555
- /** 插件配置、settings 数据模型与默认常量(settings 接口层)。 */
556
- const NS = settingsNamespace("prompt-tool");
554
+ //#region src/shared/param-keys.ts
557
555
  /**
558
556
  * 引擎行为参数键(按预设存储:激活预设 preset.yml 的 params + promptConfigs)。
559
557
  * 不进 Config schema、不进 settings namespace——每预设一份,随预设走(官方范式:
560
558
  * Config = 部署轴,引擎行为在预设文件)。
559
+ *
560
+ * 这些键在 UI 有专门编辑入口(模型设置 / 工具与深度 / 开关),writePreset 把
561
+ * 预设级 params 合并进每条 promptConfig 的 params 时排除——「params(高级参数
562
+ * JSON)」框只保留内容变量与配置自身策略参数,避免 UI 已管理的参数冗余回写。
563
+ * 放 shared:host(write-preset)与 config/settings-bridge 共用,单一来源。
561
564
  */
562
565
  const PARAM_KEYS = /* @__PURE__ */ new Set([
563
566
  "firstTurnAnchor",
@@ -575,7 +578,6 @@ const PARAM_KEYS = /* @__PURE__ */ new Set([
575
578
  "subagentReasoningEffort",
576
579
  "subagentTemperature",
577
580
  "subagentMaxTokens",
578
- "mainPersona",
579
581
  "subagentPersona",
580
582
  "toolFilterAllow",
581
583
  "toolFilterDeny",
@@ -587,6 +589,10 @@ const PARAM_KEYS = /* @__PURE__ */ new Set([
587
589
  "injectPrompt",
588
590
  "promptConfigs"
589
591
  ]);
592
+ //#endregion
593
+ //#region src/config.ts
594
+ /** 插件配置、settings 数据模型与默认常量(settings 接口层)。 */
595
+ const NS = settingsNamespace("prompt-tool");
590
596
  const Config = z.object({
591
597
  injectAgentsPrompt: z.boolean().default(false),
592
598
  writeAgents: z.boolean().default(true),
@@ -845,26 +851,42 @@ function fixSkillEntry(skillsDir, folder) {
845
851
  }
846
852
  //#endregion
847
853
  //#region src/host/sillytavern.ts
854
+ /**
855
+ * SillyTavern 预设 → 本项目单文件预设转换引擎(纯函数,无文件 IO)。
856
+ *
857
+ * 按需转换原则:只按注入层级映射 SillyTavern 实际内容,不注入本项目默认内容。
858
+ * - prompts[] → promptConfigs:system_prompt+role=system → system-section(可拼接);
859
+ * 其余 → pre-step(role/position/dedupe 按 ST 语义);
860
+ * - 角色卡正文(chara_card_v3:data 内层;旧版顶层直存)→ promptConfigs:
861
+ * description/personality/scenario 拼接为「角色设定」system-section,
862
+ * system_prompt / post_history_instructions → system-section,
863
+ * first_mes → pre-step 开场白(dedupe=session 每会话一次);
864
+ * - 采样参数(temperature/openai_max_tokens/reasoning_effort)**不转译**:
865
+ * 模型参数由「模型设置」UI 统一管理(预设级 params.model*,writePreset 渲染
866
+ * agent-request patch),ST 卡固化值会覆盖用户在模型设置里的设置,故剥离;
867
+ * - modules 按需组装:prompt-config-engine 始终,system-section 注入需要
868
+ * persona(complete: false 允许 system-section 生效)。
869
+ */
848
870
  /** ST 运行时指令(渲染时执行、不发送给模型):setvar/getvar/ERA/trim/注释 → 剥离。 */
849
871
  const ST_DIRECTIVE = /\{\{(setvar|getvar|ERA|trim|\/\/)[^}]*\}\}/gi;
850
872
  /**
851
873
  * 处理 SillyTavern 文本,ST 变量语义 → 本项目 params fallback 插值:
852
874
  * {{setvar::k::v}} → 收集 k=v 进 params(会话变量初始值 = fallback 基准),指令剥离;
853
875
  * {{getvar::k}} → 改写为 {{k}}(引擎按 params 插值,无值保留原样);
854
- * {{getvar::k::default}} → 改写为 {{k}} 且 params 缺 k 时写入 default(fallback 落 params);
876
+ * {{getvar::k::default}} → 改写为 {{k}} 且 variables 缺 k 时写入 default(fallback 落顶层 variables);
855
877
  * {{trim}}/{{//注释}}/{{ERA:...}} → 剥离(格式化/注释/第三方运行时);
856
878
  * {{user}}/{{char}} → 替换占位符。
857
879
  */
858
- function processStText(text, cardName, params) {
859
- let cleaned = text.replace(/\{\{getvar::([A-Za-z0-9_.\u4e00-\u9fff-]+)::([^}]*)\}\}/g, (_whole, key, fallback) => {
860
- if (!(key in params) && fallback.length > 0) params[key] = fallback;
861
- return `{{${key}}}`;
862
- });
863
- cleaned = cleaned.replace(/\{\{getvar::([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (_whole, key) => `{{${key}}}`);
864
- cleaned = cleaned.replace(/\{\{setvar::([A-Za-z0-9_.\u4e00-\u9fff-]+)::([^}]*)\}\}/g, (_whole, key, value) => {
865
- params[key] = value;
880
+ function processStText(text, cardName, variables) {
881
+ let cleaned = text.replace(/\{\{setvar::([A-Za-z0-9_.\u4e00-\u9fff-]+)::([^}]*)\}\}/g, (_whole, key, value) => {
882
+ variables[key] = value;
866
883
  return "";
867
884
  });
885
+ cleaned = cleaned.replace(/\{\{getvar::([A-Za-z0-9_.\u4e00-\u9fff-]+)::([^}]*)\}\}/g, (_whole, key, fallback) => {
886
+ if (!Object.prototype.hasOwnProperty.call(variables, key) && fallback.length > 0) variables[key] = fallback;
887
+ return `{{${key}}}`;
888
+ });
889
+ cleaned = cleaned.replace(/\{\{getvar::([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (_whole, key) => Object.prototype.hasOwnProperty.call(variables, key) ? `{{${key}}}` : "");
868
890
  cleaned = cleaned.replace(ST_DIRECTIVE, "");
869
891
  cleaned = cleaned.replace(/\{\{char\}\}/gi, cardName.trim().length > 0 ? cardName.trim() : "角色").replace(/\{\{user\}\}/gi, "用户").replace(/\n{3,}/g, "\n\n").trim();
870
892
  return cleaned;
@@ -877,6 +899,7 @@ function mergeStPresets(specs) {
877
899
  if (config === null || typeof config !== "object" || Array.isArray(config)) continue;
878
900
  const entry = config;
879
901
  const base = String(entry.id ?? "");
902
+ if (base.length === 0) continue;
880
903
  let id = base;
881
904
  for (let suffix = 2; seen.has(id); suffix++) id = `${base}-${suffix}`;
882
905
  seen.add(id);
@@ -933,8 +956,8 @@ function convertStToPreset(card, baseName) {
933
956
  }
934
957
  const bodyText = (key) => typeof body[key] === "string" ? body[key].trim() : "";
935
958
  const cardName = typeof record.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof body.name === "string" && body.name.trim().length > 0 ? body.name.trim() : "";
936
- const params = {};
937
- const clean = (text) => processStText(text, cardName, params);
959
+ const variables = {};
960
+ const clean = (text) => processStText(text, cardName, variables);
938
961
  const characterDefinitionClean = clean([
939
962
  bodyText("description"),
940
963
  bodyText("personality"),
@@ -979,32 +1002,40 @@ function convertStToPreset(card, baseName) {
979
1002
  systemSectionCount += 1;
980
1003
  }
981
1004
  const book = body.character_book;
982
- if (book !== null && typeof book === "object" && !Array.isArray(book) && Array.isArray(book.entries)) for (const [index, entry] of book.entries.entries()) {
983
- if (entry === null || typeof entry !== "object") continue;
984
- const content = clean(typeof entry.content === "string" ? entry.content : "");
985
- if (content.length === 0) continue;
986
- const comment = typeof entry.comment === "string" && entry.comment.trim().length > 0 ? entry.comment.trim() : `世界书 ${String(entry.id ?? index)}`;
987
- const keys = Array.isArray(entry.keys) ? entry.keys.map(String).filter((key) => key.trim().length > 0) : [];
988
- const secondaryKeys = Array.isArray(entry.secondary_keys) ? entry.secondary_keys.map(String).filter((key) => key.trim().length > 0) : [];
989
- const constant = entry.constant === true;
990
- configs.push({
991
- id: `lore-${String(entry.id ?? index)}`,
992
- name: comment,
993
- enabled: entry.enabled !== false,
994
- strategy: "world-book",
995
- order: typeof entry.insertion_order === "number" ? entry.insertion_order : 100,
996
- text: content,
997
- layer: "pre-step",
998
- position: "before-all",
999
- params: {
1000
- constant,
1001
- ...keys.length > 0 ? { keys } : {},
1002
- ...secondaryKeys.length > 0 ? { secondaryKeys } : {},
1003
- ...entry.case_sensitive === true ? { caseSensitive: true } : {},
1004
- ...entry.match_whole_words === true ? { wholeWords: true } : {},
1005
- ...entry.use_regex === true ? { useRegex: true } : {}
1006
- }
1007
- });
1005
+ if (book !== null && typeof book === "object" && !Array.isArray(book)) {
1006
+ const rawEntries = book.entries;
1007
+ const entryList = Array.isArray(rawEntries) ? rawEntries : rawEntries !== null && typeof rawEntries === "object" ? Object.values(rawEntries).filter((e) => e !== null && typeof e === "object" && !Array.isArray(e)) : [];
1008
+ for (const [index, entry] of entryList.entries()) {
1009
+ if (entry === null || typeof entry !== "object") continue;
1010
+ const content = clean(typeof entry.content === "string" ? entry.content : "");
1011
+ if (content.length === 0) continue;
1012
+ const comment = typeof entry.comment === "string" && entry.comment.trim().length > 0 ? entry.comment.trim() : `世界书 ${String(entry.id ?? entry.uid ?? index)}`;
1013
+ const rawKeys = entry.keys ?? entry.key;
1014
+ const keys = Array.isArray(rawKeys) ? rawKeys.map(String).filter((key) => key.trim().length > 0) : [];
1015
+ const rawSecondary = entry.secondary_keys ?? entry.keysecondary;
1016
+ const secondaryKeys = Array.isArray(rawSecondary) ? rawSecondary.map(String).filter((key) => key.trim().length > 0) : [];
1017
+ const constant = entry.constant === true || entry.add_always === true;
1018
+ const enabled = entry.disable !== void 0 ? entry.disable !== true : entry.enabled !== false;
1019
+ configs.push({
1020
+ id: `lore-${String(entry.id ?? entry.uid ?? index)}`,
1021
+ name: comment,
1022
+ enabled,
1023
+ strategy: "world-book",
1024
+ order: typeof entry.insertion_order === "number" ? entry.insertion_order : typeof entry.order === "number" ? entry.order : 100,
1025
+ 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
+ });
1038
+ }
1008
1039
  }
1009
1040
  const firstMes = clean(bodyText("first_mes"));
1010
1041
  if (firstMes.length > 0) configs.push({
@@ -1042,9 +1073,9 @@ function convertStToPreset(card, baseName) {
1042
1073
  const base = {
1043
1074
  id,
1044
1075
  name: typeof prompt.name === "string" && prompt.name.length > 0 ? prompt.name : id,
1045
- enabled: orderDisabled.has(id) ? false : prompt.enabled !== false,
1076
+ enabled: orderDisabled.has(rawId) ? false : prompt.enabled !== false,
1046
1077
  strategy: "static",
1047
- order: (orderIndex.get(id) ?? index) * 10,
1078
+ order: ((rawId.length > 0 ? orderIndex.get(rawId) : void 0) ?? index) * 10,
1048
1079
  text: content
1049
1080
  };
1050
1081
  if (role === "system") {
@@ -1063,9 +1094,6 @@ function convertStToPreset(card, baseName) {
1063
1094
  dedupe: "none"
1064
1095
  });
1065
1096
  }
1066
- if (typeof record.temperature === "number") params.modelTemperature = String(record.temperature);
1067
- if (typeof record.openai_max_tokens === "number" && record.openai_max_tokens > 0) params.modelMaxTokens = String(record.openai_max_tokens);
1068
- if (typeof record.reasoning_effort === "string" && record.reasoning_effort.trim().length > 0) params.modelReasoningEffort = record.reasoning_effort.trim();
1069
1097
  const modules = ["prompt-config-engine"];
1070
1098
  const moduleConfigs = {};
1071
1099
  if (systemSectionCount > 0) {
@@ -1082,18 +1110,57 @@ function convertStToPreset(card, baseName) {
1082
1110
  deny: ["web_search", "web_fetch"]
1083
1111
  };
1084
1112
  }
1113
+ const presetId = stPresetId(baseName);
1114
+ const RUNTIME_MACROS = /* @__PURE__ */ new Set([
1115
+ "lastusermessage",
1116
+ "lastcharmessage",
1117
+ "charifnotgroup"
1118
+ ]);
1119
+ const BUILTIN_KEYS = /* @__PURE__ */ new Set([
1120
+ "DSH_HOME",
1121
+ "WORKSPACE",
1122
+ "CWD"
1123
+ ]);
1124
+ const MACRO_RE = /\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g;
1125
+ const knownKeys = new Set(Object.keys(variables).map((key) => key.toLowerCase()));
1126
+ for (const config of configs) {
1127
+ const configRecord = config;
1128
+ const texts = [
1129
+ ...typeof config.text === "string" && config.text.length > 0 ? [config.text] : [],
1130
+ ...Array.isArray(config.texts) ? config.texts : [],
1131
+ ...typeof configRecord.params?.text === "string" ? [configRecord.params.text] : []
1132
+ ];
1133
+ for (const raw of texts) {
1134
+ const text = String(raw);
1135
+ MACRO_RE.lastIndex = 0;
1136
+ for (const match of text.matchAll(MACRO_RE)) {
1137
+ const key = match[1];
1138
+ const lower = key.toLowerCase();
1139
+ if (knownKeys.has(lower) || RUNTIME_MACROS.has(lower) || BUILTIN_KEYS.has(key)) continue;
1140
+ knownKeys.add(lower);
1141
+ variables[key] = "";
1142
+ }
1143
+ }
1144
+ }
1085
1145
  return {
1086
- id: baseName.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "") || "sillytavern",
1146
+ id: presetId,
1087
1147
  name: `${cardName || baseName}(SillyTavern 转换)`,
1088
1148
  version: "1.0.0",
1089
1149
  engineCompat: ">=0.4.2",
1090
1150
  meta: { source: "sillytavern" },
1091
- ...Object.keys(params).length > 0 ? { params } : {},
1151
+ ...Object.keys(variables).length > 0 ? { variables } : {},
1092
1152
  modules,
1093
1153
  moduleConfigs,
1094
1154
  promptConfigs: configs
1095
1155
  };
1096
1156
  }
1157
+ /** ST 导入预设 id:必须是官方 agent-presets 可发现的目录名(PRESET_ID =
1158
+ * /^[a-z0-9][a-z0-9-]*$/——含中文的目录会被宿主 discovery 静默跳过,会话
1159
+ * resume 报 preset not found)。文件名 slug 化(去中文);纯中文名退化为
1160
+ * st-<文件名短哈希>(唯一且合法);显示名 name 仍保留中文原名。 */
1161
+ function stPresetId(baseName) {
1162
+ return baseName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || `st-${createHash("sha1").update(baseName).digest("hex").slice(0, 6)}`;
1163
+ }
1097
1164
  //#endregion
1098
1165
  //#region src/host/characters.ts
1099
1166
  /** 角色卡库:独立于预设根的素材+参数存储(预设根下点前缀目录,官方 discovery 跳过)。
@@ -1326,7 +1393,7 @@ function removeCharacterFromPreset(presetRoot, templateName, cardId) {
1326
1393
  return !isCard;
1327
1394
  });
1328
1395
  doc.setIn(["promptConfigs"], kept);
1329
- for (const key of Object.keys(spec?.params ?? {})) doc.deleteIn(["params", key]);
1396
+ for (const [key, value] of Object.entries(spec?.params ?? {})) if (doc.getIn(["params", key]) === value) doc.deleteIn(["params", key]);
1330
1397
  const list = Array.isArray(current.meta?.importedCharacters) ? current.meta.importedCharacters : [];
1331
1398
  doc.setIn(["meta", "importedCharacters"], list.filter((entry) => String(entry) !== cardId));
1332
1399
  });
@@ -1363,6 +1430,7 @@ const BRIDGE_ENDPOINTS = {
1363
1430
  presetContent: "/preset-content",
1364
1431
  importPreset: "/import-preset",
1365
1432
  paramOverrides: "/param-overrides",
1433
+ presetVariables: "/preset-variables",
1366
1434
  importPresetPackage: "/import-preset-package",
1367
1435
  exportPreset: "/export-preset",
1368
1436
  presetDelete: "/preset-delete",
@@ -1898,6 +1966,64 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
1898
1966
  }
1899
1967
  }
1900
1968
  }),
1969
+ sctx.webServer.register({
1970
+ kind: "exact",
1971
+ path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.presetVariables,
1972
+ handler: async (req, res) => {
1973
+ if (!guard(req, res)) return;
1974
+ const dir = getPresetConfigsDir?.() ?? "";
1975
+ if (dir.length === 0) {
1976
+ writeBridgeJson(res, 400, {
1977
+ ok: false,
1978
+ code: "preset-dir-unavailable",
1979
+ message: "presetDir 未配置"
1980
+ });
1981
+ return;
1982
+ }
1983
+ const presetRoot = dirname(dir);
1984
+ const templateName = basename(dir);
1985
+ const { body } = await readBridgeBody(req);
1986
+ const record = body ?? {};
1987
+ if (record.variables === void 0 && record.enabled === void 0) {
1988
+ try {
1989
+ 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
+ writeBridgeJson(res, 200, {
1994
+ ok: true,
1995
+ value: {
1996
+ variables,
1997
+ enabled: spec.variablesEnabled !== false
1998
+ }
1999
+ });
2000
+ } catch {
2001
+ writeBridgeJson(res, 200, {
2002
+ ok: true,
2003
+ value: {
2004
+ variables: {},
2005
+ enabled: true
2006
+ }
2007
+ });
2008
+ }
2009
+ return;
2010
+ }
2011
+ 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);
2013
+ afterOverridesChange?.();
2014
+ writeBridgeJson(res, 200, {
2015
+ ok: true,
2016
+ value: { variables: record.variables }
2017
+ });
2018
+ } catch (error) {
2019
+ writeBridgeJson(res, 409, {
2020
+ ok: false,
2021
+ code: "preset-variables-rejected",
2022
+ message: `模板变量保存失败:${error instanceof Error ? error.message : String(error)}`
2023
+ });
2024
+ }
2025
+ }
2026
+ }),
1901
2027
  sctx.webServer.register({
1902
2028
  kind: "exact",
1903
2029
  path: SETTINGS_BRIDGE_PREFIX + BRIDGE_ENDPOINTS.importPresetPackage,
@@ -2270,7 +2396,8 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2270
2396
  const path = typeof f.path === "string" && f.path.length > 0 ? f.path : "";
2271
2397
  const content = typeof f.content === "string" ? f.content : "";
2272
2398
  if (path.length === 0) return [];
2273
- if (path.includes("..") || /^[a-zA-Z]:/.test(path) || path.startsWith("/") || path.startsWith("\\")) return [];
2399
+ if (/^[a-zA-Z]:/.test(path) || path.startsWith("/") || path.startsWith("\\")) return [];
2400
+ if (path.split(/[\\/]/).some((segment) => segment === "..")) return [];
2274
2401
  return [{
2275
2402
  path,
2276
2403
  content
@@ -2459,7 +2586,7 @@ function registerSettingsBridge(ctx, ns, getModelsState, getSkillsState, getEngi
2459
2586
  }
2460
2587
  //#endregion
2461
2588
  //#region src/runtime/character-tools.ts
2462
- const text$1 = (text) => [{
2589
+ const text$2 = (text) => [{
2463
2590
  type: "text",
2464
2591
  text
2465
2592
  }];
@@ -2497,7 +2624,7 @@ function registerCharacterTools(ctx, host) {
2497
2624
  }
2498
2625
  } }
2499
2626
  },
2500
- render: (_args, value) => text$1(JSON.stringify(value.characters))
2627
+ render: (_args, value) => text$2(JSON.stringify(value.characters))
2501
2628
  },
2502
2629
  execute: async () => ({ characters: listCharacterCards(host.presetRoot(), host.templateName()) })
2503
2630
  }));
@@ -2531,7 +2658,7 @@ function registerCharacterTools(ctx, host) {
2531
2658
  }
2532
2659
  }
2533
2660
  },
2534
- render: (_args, value) => text$1(`角色卡已入库:${value.name}(id=${value.id})。调用 character_apply 可应用到当前预设。`)
2661
+ render: (_args, value) => text$2(`角色卡已入库:${value.name}(id=${value.id})。调用 character_apply 可应用到当前预设。`)
2535
2662
  },
2536
2663
  execute: async (args) => {
2537
2664
  const result = importCharacterCard(host.presetRoot(), [{
@@ -2568,7 +2695,7 @@ function registerCharacterTools(ctx, host) {
2568
2695
  }
2569
2696
  }
2570
2697
  },
2571
- render: (_args, value) => text$1(`已导入到当前预设(${value.count} 条配置),生成目录已重建。`)
2698
+ render: (_args, value) => text$2(`已导入到当前预设(${value.count} 条配置),生成目录已重建。`)
2572
2699
  },
2573
2700
  execute: async (args) => {
2574
2701
  const result = applyCharacterToPreset(host.presetRoot(), host.templateName(), args.id);
@@ -2603,7 +2730,7 @@ function registerCharacterTools(ctx, host) {
2603
2730
  }
2604
2731
  }
2605
2732
  },
2606
- render: (_args, value) => text$1(`已从当前预设移除(${value.count} 条配置),生成目录已重建。`)
2733
+ render: (_args, value) => text$2(`已从当前预设移除(${value.count} 条配置),生成目录已重建。`)
2607
2734
  },
2608
2735
  execute: async (args) => {
2609
2736
  const result = removeCharacterFromPreset(host.presetRoot(), host.templateName(), args.id);
@@ -2632,7 +2759,7 @@ function registerCharacterTools(ctx, host) {
2632
2759
  required: true
2633
2760
  } }
2634
2761
  },
2635
- render: (_args, value) => text$1(`角色卡 ${value.id} 已从库中删除。`)
2762
+ render: (_args, value) => text$2(`角色卡 ${value.id} 已从库中删除。`)
2636
2763
  },
2637
2764
  execute: async (args) => {
2638
2765
  const result = deleteCharacterCard(host.presetRoot(), args.id);
@@ -2644,7 +2771,7 @@ function registerCharacterTools(ctx, host) {
2644
2771
  }
2645
2772
  //#endregion
2646
2773
  //#region src/runtime/world-book-tools.ts
2647
- const text = (text) => [{
2774
+ const text$1 = (text) => [{
2648
2775
  type: "text",
2649
2776
  text
2650
2777
  }];
@@ -2723,7 +2850,7 @@ function registerWorldBookTools(ctx, host) {
2723
2850
  }
2724
2851
  } }
2725
2852
  },
2726
- render: (_args, value) => text(JSON.stringify(value.entries))
2853
+ render: (_args, value) => text$1(JSON.stringify(value.entries))
2727
2854
  },
2728
2855
  execute: async () => {
2729
2856
  return { entries: readConfigs(host.activeDir()).filter((config) => config.strategy === "world-book").map((config) => ({
@@ -2737,7 +2864,7 @@ function registerWorldBookTools(ctx, host) {
2737
2864
  }));
2738
2865
  toolsCtx.tools.register(defineTool({
2739
2866
  name: "world_book_upsert",
2740
- description: "新增或更新当前预设的一条世界书条目(world-book 策略配置):按 id 更新(不存在则新增,id 自动生成 lore-<n>)。constant=true 常驻注入;否则命中 keys(或 secondaryKeys)任一关键字注入;无 keys 条目按全局每次注入。note 可选:写入来源角色卡的本地记忆(memory.md,条目 id 带 chara-<卡>- 前缀时)或预设记忆。写盘后立即重建生成目录。",
2867
+ description: "新增或更新当前预设的一条世界书条目(world-book 策略配置):按 id 更新(不存在则新增,id 自动生成 lore-<n>)。constant=true 常驻注入;否则命中 keys(或 secondaryKeys)任一关键字注入;无 keys 条目按全局每次注入。note 可选:写入来源角色卡的持久记忆(memory.md,条目 id 带 chara-<卡>- 前缀时)或预设记忆——持久记忆跨会话跟随角色卡(与 session_var 会话变量的临时状态不同,适合长期关系记录)。写盘后立即重建生成目录。",
2741
2868
  parameters: {
2742
2869
  id: {
2743
2870
  type: "string",
@@ -2777,7 +2904,7 @@ function registerWorldBookTools(ctx, host) {
2777
2904
  },
2778
2905
  note: {
2779
2906
  type: "string",
2780
- description: "可选:操作笔记,写入来源角色卡记忆或预设记忆。"
2907
+ description: "可选:操作笔记,写入来源角色卡持久记忆(memory.md,跨会话跟随角色卡)或预设记忆。"
2781
2908
  }
2782
2909
  },
2783
2910
  output: {
@@ -2795,7 +2922,7 @@ function registerWorldBookTools(ctx, host) {
2795
2922
  }
2796
2923
  }
2797
2924
  },
2798
- render: (_args, value) => text(`世界书条目 ${value.id} 已保存(当前共 ${value.count} 条),生成目录已重建。`)
2925
+ render: (_args, value) => text$1(`世界书条目 ${value.id} 已保存(当前共 ${value.count} 条),生成目录已重建。`)
2799
2926
  },
2800
2927
  execute: async (args) => {
2801
2928
  const dir = host.activeDir();
@@ -2831,7 +2958,7 @@ function registerWorldBookTools(ctx, host) {
2831
2958
  }));
2832
2959
  toolsCtx.tools.register(defineTool({
2833
2960
  name: "world_book_delete",
2834
- description: "删除当前预设的一条世界书条目(world_book_list 获取 id)。note 可选:写入来源角色卡记忆或预设记忆。删除后立即重建生成目录。",
2961
+ description: "删除当前预设的一条世界书条目(world_book_list 获取 id)。note 可选:写入来源角色卡持久记忆(memory.md,跨会话跟随角色卡)或预设记忆。删除后立即重建生成目录。",
2835
2962
  parameters: {
2836
2963
  id: {
2837
2964
  type: "string",
@@ -2840,7 +2967,7 @@ function registerWorldBookTools(ctx, host) {
2840
2967
  },
2841
2968
  note: {
2842
2969
  type: "string",
2843
- description: "可选:操作笔记。"
2970
+ description: "可选:操作笔记,写入来源角色卡持久记忆(memory.md)或预设记忆。"
2844
2971
  }
2845
2972
  },
2846
2973
  output: {
@@ -2858,7 +2985,7 @@ function registerWorldBookTools(ctx, host) {
2858
2985
  }
2859
2986
  }
2860
2987
  },
2861
- render: (_args, value) => text(`世界书条目 ${value.id} 已删除(剩余 ${value.count} 条),生成目录已重建。`)
2988
+ render: (_args, value) => text$1(`世界书条目 ${value.id} 已删除(剩余 ${value.count} 条),生成目录已重建。`)
2862
2989
  },
2863
2990
  execute: async (args) => {
2864
2991
  const dir = host.activeDir();
@@ -2877,6 +3004,112 @@ function registerWorldBookTools(ctx, host) {
2877
3004
  });
2878
3005
  }
2879
3006
  //#endregion
3007
+ //#region src/runtime/session-var-tools.ts
3008
+ const text = (value) => [{
3009
+ type: "text",
3010
+ text: value
3011
+ }];
3012
+ function registerSessionVarTools(ctx) {
3013
+ ctx.inject(["tools"], (toolsCtx) => {
3014
+ toolsCtx.tools.register(defineTool({
3015
+ name: "session_var",
3016
+ description: "会话变量管理(SillyTavern setvar/getvar 语义):list 查看当前会话全部变量、get 读取、set 设置、clear 清除。提示词/世界书文本中的 {{变量名}} 会在注入时替换为会话变量值(会话级覆盖预设默认值);适合维护角色状态(如 {{心情}}、{{接受度}} 等)。注意:会话变量仅存于当前会话(结束即失);跨会话长期记忆请用 world_book 工具的 note 参数写入角色卡记忆(持久,跟随角色卡)。",
3017
+ parameters: {
3018
+ action: {
3019
+ type: "string",
3020
+ enum: [
3021
+ "list",
3022
+ "get",
3023
+ "set",
3024
+ "clear"
3025
+ ],
3026
+ required: true,
3027
+ description: "操作:list / get / set / clear"
3028
+ },
3029
+ key: {
3030
+ type: "string",
3031
+ description: "变量名(get / set / clear 时必填;支持中文与下划线)"
3032
+ },
3033
+ value: {
3034
+ type: "string",
3035
+ description: "变量值(set 时必填;空串表示清除该变量)"
3036
+ }
3037
+ },
3038
+ output: {
3039
+ schema: {
3040
+ type: "object",
3041
+ additionalProperties: false,
3042
+ properties: {
3043
+ ok: {
3044
+ type: "boolean",
3045
+ required: true
3046
+ },
3047
+ error: { type: "string" },
3048
+ variables: {
3049
+ type: "object",
3050
+ additionalProperties: true
3051
+ },
3052
+ key: { type: "string" },
3053
+ value: { type: "string" }
3054
+ }
3055
+ },
3056
+ render: (_args, value) => text(JSON.stringify(value))
3057
+ },
3058
+ execute: async (args, exec) => {
3059
+ const session = exec.agent?.session;
3060
+ if (session === void 0) return {
3061
+ ok: false,
3062
+ error: "no active session"
3063
+ };
3064
+ const { sessionVarsSnapshot, getSessionVar, setSessionVar, clearSessionVars } = await import(new URL("../engine/session-vars.mjs", import.meta.url).href);
3065
+ const record = args ?? {};
3066
+ const action = typeof record.action === "string" ? record.action : "";
3067
+ const key = typeof record.key === "string" ? record.key : "";
3068
+ if (action === "list") return {
3069
+ ok: true,
3070
+ variables: sessionVarsSnapshot(session)
3071
+ };
3072
+ if (action === "get") {
3073
+ if (key.length === 0) return {
3074
+ ok: false,
3075
+ error: "get 需要 key"
3076
+ };
3077
+ return {
3078
+ ok: true,
3079
+ key,
3080
+ value: getSessionVar(session, key) ?? ""
3081
+ };
3082
+ }
3083
+ if (action === "set") {
3084
+ if (key.length === 0) return {
3085
+ ok: false,
3086
+ error: "set 需要 key"
3087
+ };
3088
+ const value = typeof record.value === "string" ? record.value : "";
3089
+ if (value.length === 0) clearSessionVars(session, key);
3090
+ else setSessionVar(session, key, value);
3091
+ return {
3092
+ ok: true,
3093
+ key,
3094
+ value
3095
+ };
3096
+ }
3097
+ if (action === "clear") {
3098
+ clearSessionVars(session, key.length > 0 ? key : void 0);
3099
+ return {
3100
+ ok: true,
3101
+ key
3102
+ };
3103
+ }
3104
+ return {
3105
+ ok: false,
3106
+ error: `未知 action: ${action}`
3107
+ };
3108
+ }
3109
+ }));
3110
+ });
3111
+ }
3112
+ //#endregion
2880
3113
  //#region src/runtime/tui.ts
2881
3114
  /** dsh-tui 全局开关:键名与 settings 路径一致(settings mutate)。 */
2882
3115
  const TUI_GLOBAL_SWITCHES = [
@@ -3292,6 +3525,11 @@ function engineFingerprint() {
3292
3525
  return parts.sort().join("|");
3293
3526
  }
3294
3527
  const ENGINE_FINGERPRINT_MARKER = ".pt-engine-fingerprint";
3528
+ /** 剥离文本中的预设级变量引用({{key}} → 空串);内置变量({{DSH_HOME}} 等)保留。
3529
+ * 模板变量插值停用时由 writePreset 调用,避免 {{key}} 残留导致官方渲染 unknown variable。 */
3530
+ function stripVariableRefs(text, keys) {
3531
+ return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (whole, key) => keys.has(key) ? "" : whole);
3532
+ }
3295
3533
  /** Windows 瞬时文件锁(杀软/宿主短读)下的重试:rename/写文件失败最多重试 3 次、间隔 400ms。 */
3296
3534
  function withLockRetry(action, retries = 3) {
3297
3535
  for (let attempt = 0;; attempt++) try {
@@ -3326,7 +3564,6 @@ function runtimeOf(options, prompt) {
3326
3564
  toolFilterAllow: options.toolFilterAllow !== void 0 && (Array.isArray(options.toolFilterAllow) ? options.toolFilterAllow.length > 0 : String(options.toolFilterAllow).trim().length > 0) ? options.toolFilterAllow : void 0,
3327
3565
  toolFilterDeny: options.toolFilterDeny !== void 0 && (Array.isArray(options.toolFilterDeny) ? options.toolFilterDeny.length > 0 : String(options.toolFilterDeny).trim().length > 0) ? options.toolFilterDeny : void 0,
3328
3566
  maxDepth: options.maxDepth,
3329
- mainPersona: typeof options.mainPersona === "string" && options.mainPersona.trim().length > 0 ? options.mainPersona : void 0,
3330
3567
  allowKinds: options.allowKinds,
3331
3568
  firstTurnWord: typeof options.firstTurnWord === "string" && options.firstTurnWord.length > 0 ? options.firstTurnWord : void 0
3332
3569
  };
@@ -3371,7 +3608,7 @@ function modelRequestConfigs(params) {
3371
3608
  function writePreset(prompt, options) {
3372
3609
  const presetDir = options.presetDir.trim().length > 0 ? options.presetDir : DEFAULT_PRESET_DIR;
3373
3610
  const templateName = typeof options.presetTemplate === "string" && options.presetTemplate.trim().length > 0 ? options.presetTemplate.trim() : "anchored";
3374
- if (!/^[a-zA-Z0-9\u4e00-\u9fff_-]+$/.test(templateName)) throw new Error(`invalid presetTemplate ${JSON.stringify(templateName)}: must be a bare directory name`);
3611
+ 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-]*$/`);
3375
3612
  const templateDir = resolvePresetDir(templateName);
3376
3613
  const spec = loadPresetSpec(templateDir);
3377
3614
  const worldBook = spec.worldBook;
@@ -3529,11 +3766,17 @@ function writePreset(prompt, options) {
3529
3766
  });
3530
3767
  else templateDefaults = [];
3531
3768
  const merged = mergePromptConfigs(modelRequestConfigs(params), templateDefaults, options.promptConfigs);
3769
+ const presetVariables = {};
3770
+ for (const [key, value] of Object.entries(spec.params ?? {})) {
3771
+ if (PARAM_KEYS.has(key)) continue;
3772
+ const text = String(value);
3773
+ if (text.length > 0) presetVariables[key] = text;
3774
+ }
3775
+ for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string" && value.length > 0) presetVariables[key] = value;
3776
+ const variablesEnabled = spec.variablesEnabled !== false;
3777
+ const presetVariableKeys = new Set(Object.keys(presetVariables));
3778
+ if (variablesEnabled && presetVariableKeys.size > 0) writeFileSync(join(promptConfigsDir, "variables.yml"), stringify(presetVariables), "utf8");
3532
3779
  for (const [index, config] of merged.entries()) {
3533
- config.params = {
3534
- ...params,
3535
- ...config.params
3536
- };
3537
3780
  if (config.id === "prompt-injector") {
3538
3781
  delete config.text;
3539
3782
  config.texts = [];
@@ -3549,6 +3792,15 @@ function writePreset(prompt, options) {
3549
3792
  agentsInstructionPath: `../${templateName}/agents-instruction.md`,
3550
3793
  ...options.injectAgentsPrompt === true ? { text: asString(options.agentsInstructionText) } : {}
3551
3794
  };
3795
+ if (!variablesEnabled && presetVariableKeys.size > 0) {
3796
+ const strip = (item) => stripVariableRefs(item, presetVariableKeys);
3797
+ config.texts = (config.texts ?? []).map(strip);
3798
+ if (typeof config.text === "string") config.text = strip(config.text);
3799
+ if (typeof config.params?.text === "string") config.params = {
3800
+ ...config.params,
3801
+ text: strip(config.params.text)
3802
+ };
3803
+ }
3552
3804
  writeFileSync(join(promptConfigsDir, `${String(index * 10).padStart(2, "0")}-${config.id}.yml`), renderPromptConfigYaml(config), "utf8");
3553
3805
  }
3554
3806
  for (const legacy of spec.legacyCleanup ?? []) rmSync(join(outDir, legacy), { force: true });
@@ -3733,7 +3985,6 @@ function apply(ctx, configIn) {
3733
3985
  runtime.bootstrapMaxTokens = Number.isSafeInteger(params.bootstrapMaxTokens) && params.bootstrapMaxTokens >= 0 ? params.bootstrapMaxTokens : 0;
3734
3986
  runtime.usePtcMode = params.usePtcMode !== false;
3735
3987
  runtime.injectPrompt = params.injectPrompt !== false;
3736
- runtime.mainPersona = asString(params.mainPersona) || void 0;
3737
3988
  runtime.subagentPersona = asString(params.subagentPersona) || void 0;
3738
3989
  runtime.toolFilterAllow = params.toolFilterAllow;
3739
3990
  runtime.toolFilterDeny = params.toolFilterDeny;
@@ -3765,7 +4016,6 @@ function apply(ctx, configIn) {
3765
4016
  subagentReasoningEffort: runtime.subagentReasoningEffort,
3766
4017
  subagentTemperature: runtime.subagentTemperature,
3767
4018
  subagentMaxTokens: runtime.subagentMaxTokens,
3768
- mainPersona: runtime.mainPersona,
3769
4019
  subagentPersona: runtime.subagentPersona,
3770
4020
  toolFilterAllow: runtime.toolFilterAllow,
3771
4021
  toolFilterDeny: runtime.toolFilterDeny,
@@ -3797,13 +4047,26 @@ function apply(ctx, configIn) {
3797
4047
  }
3798
4048
  }
3799
4049
  writePreset(presetPrompt, options);
3800
- } else try {
3801
- rmSync(runtime.presetDir, {
3802
- recursive: true,
3803
- force: true
3804
- });
3805
- } catch (error) {
3806
- warn(ctx, "prompt-tool: failed to remove " + runtime.presetDir + ": " + String(error));
4050
+ } else {
4051
+ let cleaned = 0;
4052
+ for (const preset of listPresets()) {
4053
+ const dir = join(runtime.presetDir, preset.id);
4054
+ for (const name of [
4055
+ "agent.cordis.yml",
4056
+ "prompt-configs",
4057
+ "preset.md",
4058
+ "agents.md",
4059
+ "agents-instruction.md",
4060
+ "engine"
4061
+ ]) try {
4062
+ rmSync(join(dir, name), {
4063
+ recursive: true,
4064
+ force: true
4065
+ });
4066
+ } catch {}
4067
+ cleaned += 1;
4068
+ }
4069
+ warn(ctx, `prompt-tool: writePreset 已关闭,清理 ${cleaned} 个预设目录的生成物(preset.yml 参数保留)`);
3807
4070
  }
3808
4071
  };
3809
4072
  /** 激活预设目录(内容按预设根 presetDir/<template>/ 隔离;非法名回退 anchored)。 */
@@ -3845,7 +4108,6 @@ function apply(ctx, configIn) {
3845
4108
  if (typeof overrides.subagentReasoningEffort === "string") runtime.subagentReasoningEffort = overrides.subagentReasoningEffort;
3846
4109
  if (typeof overrides.subagentTemperature === "string") runtime.subagentTemperature = overrides.subagentTemperature;
3847
4110
  if (typeof overrides.subagentMaxTokens === "string") runtime.subagentMaxTokens = overrides.subagentMaxTokens;
3848
- if (typeof overrides.mainPersona === "string" && overrides.mainPersona.trim().length > 0) runtime.mainPersona = overrides.mainPersona;
3849
4111
  if (typeof overrides.subagentPersona === "string") runtime.subagentPersona = overrides.subagentPersona;
3850
4112
  if (typeof overrides.firstTurnWord === "string") runtime.firstTurnWord = overrides.firstTurnWord;
3851
4113
  if (typeof overrides.bootstrapMaxTokens === "number") runtime.bootstrapMaxTokens = overrides.bootstrapMaxTokens;
@@ -4034,7 +4296,6 @@ function apply(ctx, configIn) {
4034
4296
  subagentMaxTokens: asString(initialParams.subagentMaxTokens),
4035
4297
  bootstrapMaxTokens: Number.isSafeInteger(initialParams.bootstrapMaxTokens) && initialParams.bootstrapMaxTokens >= 0 ? initialParams.bootstrapMaxTokens : 0,
4036
4298
  usePtcMode: initialParams.usePtcMode !== false,
4037
- mainPersona: asString(initialParams.mainPersona) || void 0,
4038
4299
  subagentPersona: asString(initialParams.subagentPersona) || void 0,
4039
4300
  toolFilterAllow: initialParams.toolFilterAllow,
4040
4301
  toolFilterDeny: initialParams.toolFilterDeny,
@@ -4054,6 +4315,10 @@ function apply(ctx, configIn) {
4054
4315
  const s = hostSettingsService;
4055
4316
  if (s === void 0) return;
4056
4317
  const template = runtime.presetTemplate;
4318
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(template)) {
4319
+ warn(ctx, `prompt-tool: 预设 id ${JSON.stringify(template)} 不符合官方 agent-presets 命名(^[a-z0-9][a-z0-9-]*$),跳过宿主 default 同步;请将预设目录改名为合法 id`);
4320
+ return;
4321
+ }
4057
4322
  if (reason === "switch" && lastSyncedHostDefault === template) return;
4058
4323
  lastSyncedHostDefault = template;
4059
4324
  try {
@@ -4116,7 +4381,6 @@ function apply(ctx, configIn) {
4116
4381
  modelName: "",
4117
4382
  subagentModelProvider: "",
4118
4383
  subagentModelName: "",
4119
- mainPersona: "",
4120
4384
  subagentPersona: "",
4121
4385
  injectAgentsPrompt: false,
4122
4386
  bootstrapMaxTokens: 0,
@@ -4214,6 +4478,7 @@ function apply(ctx, configIn) {
4214
4478
  }
4215
4479
  }
4216
4480
  });
4481
+ registerSessionVarTools(ctx);
4217
4482
  const settingsEntry = currentSource();
4218
4483
  const ensureRegistered = (sctx) => ensureSettingsRegistered(sctx, NS, PromptSettingsSchema, {
4219
4484
  base: () => settingsEntry,
@@ -4234,11 +4499,21 @@ function apply(ctx, configIn) {
4234
4499
  },
4235
4500
  onError: (message) => warn(ctx, `prompt-tool: settings register failed: ${message}`)
4236
4501
  });
4237
- /** 阶段 2 迁移:旧全局 settings 引擎参数 → 激活预设 preset.yml(一次性,标记后跳过)。 */
4238
- const PARAMS_MIGRATED_MARK = ".pt-params-migrated";
4502
+ /** 阶段 2 迁移:旧全局 settings 引擎参数 → 激活预设 preset.yml(一次性,state.paramsMigrated 后跳过)。
4503
+ * 兼容旧版预设根内 .pt-params-migrated 标记:存在即视为已迁移,并迁入状态文件后删除。 */
4239
4504
  const migrateSettingsParamsToPreset = (sctx) => {
4240
- const mark = join(runtime.presetDir, PARAMS_MIGRATED_MARK);
4241
- if (existsSync(mark)) return;
4505
+ const legacyMark = join(runtime.presetDir, ".pt-params-migrated");
4506
+ const state = readPluginState();
4507
+ if (state.paramsMigrated === true || existsSync(legacyMark)) {
4508
+ if (existsSync(legacyMark)) try {
4509
+ writePluginState({
4510
+ ...state,
4511
+ paramsMigrated: true
4512
+ });
4513
+ rmSync(legacyMark, { force: true });
4514
+ } catch {}
4515
+ return;
4516
+ }
4242
4517
  let userSection = {};
4243
4518
  try {
4244
4519
  const descriptor = sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
@@ -4252,7 +4527,10 @@ function apply(ctx, configIn) {
4252
4527
  if (Object.keys(params).length === 0 && promptConfigs === void 0) {
4253
4528
  try {
4254
4529
  mkdirSync(runtime.presetDir, { recursive: true });
4255
- writeFileSync(mark, "", "utf8");
4530
+ writePluginState({
4531
+ ...readPluginState(),
4532
+ paramsMigrated: true
4533
+ });
4256
4534
  } catch {}
4257
4535
  return;
4258
4536
  }
@@ -4265,12 +4543,18 @@ function apply(ctx, configIn) {
4265
4543
  written++;
4266
4544
  } catch {}
4267
4545
  if (written === 0) {
4268
- writeFileSync(mark, "", "utf8");
4546
+ writePluginState({
4547
+ ...readPluginState(),
4548
+ paramsMigrated: true
4549
+ });
4269
4550
  return;
4270
4551
  }
4271
4552
  reloadPresetParams();
4272
4553
  rebuildPreset();
4273
- writeFileSync(mark, "", "utf8");
4554
+ writePluginState({
4555
+ ...readPluginState(),
4556
+ paramsMigrated: true
4557
+ });
4274
4558
  warn(ctx, `prompt-tool: 已把旧全局 settings 参数迁移到 ${written} 个预设 preset.yml(每预设独立存储)`);
4275
4559
  } catch (error) {
4276
4560
  warn(ctx, `prompt-tool: settings 参数迁移失败(下次启动重试):${error instanceof Error ? error.message : String(error)}`);
@@ -4292,4 +4576,4 @@ function apply(ctx, configIn) {
4292
4576
  });
4293
4577
  }
4294
4578
  //#endregion
4295
- 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, readSkills, registerCharacterTools, registerSettingsBridge, registerTuiCommand, registerWorldBookTools, removeUserPreset, resolvePresetDir, resolvePresetParams, resolveProfileDir, resolveProfileSkillsDir, savePresetParams, toKebabName, userPresetsDir, validSkills, validatePromptConfigs, writePreset };
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 };