dsh-plugin-prompt-tool 0.8.0 → 1.0.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 (75) hide show
  1. package/README.md +58 -50
  2. package/engine/character-tools.mjs +3 -6
  3. package/engine/compositions/library/agent-instructions.yml +1 -1
  4. package/engine/compositions/library/command-goal.yml +1 -1
  5. package/engine/compositions/library/compaction.yml +1 -1
  6. package/engine/compositions/library/delegation-ptc.yml +1 -1
  7. package/engine/compositions/library/delegation.yml +1 -1
  8. package/engine/compositions/library/persistent-shell.yml +1 -1
  9. package/engine/compositions/library/planning.yml +1 -1
  10. package/engine/compositions/library/skill-filesystem-cordis.yml +1 -1
  11. package/engine/compositions/library/skill-filesystem.yml +1 -1
  12. package/engine/compositions/library/tool-ask-user.yml +1 -1
  13. package/engine/compositions/library/tool-bash.yml +1 -1
  14. package/engine/compositions/library/tool-cordis.yml +1 -1
  15. package/engine/compositions/library/tool-fs-search.yml +1 -1
  16. package/engine/compositions/library/tool-fs.yml +1 -1
  17. package/engine/compositions/library/tool-goal.yml +1 -1
  18. package/engine/compositions/library/tool-jobs.yml +1 -1
  19. package/engine/compositions/library/tool-plugin-manager-disabled.yml +1 -1
  20. package/engine/compositions/library/tool-plugin-manager.yml +1 -1
  21. package/engine/compositions/library/tool-present.yml +1 -1
  22. package/engine/compositions/library/tool-presentation.yml +1 -1
  23. package/engine/compositions/library/tool-pwsh.yml +1 -1
  24. package/engine/compositions/library/tool-skill.yml +1 -1
  25. package/engine/compositions/library/tool-todo.yml +1 -1
  26. package/engine/compositions/library/tool-web.yml +1 -1
  27. package/engine/compositions/source/local/character-tools.yml +4 -4
  28. package/engine/compositions/source/local/prompt-config-engine.yml +1 -1
  29. package/engine/compositions/source/local/session-var-tools.yml +4 -4
  30. package/engine/compositions/source/local/world-book-tools.yml +4 -4
  31. package/engine/executor.mjs +39 -26
  32. package/engine/instruction-hint.mjs +6 -2
  33. package/engine/layers.mjs +38 -30
  34. package/engine/order.mjs +17 -0
  35. package/engine/schema.mjs +16 -6
  36. package/engine/session-var-tools.mjs +3 -6
  37. package/engine/st-render.mjs +8 -4
  38. package/engine/subagent-tool-policy.mjs +25 -10
  39. package/engine/world-book-tools.mjs +3 -6
  40. package/lib/client.js +3451 -3519
  41. package/lib/client.js.map +1 -1
  42. package/lib/index.d.mts +233 -272
  43. package/lib/index.mjs +1020 -1226
  44. package/lib/{preset-package-BnJqrU4R.mjs → module-package-Cm3fHHlp.mjs} +997 -594
  45. package/lib/{paths-CY5psBqE.d.mts → paths-BgL42sSM.d.mts} +191 -149
  46. package/lib/preset-transfer.d.mts +2 -2
  47. package/lib/preset-transfer.mjs +2 -2
  48. package/modules/ponytail/configs/0000-ponytail-rules.yml +102 -0
  49. package/modules/ponytail/configs/0010-ponytail-level-full.yml +19 -0
  50. package/modules/ponytail/configs/0020-ponytail-level-lite.yml +19 -0
  51. package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +19 -0
  52. package/modules/ponytail/module.yml +172 -0
  53. package/package.json +46 -80
  54. package/templates/20-system-section.yml +4 -1
  55. package/templates/70-subagent-maintenance.yml +1 -1
  56. package/templates/tools/50-delegate-worldbook.yml +1 -1
  57. package/preset/pt-cordis/preset.yml +0 -43
  58. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +0 -29
  59. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +0 -501
  60. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +0 -38
  61. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +0 -60
  62. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +0 -5
  63. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +0 -13
  64. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +0 -5
  65. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +0 -22
  66. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +0 -3
  67. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +0 -2
  68. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +0 -15
  69. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +0 -8
  70. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +0 -7
  71. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +0 -84
  72. package/preset/pt-custom/preset.yml +0 -15
  73. package/preset/pt-minimal/preset.yml +0 -27
  74. package/preset/pt-ptc/preset.yml +0 -45
  75. package/preset/pt-standard/preset.yml +0 -43
@@ -1,8 +1,8 @@
1
- import { appendFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
1
  import { basename, dirname, isAbsolute, join, posix, resolve, sep } from "node:path";
2
+ import { appendFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { spawn } from "node:child_process";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { Pair, Scalar, YAMLMap, YAMLSeq, parse, parseDocument, stringify } from "yaml";
5
+ import { Pair, Scalar, YAMLMap, YAMLSeq, isMap, parse, parseDocument, stringify } from "yaml";
6
6
  import { homedir } from "node:os";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { Buffer as Buffer$1 } from "node:buffer";
@@ -29,177 +29,196 @@ const DSH_HOME = resolveDshHome();
29
29
  * 插件不再内置任何技能:包内没有 skills 目录,也不再有安装副本与内容账本。 */
30
30
  const USER_SKILLS_DIR = join(DSH_HOME, "skills");
31
31
  /**
32
- * 预设根:官方 USER_PRESET_DIR(DSH_HOME/.agent-presets)。
33
- * 每个预设一个官方预设目录(agent.cordis.yml 组合本体 + preset.yml 参数),
34
- * 宿主 agent-presets 直接可挂载;共享引擎自阶段 2 起由**插件包**提供(组合行写包名说明符),
35
- * 不再物化到预设根下。
32
+ * 插件自有**存储根**(DSH_HOME/.prompt-tool)。
33
+ *
34
+ * 根下按物分目录:`modules/` 放模块定义与物化产物,`.characters/` 放角色卡库。
35
+ * 与模块**同级的兄弟物**从模块路径上溯一级定位(`charactersDir(moduleRoot)` 就是这么做),
36
+ * 不从这里读——模块级常量在 import 时按 `DSH_HOME` 冻结,测试内改环境变量对它无效。
37
+ * 旧根 `.agent-presets/` 只读不删,本插件不再读写它。
38
+ */
39
+ const STORAGE_ROOT = join(DSH_HOME, ".prompt-tool");
40
+ /**
41
+ * 存储根下的**模块根**:一个模块一个目录(`modules/<id>/`),定义与物化产物都在其内。
42
+ *
43
+ * 这是 `resolveModuleDir` / `assertModuleDirectory` / `listModules` 的扫描根,
44
+ * 也是「本插件管理的模块」身份判定的基准。模块目录必须是它的**直接子目录**
45
+ * (`assertModuleDirectory` 的一层深度契约由此成立)。
36
46
  */
37
- const DEFAULT_PRESET_DIR = join(DSH_HOME, ".agent-presets");
38
- join(DEFAULT_PRESET_DIR, ".engine");
47
+ const MODULES_DIR = join(STORAGE_ROOT, "modules");
48
+ /**
49
+ * 模块定义文件名:`<模块根>/<id>/module.yml`。
50
+ *
51
+ * 与物化产物(`configs/`、`custom-tools/`、`triggers.yml`、`subagent-tools/`)同目录。
52
+ * 旧形态的 `preset.yml` **不做兼容读取**:实测真实数据(beta 形态遗留)已经是本名,
53
+ * 且仓库从 `02754cc` 起就没有 `preset.yml` 形态的用户数据;读到旧名目录时由
54
+ * `listModules` 给出明确诊断,而不是静默跳过。
55
+ */
56
+ const MODULE_DEFINITION_FILE = "module.yml";
57
+ /** 模块物化目录名:提示词配置切片的落点(原 `prompt-configs/`)。 */
58
+ const MODULE_CONFIGS_DIR = "configs";
39
59
  //#endregion
40
- //#region src/shared/engine-params.ts
41
- const FINITE_NUMBER = (value) => Number.isFinite(value) ? void 0 : "必须是有限数字";
42
- const POSITIVE_INTEGER = (value) => Number.isSafeInteger(value) && value > 0 ? void 0 : "必须是正整数";
43
- /** 类型、校验、卡片归属、草稿默认值和组合行映射的唯一运行时目录。 */
44
- const ENGINE_PARAM_DEFINITIONS = {
60
+ //#region src/shared/legacy-prompt-params.ts
61
+ /** 旧规则快捷参数只用于读取与旧 API 兼容;新编辑器直接保存规则实例。 */
62
+ const LEGACY_PROMPT_PARAM_DEFINITIONS = {
45
63
  firstTurnAnchor: {
46
64
  kind: "boolean",
47
- defaultValue: false,
48
- card: "prompt-defaults"
65
+ targets: ["near-anchor"]
49
66
  },
50
67
  firstTurnText: {
51
68
  kind: "string",
52
- defaultValue: "",
53
- card: "prompt-defaults"
69
+ targets: ["near-anchor", "prompt-injector"]
54
70
  },
55
71
  firstTurnCustom: {
56
72
  kind: "boolean",
57
- defaultValue: false,
58
- card: "prompt-defaults"
73
+ targets: ["near-anchor"]
59
74
  },
60
75
  guideText: {
61
76
  kind: "string",
62
- defaultValue: "",
63
- card: "prompt-defaults"
77
+ targets: ["router-guide"]
64
78
  },
65
79
  guideCustom: {
66
80
  kind: "boolean",
67
- defaultValue: false,
68
- card: "prompt-defaults"
81
+ targets: ["router-guide"]
69
82
  },
70
83
  guideEnabled: {
71
84
  kind: "boolean",
72
- defaultValue: false,
73
- card: "prompt-defaults"
85
+ targets: ["router-guide"]
74
86
  },
75
87
  injectPrompt: {
76
88
  kind: "boolean",
77
- defaultValue: true,
78
- card: "prompt-defaults"
89
+ targets: ["prompt-injector"]
79
90
  },
91
+ firstTurnWord: {
92
+ kind: "string",
93
+ targets: ["prompt-injector"]
94
+ },
95
+ buildPattern: {
96
+ kind: "pattern",
97
+ targets: ["near-anchor"]
98
+ },
99
+ complexPattern: {
100
+ kind: "pattern",
101
+ targets: ["near-anchor", "router-guide"]
102
+ },
103
+ firstTurnBuild: {
104
+ kind: "string",
105
+ targets: ["near-anchor", "prompt-injector"]
106
+ },
107
+ firstTurnInspect: {
108
+ kind: "string",
109
+ targets: ["near-anchor", "prompt-injector"]
110
+ },
111
+ firstTurnDeep: {
112
+ kind: "string",
113
+ targets: ["near-anchor", "prompt-injector"]
114
+ },
115
+ guideWeak: {
116
+ kind: "string",
117
+ targets: ["router-guide"]
118
+ },
119
+ guideDeep: {
120
+ kind: "string",
121
+ targets: ["router-guide"]
122
+ }
123
+ };
124
+ const LEGACY_PROMPT_PARAM_KEYS = Object.keys(LEGACY_PROMPT_PARAM_DEFINITIONS);
125
+ const isLegacyPromptParam = (key) => Object.hasOwn(LEGACY_PROMPT_PARAM_DEFINITIONS, key);
126
+ const legacyPromptParamPath = (key) => [
127
+ "layerSettings",
128
+ "pre-step",
129
+ key
130
+ ];
131
+ //#endregion
132
+ //#region src/shared/engine-params.ts
133
+ const FINITE_NUMBER = (value) => Number.isFinite(value) ? void 0 : "必须是有限数字";
134
+ const POSITIVE_INTEGER = (value) => Number.isSafeInteger(value) && value > 0 ? void 0 : "必须是正整数";
135
+ /** 类型、校验、卡片归属、草稿默认值和组合行映射的唯一运行时目录。 */
136
+ const ENGINE_PARAM_DEFINITIONS = {
80
137
  modelProvider: {
81
138
  kind: "string",
82
139
  defaultValue: "",
83
- card: "main-model"
140
+ card: "main-model",
141
+ storageLayer: "agent-request"
84
142
  },
85
143
  modelName: {
86
144
  kind: "string",
87
145
  defaultValue: "",
88
- card: "main-model"
146
+ card: "main-model",
147
+ storageLayer: "agent-request"
89
148
  },
90
149
  subagentModelProvider: {
91
150
  kind: "string",
92
151
  defaultValue: "",
93
- card: "subagent-model"
152
+ card: "subagent-model",
153
+ storageLayer: "subagent-start"
94
154
  },
95
155
  subagentModelName: {
96
156
  kind: "string",
97
157
  defaultValue: "",
98
- card: "subagent-model"
158
+ card: "subagent-model",
159
+ storageLayer: "subagent-start"
99
160
  },
100
161
  modelReasoningEffort: {
101
162
  kind: "string",
102
163
  defaultValue: "",
103
- card: "main-model"
164
+ card: "main-model",
165
+ storageLayer: "agent-request"
104
166
  },
105
167
  modelTemperature: {
106
168
  kind: "number",
107
169
  check: FINITE_NUMBER,
108
170
  defaultValue: "",
109
- card: "main-model"
171
+ card: "main-model",
172
+ storageLayer: "agent-request"
110
173
  },
111
174
  modelMaxTokens: {
112
175
  kind: "number",
113
176
  check: POSITIVE_INTEGER,
114
177
  defaultValue: "",
115
- card: "main-model"
178
+ card: "main-model",
179
+ storageLayer: "agent-request"
116
180
  },
117
181
  subagentReasoningEffort: {
118
182
  kind: "string",
119
183
  defaultValue: "",
120
- card: "subagent-model"
184
+ card: "subagent-model",
185
+ storageLayer: "subagent-start"
121
186
  },
122
187
  subagentTemperature: {
123
188
  kind: "number",
124
189
  check: FINITE_NUMBER,
125
190
  defaultValue: "",
126
- card: "subagent-model"
191
+ card: "subagent-model",
192
+ storageLayer: "subagent-start"
127
193
  },
128
194
  subagentMaxTokens: {
129
195
  kind: "number",
130
196
  check: POSITIVE_INTEGER,
131
197
  defaultValue: "",
132
- card: "subagent-model"
198
+ card: "subagent-model",
199
+ storageLayer: "subagent-start"
133
200
  },
134
201
  maxDepth: {
135
202
  kind: "max-depth",
136
203
  defaultValue: "",
137
- card: "subagent-tools"
138
- },
139
- firstTurnWord: {
140
- kind: "string",
141
- defaultValue: "",
142
- card: "prompt-defaults"
143
- },
144
- buildPattern: {
145
- kind: "pattern",
146
- defaultValue: "",
147
- card: "prompt-defaults"
148
- },
149
- complexPattern: {
150
- kind: "pattern",
151
- defaultValue: "",
152
- card: "prompt-defaults"
153
- },
154
- firstTurnBuild: {
155
- kind: "string",
156
- defaultValue: "",
157
- card: "prompt-defaults"
158
- },
159
- firstTurnInspect: {
160
- kind: "string",
161
- defaultValue: "",
162
- card: "prompt-defaults"
163
- },
164
- firstTurnDeep: {
165
- kind: "string",
166
- defaultValue: "",
167
- card: "prompt-defaults"
168
- },
169
- guideWeak: {
170
- kind: "string",
171
- defaultValue: "",
172
- card: "prompt-defaults"
173
- },
174
- guideDeep: {
175
- kind: "string",
176
- defaultValue: "",
177
- card: "prompt-defaults"
204
+ card: "subagent-tools",
205
+ storageLayer: "subagent-start"
178
206
  },
179
207
  instructionHint: {
180
208
  kind: "boolean",
181
209
  defaultValue: false,
182
210
  card: "prompt-defaults",
211
+ storageLayer: "pre-step",
183
212
  module: {
184
213
  row: "instruction-hint",
185
214
  key: "enabled"
186
215
  }
187
216
  },
188
- strReplaceEditorMaxOutputChars: {
189
- kind: "number",
190
- check: POSITIVE_INTEGER,
191
- defaultValue: 16e3,
192
- card: "str-replace-editor",
193
- module: {
194
- row: "str-replace-editor",
195
- key: "maxOutputChars",
196
- mode: "editor-default"
197
- }
198
- },
199
217
  toolGitBashEnabled: {
200
218
  kind: "boolean",
201
219
  defaultValue: true,
202
220
  card: "tool-git-bash",
221
+ storageLayer: "tool-pipeline",
203
222
  module: {
204
223
  row: "tool-git-bash",
205
224
  key: "enabled"
@@ -216,6 +235,7 @@ const ENGINE_PARAM_DEFINITIONS = {
216
235
  ],
217
236
  defaultValue: "",
218
237
  card: "tool-config-engine",
238
+ storageLayer: "tool-pipeline",
219
239
  module: {
220
240
  row: "tool-config-engine",
221
241
  key: "requireApproval"
@@ -224,7 +244,7 @@ const ENGINE_PARAM_DEFINITIONS = {
224
244
  };
225
245
  const ENGINE_PARAM_KEYS = Object.keys(ENGINE_PARAM_DEFINITIONS);
226
246
  /** writePreset.runtimeOf 实际透传键:全部引擎参数可直接进入 writer。 */
227
- const WRITER_PARAM_KEYS = ENGINE_PARAM_KEYS;
247
+ const WRITER_PARAM_KEYS = [...ENGINE_PARAM_KEYS, ...LEGACY_PROMPT_PARAM_KEYS];
228
248
  /** 已有逗号分隔/flow-array 语义,client 与生成器共享。 */
229
249
  function engineParamList(value) {
230
250
  if (Array.isArray(value)) return value.map(String).filter((item) => item.length > 0);
@@ -240,10 +260,6 @@ function buildEngineModuleParams(params) {
240
260
  const binding = definition.module;
241
261
  if (binding === void 0) continue;
242
262
  let value = params[key];
243
- if (binding.mode === "editor-default") {
244
- if (typeof value === "string" && value.trim().length > 0) value = Number(value);
245
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) continue;
246
- }
247
263
  if (value === void 0 || value === null) continue;
248
264
  if (definition.kind === "boolean") value = value === true;
249
265
  else if (definition.kind === "string-list") value = engineParamList(value);
@@ -321,11 +337,11 @@ function validateEngineParamValues(overrides) {
321
337
  const errors = [];
322
338
  for (const [key, value] of Object.entries(overrides)) {
323
339
  if (value === void 0 || value === null) continue;
324
- const rule = Object.hasOwn(ENGINE_PARAM_DEFINITIONS, key) ? ENGINE_PARAM_DEFINITIONS[key] : void 0;
340
+ const rule = Object.hasOwn(ENGINE_PARAM_DEFINITIONS, key) ? ENGINE_PARAM_DEFINITIONS[key] : isLegacyPromptParam(key) ? LEGACY_PROMPT_PARAM_DEFINITIONS[key] : void 0;
325
341
  if (rule === void 0) {
326
342
  errors.push({
327
343
  key,
328
- message: `${key}: 未知参数键,请按当前参数定义更新预设`
344
+ message: `${key}: 未知参数键,请按当前参数定义更新模块`
329
345
  });
330
346
  continue;
331
347
  }
@@ -405,12 +421,6 @@ const ENGINE_CAPABILITIES = [
405
421
  }
406
422
  }
407
423
  },
408
- {
409
- id: "str-replace-editor",
410
- moduleKeys: ["filesystem-editor"],
411
- rowIds: ["str-replace-editor"],
412
- displayLayer: "tool-pipeline"
413
- },
414
424
  {
415
425
  id: "tool-config-engine",
416
426
  moduleKeys: ["tool-config-engine"],
@@ -426,7 +436,7 @@ const ENGINE_CAPABILITIES = [
426
436
  ];
427
437
  /**
428
438
  * 非能力编辑组的主归属。能力组不在这里重复登记:它们的 displayLayer 由能力定义本身承载
429
- * (例如「编辑器输出上限」参数的 card 就是能力 id `str-replace-editor`,已随能力登记)。
439
+ * (例如工具批准策略的 card 就是能力 id `tool-config-engine`,已随能力登记)。
430
440
  */
431
441
  const ENGINE_EDITOR_GROUPS = [
432
442
  {
@@ -452,9 +462,8 @@ const ENGINE_EDITOR_GROUPS = [
452
462
  },
453
463
  {
454
464
  id: "subagent-model",
455
- displayLayer: "subagent-start",
456
- relatedLayers: ["agent-request"],
457
- hook: "subagent-start"
465
+ displayLayer: "agent-request",
466
+ hook: "agent-request"
458
467
  },
459
468
  {
460
469
  id: "subagent-tools",
@@ -493,10 +502,10 @@ function engineRecipe(id) {
493
502
  return ENGINE_RECIPES.find((recipe) => recipe.id === id);
494
503
  }
495
504
  /**
496
- * 预设里显式写了的引擎参数 / 行配置所隐含的模块:**参数在 ⇒ 装配在**。
505
+ * 模块里显式写了的引擎参数 / 行配置所隐含的模块:**参数在 ⇒ 装配在**。
497
506
  *
498
507
  * 只认显式声明(`params` 的登记参数键与 `moduleConfigs` 的行键),组合源自带的默认值不算——
499
- * 否则任何预设都会把全部能力装回来。返回能力拥有的模块 id(去重,顺序稳定)。
508
+ * 否则任何模块都会把全部能力装回来。返回能力拥有的模块 id(去重,顺序稳定)。
500
509
  * 装配入口、模块事实与"移除能力"三处共用这一份派生,避免各自判断漂移。
501
510
  */
502
511
  function impliedModulesForParams(params, moduleConfigs) {
@@ -519,7 +528,7 @@ function impliedModulesForParams(params, moduleConfigs) {
519
528
  if (moduleConfigs !== void 0 && moduleConfigs !== null) for (const rowId of Object.keys(moduleConfigs)) addRow(rowId);
520
529
  return [...implied];
521
530
  }
522
- /** 显式模块预设中的实际能力;包含当前参数与顶层策略所需的装配。
531
+ /** 显式模块声明中的实际能力;包含当前参数与顶层策略所需的装配。
523
532
  * 创建补声明由 host 单独检查 declaredModules;官方组合行不伪装成可编辑能力。 */
524
533
  function isEngineCapabilityPresent(id, facts) {
525
534
  if (facts === void 0 || facts.sourceMode !== "explicit") return false;
@@ -582,10 +591,15 @@ function personaRowConfig(persona) {
582
591
  }
583
592
  //#endregion
584
593
  //#region src/shared/preset-ids.ts
585
- /** 包内模板、用户副本与当前预设共用同名 ID;避免与宿主官方预设重名。 */
586
- const DEFAULT_PRESET_ID = "pt-standard";
594
+ /**
595
+ * 包内模块与用户副本共用同名 ID;避免与宿主官方预设重名。
596
+ *
597
+ * 内置模块随包分发在仓库根的 `modules/`。旧的内置预设 `pt-*` 已整体退场——它们是官方
598
+ * 预设形态的遗留,重构后不再读取也不再维护;这里指向当前随包分发的内置模块作为兜底。
599
+ */
600
+ const DEFAULT_MODULE_ID = "ponytail";
587
601
  //#endregion
588
- //#region src/host/preset-install.ts
602
+ //#region src/host/module-install.ts
589
603
  function assertPresetId(id) {
590
604
  if (typeof id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(id)) throw new Error(`非法预设 id:${String(id)};必须为小写字母、数字和连字符`);
591
605
  }
@@ -624,7 +638,7 @@ function canonicalPresetRoot(root, allowMissing = false) {
624
638
  return realpathSync(absoluteRoot);
625
639
  }
626
640
  /** 根与目标先解析真实路径,拒绝链接目录及定义身份不一致。 */
627
- function assertPresetDirectory(root, id, allowMissing = false) {
641
+ function assertModuleDirectory(root, id, allowMissing = false) {
628
642
  assertPresetId(id);
629
643
  const canonicalRoot = canonicalPresetRoot(root, allowMissing);
630
644
  const target = join(canonicalRoot, id);
@@ -635,7 +649,7 @@ function assertPresetDirectory(root, id, allowMissing = false) {
635
649
  const stat = lstatSync(target);
636
650
  if (stat.isSymbolicLink() || !stat.isDirectory() || dirname(realpathSync(target)) !== canonicalRoot) throw new Error(`预设路径包含链接或越界:${id}`);
637
651
  if (!readdirSync(canonicalRoot).includes(id)) throw new Error(`预设目录身份不匹配:${id}`);
638
- const definition = join(target, "preset.yml");
652
+ const definition = join(target, MODULE_DEFINITION_FILE);
639
653
  const fileStat = lstatSync(definition);
640
654
  if (fileStat.isSymbolicLink() || !fileStat.isFile()) throw new Error(`预设定义不是普通文件:${id}`);
641
655
  const doc = parseDocument(readFileSync(definition, "utf8"), { logLevel: "silent" });
@@ -676,7 +690,7 @@ function engineModuleFileNames(dir) {
676
690
  const ENGINE_MANAGED_PATHS = {
677
691
  "prompt-config-engine.mjs": {
678
692
  field: "configsDir",
679
- directory: "prompt-configs"
693
+ directory: MODULE_CONFIGS_DIR
680
694
  },
681
695
  "tool-config-engine.mjs": {
682
696
  field: "configsDir",
@@ -752,12 +766,10 @@ function rewritePresetEngineReferences(raw, outputId, engineFiles) {
752
766
  return changed ? doc.toString() : raw;
753
767
  }
754
768
  //#endregion
755
- //#region src/host/preset-layer-settings.ts
756
- /** 预设磁盘参数:按编辑组主归属层存储,运行时仍使用 EngineParams 平铺接口。 */
769
+ //#region src/host/module-layer-settings.ts
770
+ /** 模块磁盘参数:按稳定存储层归属,运行时仍使用 EngineParams 平铺接口。 */
757
771
  const ENGINE_PARAM_LAYERS = Object.fromEntries(ENGINE_PARAM_KEYS.map((key) => {
758
- const layer = ENGINE_EDITOR_GROUP_MAP.find((group) => group.id === ENGINE_PARAM_DEFINITIONS[key].card)?.displayLayer;
759
- if (layer === void 0) throw new Error(`引擎参数未登记所属层:${key}`);
760
- return [key, layer];
772
+ return [key, ENGINE_PARAM_DEFINITIONS[key].storageLayer];
761
773
  }));
762
774
  var PresetLayerSettingsError = class extends Error {
763
775
  code = "preset-layer-settings-invalid";
@@ -766,7 +778,7 @@ var PresetLayerSettingsError = class extends Error {
766
778
  this.name = "PresetLayerSettingsError";
767
779
  }
768
780
  };
769
- const isRecord$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
781
+ const isRecord$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
770
782
  function engineParamPath(key) {
771
783
  if (!Object.prototype.hasOwnProperty.call(ENGINE_PARAM_LAYERS, key)) throw new Error(`未知引擎参数:${key}`);
772
784
  return [
@@ -781,11 +793,11 @@ function readLayerSettings(value) {
781
793
  throw new PresetLayerSettingsError(message);
782
794
  };
783
795
  if (value === void 0) return {};
784
- if (!isRecord$3(value)) return invalid("layerSettings 必须是对象");
796
+ if (!isRecord$4(value)) return invalid("layerSettings 必须是对象");
785
797
  const params = {};
786
798
  for (const [layer, settings] of Object.entries(value)) {
787
799
  if (!ENGINE_LAYER_ORDER.includes(layer)) invalid(`未知插入点:layerSettings.${layer}`);
788
- if (!isRecord$3(settings)) return invalid(`layerSettings.${layer} 必须是对象`);
800
+ if (!isRecord$4(settings)) return invalid(`layerSettings.${layer} 必须是对象`);
789
801
  for (const [key, entry] of Object.entries(settings)) {
790
802
  if (!Object.prototype.hasOwnProperty.call(ENGINE_PARAM_LAYERS, key)) continue;
791
803
  const expected = ENGINE_PARAM_LAYERS[key];
@@ -797,7 +809,7 @@ function readLayerSettings(value) {
797
809
  }
798
810
  /** 仅消费当前 layerSettings;其余段作为未知字段留存,不投影为运行时参数。 */
799
811
  function readPresetLayerSettings(source) {
800
- if (!isRecord$3(source)) throw new PresetLayerSettingsError("preset.yml 必须是对象");
812
+ if (!isRecord$4(source)) throw new PresetLayerSettingsError("preset.yml 必须是对象");
801
813
  return readLayerSettings(source.layerSettings);
802
814
  }
803
815
  //#endregion
@@ -7990,6 +8002,22 @@ function getSessionVar(session, key) {
7990
8002
  return vars === void 0 || !Object.hasOwn(vars, String(key)) ? void 0 : vars[String(key)];
7991
8003
  }
7992
8004
  //#endregion
8005
+ //#region engine/order.mjs
8006
+ /** 加载器私有元数据:不进入 YAML、公共配置或客户端载荷。 */
8007
+ const FILE_SEQUENCE = Symbol("prompt-tool:file-sequence");
8008
+ const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8009
+ /** 模块配置按持久序号执行;无物化来源的独立调用保留原 order 语义。 */
8010
+ function compareConfigSequence(a, b) {
8011
+ const difference = (a.sequence ?? a.order ?? 0) - (b.sequence ?? b.order ?? 0);
8012
+ if (difference !== 0) return difference;
8013
+ if (a.sourceModuleId === void 0 && b.sourceModuleId === void 0) return 0;
8014
+ return compareText(a.sourceModuleId ?? "", b.sourceModuleId ?? "") || compareText(a.id, b.id);
8015
+ }
8016
+ /** 官方文本段的定位仍由 order 决定;序号只提供同位置的稳定顺序。 */
8017
+ function compareTextPlacement(a, b) {
8018
+ return a.order - b.order || compareConfigSequence(a, b);
8019
+ }
8020
+ //#endregion
7993
8021
  //#region engine/st-render.mjs
7994
8022
  /** ST 模板变量帧:只求值显式导入的模板,不决定任何官方插入点的执行顺序。 */
7995
8023
  function visible(config, agent) {
@@ -8098,7 +8126,7 @@ function attachStRenderers(configs) {
8098
8126
  frame.evaluate = evaluate;
8099
8127
  if (session) sessions.set(session, frame);
8100
8128
  }
8101
- for (const config of templates.filter((config) => approved(config) && config.strategy === "static" && config.dedupe === "none" && !frame.text.has(config)).sort((a, b) => a.order - b.order)) frame.evaluate(config);
8129
+ if (target.sequence === void 0 || eligible === void 0) for (const config of templates.filter((config) => approved(config) && config.strategy === "static" && config.dedupe === "none" && !frame.text.has(config)).sort(compareConfigSequence)) frame.evaluate(config);
8102
8130
  if (token && typeof token === "object") tokens.set(token, frame);
8103
8131
  if (!frame.text.has(target)) frame.evaluate(target);
8104
8132
  return frame.text.get(target) ?? "";
@@ -8154,6 +8182,15 @@ function loadTemplate(file, baseUrl = import.meta.url, presetRootUrl) {
8154
8182
  return { text: raw };
8155
8183
  }
8156
8184
  /**
8185
+ * 完整 YAML 解析(vendored yaml 包):提示词配置文件直接使用标准 YAML。
8186
+ * 支持缩进 map、列表、block scalar、行尾注释、引号转义等全部语法。
8187
+ */
8188
+ function parsePromptConfigYaml(raw) {
8189
+ const parsed = parse$1(raw);
8190
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new TypeError(`${name$2}: prompt config yaml must contain a single object`);
8191
+ return parsed;
8192
+ }
8193
+ /**
8157
8194
  * 枚举提示词配置候选文件:**两条加载路径共用同一份枚举规则**。
8158
8195
  *
8159
8196
  * 扩展名、排序与跳过名单必须两侧一致——否则 host(编辑/列举)与引擎(注入)会看到不同的
@@ -8169,6 +8206,44 @@ function loadTemplate(file, baseUrl = import.meta.url, presetRootUrl) {
8169
8206
  function promptConfigFileNames(entries) {
8170
8207
  return entries.filter((entry) => entry.isFile() && /\.(ya?ml|json)$/i.test(entry.name)).sort((a, b) => a.name.localeCompare(b.name)).map((entry) => entry.name).filter((fileName) => fileName !== "variables.yml");
8171
8208
  }
8209
+ /**
8210
+ * 从提示词配置模块目录加载全部提示词配置描述:按文件名排序扫描 *.yml / *.yaml / *.json。
8211
+ * 文件名用数字前缀表达引擎执行顺序(00-…、10-…)。
8212
+ */
8213
+ function loadPromptConfigFiles(dirUrl) {
8214
+ let entries;
8215
+ try {
8216
+ entries = readdirSync(dirUrl, { withFileTypes: true });
8217
+ } catch (error) {
8218
+ throw new TypeError(`${name$2}: configsDir ${String(dirUrl)} is not readable: ${String(error?.message ?? error)}`);
8219
+ }
8220
+ let presetVariables = {};
8221
+ try {
8222
+ const parsed = parse$1(readFileSync(new URL("variables.yml", dirUrl), "utf8"));
8223
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) presetVariables = parsed;
8224
+ } catch {}
8225
+ const specs = [];
8226
+ for (const fileName of promptConfigFileNames(entries)) {
8227
+ const raw = readFileSync(new URL(fileName, dirUrl), "utf8");
8228
+ const spec = /\.json$/i.test(fileName) ? JSON.parse(raw) : parsePromptConfigYaml(raw);
8229
+ const prefix = /^(\d+)-/.exec(fileName);
8230
+ if (prefix !== null && spec !== null && typeof spec === "object" && !Array.isArray(spec)) {
8231
+ const sequence = Number(prefix[1]);
8232
+ if (!Number.isSafeInteger(sequence)) throw new TypeError(`${name$2}: unsafe config sequence in ${fileName}`);
8233
+ Object.defineProperty(spec, FILE_SEQUENCE, { value: sequence });
8234
+ }
8235
+ specs.push(spec);
8236
+ }
8237
+ for (const spec of specs) {
8238
+ if (spec === null || typeof spec !== "object" || Array.isArray(spec)) continue;
8239
+ const own = spec.variables !== null && typeof spec.variables === "object" && !Array.isArray(spec.variables) ? spec.variables : {};
8240
+ spec.variables = {
8241
+ ...presetVariables,
8242
+ ...own
8243
+ };
8244
+ }
8245
+ return specs;
8246
+ }
8172
8247
  const KNOWN_STRATEGIES = /* @__PURE__ */ new Set([
8173
8248
  "static",
8174
8249
  "placeholder",
@@ -8860,6 +8935,9 @@ function createPromptConfigs(specs, options = {}) {
8860
8935
  templatePatch,
8861
8936
  params
8862
8937
  };
8938
+ const sequence = (Object.hasOwn(options.configOrder ?? {}, spec.id) ? options.configOrder[spec.id] : void 0) ?? spec[FILE_SEQUENCE] ?? (typeof options.sourceModuleId === "string" ? index * 10 : void 0);
8939
+ if (sequence !== void 0) config.sequence = sequence;
8940
+ if (typeof options.sourceModuleId === "string") config.sourceModuleId = options.sourceModuleId;
8863
8941
  config.matchScan = match === void 0 ? void 0 : createAnchorMatcher(match).scan;
8864
8942
  config.resolve = bindResolver(config, options.strategyDir);
8865
8943
  return config;
@@ -8867,6 +8945,7 @@ function createPromptConfigs(specs, options = {}) {
8867
8945
  config,
8868
8946
  fileOrder
8869
8947
  })).sort((a, b) => {
8948
+ if (a.config.sequence !== void 0 || b.config.sequence !== void 0) return compareConfigSequence(a.config, b.config) || a.fileOrder - b.fileOrder;
8870
8949
  const aAnchor = a.config.configKind === "anchor" ? 0 : 1;
8871
8950
  const bAnchor = b.config.configKind === "anchor" ? 0 : 1;
8872
8951
  if (aAnchor !== bAnchor) return aAnchor - bAnchor;
@@ -8933,6 +9012,10 @@ function modelRequestConfigs(params) {
8933
9012
  params: { patch: mainPatch }
8934
9013
  });
8935
9014
  const subagentPatch = patchOf("subagent");
9015
+ if (typeof params.subagentModelProvider === "string" && params.subagentModelProvider.trim().length > 0 && typeof params.subagentModelName === "string" && params.subagentModelName.trim().length > 0) {
9016
+ subagentPatch.provider = params.subagentModelProvider.trim();
9017
+ subagentPatch.model = params.subagentModelName.trim();
9018
+ }
8936
9019
  if (Object.keys(subagentPatch).length > 0) configs.push({
8937
9020
  id: "subagent-model-params",
8938
9021
  name: "模型参数(子代理)",
@@ -9079,6 +9162,112 @@ function listPromptConfigSpecs(dir) {
9079
9162
  return specs;
9080
9163
  }
9081
9164
  //#endregion
9165
+ //#region src/host/legacy-prompt-params.ts
9166
+ /** 旧层参数到规则实例的唯一兼容投影;读取、导入物化与旧 API 保存共用。 */
9167
+ const isRecord$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9168
+ const text = (value) => value === void 0 || value === null ? "" : String(value);
9169
+ function readLegacyPromptParams(source) {
9170
+ const params = {};
9171
+ if (!isRecord$3(source.layerSettings)) return params;
9172
+ for (const [layer, settings] of Object.entries(source.layerSettings)) {
9173
+ if (!isRecord$3(settings)) continue;
9174
+ for (const [key, value] of Object.entries(settings)) {
9175
+ if (!isLegacyPromptParam(key)) continue;
9176
+ if (layer !== "pre-step") throw new PresetLayerSettingsError(`${key} 必须位于 layerSettings.pre-step`);
9177
+ params[key] = value === "on" ? true : value === "off" ? false : value;
9178
+ }
9179
+ }
9180
+ return params;
9181
+ }
9182
+ function legacyBody(source, moduleDir, supplied) {
9183
+ if (supplied !== void 0) return supplied;
9184
+ if (moduleDir !== void 0) try {
9185
+ const file = join(moduleDir, "preset.md");
9186
+ if (lstatSync(file).isSymbolicLink()) throw new Error(`旧模块正文不能是链接:${file}`);
9187
+ return readFileSync(file, "utf8");
9188
+ } catch (error) {
9189
+ if (error.code !== "ENOENT") throw error;
9190
+ }
9191
+ return text(source.content?.presetText);
9192
+ }
9193
+ function resolveLegacyPromptConfigs(source, options = {}) {
9194
+ const params = readLegacyPromptParams(source);
9195
+ for (const [key, value] of Object.entries(options.overrides ?? {})) if (isLegacyPromptParam(key) && value !== void 0 && value !== null) params[key] = value;
9196
+ const keys = LEGACY_PROMPT_PARAM_KEYS.filter((key) => Object.hasOwn(params, key));
9197
+ const applies = (id) => keys.some((key) => LEGACY_PROMPT_PARAM_DEFINITIONS[key].targets.includes(id));
9198
+ const provided = (key) => options.partial !== true || Object.hasOwn(params, key);
9199
+ const projected = (source.promptConfigs ?? []).map((config) => {
9200
+ if (!isRecord$3(config) || !applies(config.id)) return config;
9201
+ const next = {
9202
+ ...config,
9203
+ params: { ...config.params }
9204
+ };
9205
+ if (config.id === "near-anchor") {
9206
+ if (provided("firstTurnAnchor")) next.enabled = params.firstTurnAnchor === true;
9207
+ if (provided("firstTurnCustom")) next.params.useCustom = params.firstTurnCustom === true;
9208
+ if (provided("firstTurnText")) next.params.text = text(params.firstTurnText);
9209
+ for (const key of [
9210
+ "buildPattern",
9211
+ "complexPattern",
9212
+ "firstTurnBuild",
9213
+ "firstTurnInspect",
9214
+ "firstTurnDeep"
9215
+ ]) if (provided(key)) next.params[key] = text(params[key]);
9216
+ } else if (config.id === "router-guide") {
9217
+ if (provided("guideEnabled")) next.enabled = params.guideEnabled === true;
9218
+ if (provided("guideCustom") || provided("guideEnabled")) {
9219
+ const useCustom = provided("guideCustom") ? params.guideCustom === true : next.params.useCustom === true;
9220
+ next.params.useCustom = useCustom;
9221
+ next.modelScope = useCustom ? "all" : "flash";
9222
+ }
9223
+ if (provided("guideText")) next.params.text = text(params.guideText);
9224
+ for (const key of [
9225
+ "complexPattern",
9226
+ "guideWeak",
9227
+ "guideDeep"
9228
+ ]) if (provided(key)) next.params[key] = text(params[key]);
9229
+ }
9230
+ return next;
9231
+ });
9232
+ const configs = mergePromptConfigs(projected, options.promptConfigs).map((config) => {
9233
+ if (config.id !== "prompt-injector" || !applies(config.id)) return config;
9234
+ const body = legacyBody(source, options.moduleDir, options.prompt);
9235
+ const explicitWord = text(options.partial === true && Object.hasOwn(params, "firstTurnWord") ? params.firstTurnWord : config.params?.firstTurnWord ?? params.firstTurnWord);
9236
+ const anchor = projected.find((entry) => entry.id === "near-anchor");
9237
+ const signalWord = (value) => {
9238
+ const sentence = /the exact sentence:\s*([A-Za-z]+)/i.exec(value);
9239
+ const first = /^\P{L}*([\p{L}]+)/u.exec(value.trim());
9240
+ return (sentence?.[1] ?? first?.[1])?.toLowerCase();
9241
+ };
9242
+ const derivedWords = [...new Set([
9243
+ "firstTurnText",
9244
+ "firstTurnBuild",
9245
+ "firstTurnInspect",
9246
+ "firstTurnDeep"
9247
+ ].map((key) => signalWord(text(Object.hasOwn(params, key) || options.partial !== true ? params[key] : anchor?.params?.[key === "firstTurnText" ? "text" : key]))).filter((word) => word !== void 0))];
9248
+ const { text: _text, texts: _texts, ...rest } = config;
9249
+ return {
9250
+ ...rest,
9251
+ enabled: body.trim().length > 0 && (provided("injectPrompt") ? params.injectPrompt !== false : config.enabled !== false),
9252
+ params: {
9253
+ ...config.params,
9254
+ text: body,
9255
+ firstTurnWord: explicitWord,
9256
+ anchorWords: explicitWord.length > 0 ? [explicitWord] : derivedWords
9257
+ }
9258
+ };
9259
+ });
9260
+ const ids = new Set(configs.map((config) => config.id));
9261
+ const consumedKeys = keys.filter((key) => LEGACY_PROMPT_PARAM_DEFINITIONS[key].targets.some((id) => ids.has(id)));
9262
+ const unhandled = keys.filter((key) => !consumedKeys.includes(key));
9263
+ return {
9264
+ configs,
9265
+ consumedKeys,
9266
+ active: keys.length > 0,
9267
+ warnings: unhandled.length === 0 ? [] : [`旧规则参数没有承接配置,原值保留:${unhandled.join(", ")}`]
9268
+ };
9269
+ }
9270
+ //#endregion
9082
9271
  //#region src/host/text-file.ts
9083
9272
  /** 共用原子文本替换;来源授权与内容合法性由各领域调用方负责。 */
9084
9273
  function atomicWriteTextFile(file, content, options = {}) {
@@ -9099,24 +9288,38 @@ function atomicWriteTextFile(file, content, options = {}) {
9099
9288
  //#endregion
9100
9289
  //#region src/host/manifest.ts
9101
9290
  /**
9102
- * manifest — 预设模板单一参数 YAML(preset.yml)的加载与引擎参数解析。
9291
+ * manifest — 模块模板单一参数 YAML(module.yml)的加载与引擎参数解析。
9103
9292
  *
9104
- * 一个预设 = 一个 preset.yml:
9293
+ * 一个模块 = 一个 module.yml:
9105
9294
  * - modules/layerSettings/content/meta 全部是直读参数,无模板语法;
9106
9295
  * - layerSettings 展平为内部 params,供引擎生成默认提示词配置,promptConfigs 仅为可选覆盖;
9107
9296
  * - 组合模块的行级 config 由参数桥 buildModuleConfigsFromParams 按 params
9108
9297
  * 构造对象合并(取代旧 __TOKEN__ 文本渲染,无占位符、无文本往返),
9109
9298
  * params(UI/基础层)优先于 moduleConfigs 行级直写(旧作者锁定语义已移除)。
9110
- * 本模块负责参数归一化与引擎模块配置装配;所有预设专属行为都在引擎内部。
9299
+ * 本模块负责参数归一化与引擎模块配置装配;所有模块专属行为都在引擎内部。
9300
+ */
9301
+ /**
9302
+ * 模块目录确实存在于本插件存储根(身份判定与官方登记状态无关)。
9303
+ *
9304
+ * 启用表过滤、工具写入目标、TUI 目标共用这一条判据:本插件管理的模块 = 存储根里含
9305
+ * `module.yml` 的目录,与「宿主有没有把它登记成官方预设」是两回事。
9306
+ */
9307
+ function moduleDirExists(root, id) {
9308
+ return statSync(join(root, id, MODULE_DEFINITION_FILE), { throwIfNoEntry: false }) !== void 0;
9309
+ }
9310
+ /**
9311
+ * 包根 `modules/` 目录:随包分发的**内置模块库**(模块目录形态与存储根同构)。
9312
+ *
9313
+ * 兼容源码运行(`src/host/`)与打包运行(`lib/`)。旧的内置预设目录 `preset/` 已整体
9314
+ * 退场——那些是官方预设形态的遗留,重构后不再读取也不再维护。
9111
9315
  */
9112
- /** 包根 preset/ 目录(配置/模板文件夹):兼容源码运行(src/host)与打包运行(lib/)。 */
9113
- function packagePresetDir() {
9114
- const candidates = [new URL("../preset/", import.meta.url), new URL("../../preset/", import.meta.url)];
9316
+ function packageModulesDir() {
9317
+ const candidates = [new URL("../modules/", import.meta.url), new URL("../../modules/", import.meta.url)];
9115
9318
  for (const candidate of candidates) {
9116
9319
  const dir = fileURLToPath(candidate);
9117
9320
  if (existsSync(dir)) return dir;
9118
9321
  }
9119
- throw new Error("prompt-tool: cannot locate package preset/ directory");
9322
+ throw new Error("prompt-tool: cannot locate package modules/ directory");
9120
9323
  }
9121
9324
  /** 包根 engine/ 目录(插件引擎,与配置文件夹分离):兼容源码与打包运行。 */
9122
9325
  function packageEngineDir() {
@@ -9129,20 +9332,20 @@ function packageEngineDir() {
9129
9332
  }
9130
9333
  /**
9131
9334
  * preset.yml 读缓存:按 mtime+size 签名失效。load()/describe/param-overrides 等
9132
- * 每次请求读盘解析,加缓存后同一文件只解析一次;写盘路径(savePresetParams /
9335
+ * 每次请求读盘解析,加缓存后同一文件只解析一次;写盘路径(saveModuleParams /
9133
9336
  * withPresetDoc)与外部编辑(stat 签名变化)都会正确失效。
9134
9337
  */
9135
9338
  const presetSpecCache = /* @__PURE__ */ new Map();
9136
9339
  /** 写盘后失效缓存(调用方在写完 preset.yml 后调用;不调用也安全——stat 签名兜底)。 */
9137
- function invalidatePresetSpec(dir) {
9138
- presetSpecCache.delete(join(dir, "preset.yml"));
9340
+ function invalidateModuleSpec(dir) {
9341
+ presetSpecCache.delete(join(dir, MODULE_DEFINITION_FILE));
9139
9342
  }
9140
9343
  /**
9141
- * 从预设 preset.yml 的 modules 清单移除一个模块 id(YAML Document 保留注释
9344
+ * 从模块 module.yml 的 modules 清单移除一个模块 id(YAML Document 保留注释
9142
9345
  * 与其余字段)。返回是否发生修改;文件缺失或没有该模块时返回 false。
9143
9346
  */
9144
9347
  function removePresetModule(dir, moduleId) {
9145
- const file = join(dir, "preset.yml");
9348
+ const file = join(dir, MODULE_DEFINITION_FILE);
9146
9349
  if (!existsSync(file)) return false;
9147
9350
  const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
9148
9351
  if (doc.errors.length > 0) return false;
@@ -9153,12 +9356,21 @@ function removePresetModule(dir, moduleId) {
9153
9356
  if (kept.length === modules.items.length) return false;
9154
9357
  modules.items = kept;
9155
9358
  atomicWriteTextFile(file, doc.toString());
9156
- invalidatePresetSpec(dir);
9359
+ invalidateModuleSpec(dir);
9157
9360
  return true;
9158
9361
  }
9159
- /** 加载某个预设模板的单一参数文件 preset/<name>/preset.yml。 */
9160
- function loadPresetSpec(dir) {
9161
- const file = join(dir, "preset.yml");
9362
+ /** 加载某个模块模板的单一参数文件 modules/<name>/module.yml。 */
9363
+ function loadModuleSpec(dir, options = {}) {
9364
+ const project = (spec) => {
9365
+ if (options.legacyParams === false) return spec;
9366
+ const legacy = resolveLegacyPromptConfigs(spec, { moduleDir: dir });
9367
+ return legacy.active ? {
9368
+ ...spec,
9369
+ ...Array.isArray(spec.promptConfigs) ? { promptConfigs: legacy.configs } : {},
9370
+ legacyParamWarnings: legacy.warnings
9371
+ } : spec;
9372
+ };
9373
+ const file = join(dir, MODULE_DEFINITION_FILE);
9162
9374
  let stat;
9163
9375
  try {
9164
9376
  stat = statSync(file);
@@ -9166,7 +9378,7 @@ function loadPresetSpec(dir) {
9166
9378
  throw new Error(`preset.yml not found in ${dir}(预设模板不存在或目录不完整)`);
9167
9379
  }
9168
9380
  const cached = presetSpecCache.get(file);
9169
- if (cached !== void 0 && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.spec;
9381
+ if (cached !== void 0 && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return project(cached.spec);
9170
9382
  const raw = readFileSync(file, "utf8");
9171
9383
  let parsed;
9172
9384
  try {
@@ -9174,58 +9386,43 @@ function loadPresetSpec(dir) {
9174
9386
  } catch (error) {
9175
9387
  throw new Error(`preset ${file} YAML 解析失败: ${String(error.message ?? error)}`);
9176
9388
  }
9177
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`preset ${join(dir, "preset.yml")} is not a YAML map`);
9389
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`preset ${join(dir, MODULE_DEFINITION_FILE)} is not a YAML map`);
9178
9390
  if (typeof parsed.id !== "string" || parsed.id.length === 0) parsed.id = basename(dir);
9179
9391
  const params = readPresetLayerSettings(parsed);
9392
+ readLegacyPromptParams(parsed);
9180
9393
  if (parsed.layerSettings !== void 0 || parsed.params !== void 0) parsed.params = params;
9181
9394
  presetSpecCache.set(file, {
9182
9395
  mtimeMs: stat.mtimeMs,
9183
9396
  size: stat.size,
9184
9397
  spec: parsed
9185
9398
  });
9186
- return parsed;
9187
- }
9188
- /** 读取预设模板内容资产(presetText / agentsText);模板缺失时静默降级。
9189
- * 模板目录按 resolvePresetDir 解析(用户自定义预设优先,包内模板回退)。 */
9190
- function loadPresetContent(template = DEFAULT_PRESET_ID, presetRoot = userPresetsDir()) {
9191
- try {
9192
- const spec = loadPresetSpec(resolvePresetDir(template, presetRoot));
9193
- return {
9194
- presetText: typeof spec.content?.presetText === "string" ? spec.content.presetText : "",
9195
- agentsText: typeof spec.content?.agentsText === "string" ? spec.content.agentsText : ""
9196
- };
9197
- } catch {
9198
- return {
9199
- presetText: "",
9200
- agentsText: ""
9201
- };
9202
- }
9399
+ return project(parsed);
9203
9400
  }
9204
9401
  const asString = (value, fallback = "") => {
9205
9402
  if (typeof value === "string") return value;
9206
9403
  if (value === void 0 || value === null) return fallback;
9207
9404
  return String(value);
9208
9405
  };
9209
- /** 预设根:官方 USER_PRESET_DIR(~/.dsh/.agent-presets),导入/新建/种子化的预设都放这里。 */
9210
- function userPresetsDir() {
9211
- return DEFAULT_PRESET_DIR;
9406
+ /** 模块根:本插件的存储根(~/.dsh/.prompt-tool),导入/新建/种子化的模块都放这里。 */
9407
+ function userModulesDir() {
9408
+ return MODULES_DIR;
9212
9409
  }
9213
9410
  /** 只按合法目录身份定位;现存坏身份必须报错,不能绕到同名模板。 */
9214
9411
  function findPresetDir(scanDir, template) {
9215
- const exact = assertPresetDirectory(scanDir, template, true);
9412
+ const exact = assertModuleDirectory(scanDir, template, true);
9216
9413
  return presetPathExists(exact) ? exact : void 0;
9217
9414
  }
9218
9415
  /**
9219
- * 解析预设模板目录:当前预设根优先,包内模板回退;不读取其他部署根的同名预设。
9416
+ * 解析模块模板目录:当前模块根优先,包内模板回退;不读取其他部署根的同名模块。
9220
9417
  * 目录名与声明的 preset.yml id 一致;不扫描其他目录的 id 别名。
9221
9418
  */
9222
- function resolvePresetDir(template, presetRoot = userPresetsDir()) {
9223
- return findPresetDir(presetRoot, template) ?? findPresetDir(packagePresetDir(), template) ?? join(packagePresetDir(), template);
9419
+ function resolveModuleDir(template, moduleRoot = userModulesDir()) {
9420
+ return findPresetDir(moduleRoot, template) ?? findPresetDir(packageModulesDir(), template) ?? join(packageModulesDir(), template);
9224
9421
  }
9225
- /** 预设目录是否含组合源:modules、composition 或官方 agent.cordis.yml。 */
9422
+ /** 模块目录是否含组合源:modules、composition 或官方 agent.cordis.yml。 */
9226
9423
  function isRenderablePresetDir(dir) {
9227
9424
  try {
9228
- const spec = loadPresetSpec(dir);
9425
+ const spec = loadModuleSpec(dir);
9229
9426
  if (Array.isArray(spec.modules)) return true;
9230
9427
  if (typeof spec.composition === "string" && spec.composition.length > 0) return true;
9231
9428
  } catch (error) {
@@ -9234,13 +9431,13 @@ function isRenderablePresetDir(dir) {
9234
9431
  }
9235
9432
  return existsSync(join(dir, "agent.cordis.yml"));
9236
9433
  }
9237
- /** 本插件预设清单;所有目录按同一身份与组合源规则列举。 */
9238
- function listPresets(presetRoot = userPresetsDir()) {
9434
+ /** 本插件模块清单;所有目录按同一身份与组合源规则列举。 */
9435
+ function listModules(moduleRoot = userModulesDir()) {
9239
9436
  const scan = (dir) => {
9240
9437
  try {
9241
9438
  return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^[a-z0-9][a-z0-9-]*$/.test(entry.name)).flatMap((entry) => {
9242
9439
  try {
9243
- const spec = loadPresetSpec(assertPresetDirectory(dir, entry.name));
9440
+ const spec = loadModuleSpec(assertModuleDirectory(dir, entry.name));
9244
9441
  if (typeof spec.id !== "string" || spec.id.length === 0) return [];
9245
9442
  return [{
9246
9443
  id: entry.name,
@@ -9259,15 +9456,15 @@ function listPresets(presetRoot = userPresetsDir()) {
9259
9456
  }
9260
9457
  };
9261
9458
  const byId = /* @__PURE__ */ new Map();
9262
- for (const preset of scan(presetRoot)) byId.set(preset.id, preset);
9459
+ for (const preset of scan(moduleRoot)) byId.set(preset.id, preset);
9263
9460
  return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
9264
9461
  }
9265
- /** 插件目录模板清单(「新建」选择器与首次种子化数据源;不直接出现在预设列表)。 */
9462
+ /** 插件目录模板清单(「新建」选择器与首次种子化数据源;不直接出现在模块列表)。 */
9266
9463
  function listBuiltinTemplates() {
9267
9464
  try {
9268
- return readdirSync(packagePresetDir(), { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => entry.name !== "pt-custom").flatMap((entry) => {
9465
+ return readdirSync(packageModulesDir(), { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
9269
9466
  try {
9270
- const spec = loadPresetSpec(join(packagePresetDir(), entry.name));
9467
+ const spec = loadModuleSpec(join(packageModulesDir(), entry.name));
9271
9468
  return [{
9272
9469
  id: entry.name,
9273
9470
  name: spec.name
@@ -9280,16 +9477,16 @@ function listBuiltinTemplates() {
9280
9477
  return [];
9281
9478
  }
9282
9479
  }
9283
- /** 按包内同名目录补建缺失预设;已有目录的定义、生成物和资源保持原样。 */
9284
- function ensurePresetSeed(root = userPresetsDir()) {
9480
+ /** 按包内同名目录补建缺失的内置模块;已有目录的定义、生成物和资源保持原样。 */
9481
+ function ensurePresetSeed(root = userModulesDir()) {
9285
9482
  const created = [];
9286
9483
  try {
9287
9484
  mkdirSync(root, { recursive: true });
9288
- for (const entry of readdirSync(packagePresetDir(), { withFileTypes: true })) {
9485
+ for (const entry of readdirSync(packageModulesDir(), { withFileTypes: true })) {
9289
9486
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
9290
9487
  const target = join(root, entry.name);
9291
9488
  if (existsSync(target)) continue;
9292
- cpSync(join(packagePresetDir(), entry.name), target, { recursive: true });
9489
+ cpSync(join(packageModulesDir(), entry.name), target, { recursive: true });
9293
9490
  created.push(entry.name);
9294
9491
  }
9295
9492
  } catch {}
@@ -9298,14 +9495,14 @@ function ensurePresetSeed(root = userPresetsDir()) {
9298
9495
  /** 完整复制到隐藏候选;只更新定义 ID 和已知共享引擎配置位置。 */
9299
9496
  function copyPresetDirectory(source, root, targetId) {
9300
9497
  assertPresetTree(source);
9301
- loadPresetSpec(source);
9302
- const target = assertPresetDirectory(root, targetId, true);
9498
+ loadModuleSpec(source);
9499
+ const target = assertModuleDirectory(root, targetId, true);
9303
9500
  if (presetPathExists(target)) throw new Error(`目标预设已存在:${targetId}`);
9304
9501
  mkdirSync(root, { recursive: true });
9305
9502
  const candidate = mkdtempSync(join(root, `.${targetId}.copy-`));
9306
9503
  try {
9307
9504
  cpSync(source, candidate, { recursive: true });
9308
- const definition = join(candidate, "preset.yml");
9505
+ const definition = join(candidate, MODULE_DEFINITION_FILE);
9309
9506
  const doc = parseDocument(readFileSync(definition, "utf8"), { logLevel: "silent" });
9310
9507
  if (doc.errors.length > 0 || !(doc.contents instanceof YAMLMap)) throw new Error("预设定义必须是合法 YAML 对象");
9311
9508
  if (doc.get("id") !== targetId) {
@@ -9340,17 +9537,17 @@ function copyPresetDirectory(source, root, targetId) {
9340
9537
  });
9341
9538
  }
9342
9539
  }
9343
- /** 从包内同名目录复制预设;autoSuffix=true 时递增目录名并同步定义身份。 */
9344
- function cloneBuiltinPreset(id, autoSuffix = false, presetRoot = userPresetsDir()) {
9540
+ /** 从包内同名目录复制模块;autoSuffix=true 时递增目录名并同步定义身份。 */
9541
+ function cloneBuiltinPreset(id, autoSuffix = false, moduleRoot = userModulesDir()) {
9345
9542
  try {
9346
9543
  assertPresetId(id);
9347
- const builtin = findPresetDir(packagePresetDir(), id);
9544
+ const builtin = findPresetDir(packageModulesDir(), id);
9348
9545
  if (builtin === void 0) return {
9349
9546
  ok: false,
9350
9547
  message: `预设 ${id} 不是包内置预设`
9351
9548
  };
9352
9549
  let targetId = id;
9353
- let target = join(presetRoot, targetId);
9550
+ let target = join(moduleRoot, targetId);
9354
9551
  if (presetPathExists(target)) {
9355
9552
  if (!autoSuffix) return {
9356
9553
  ok: false,
@@ -9358,11 +9555,11 @@ function cloneBuiltinPreset(id, autoSuffix = false, presetRoot = userPresetsDir(
9358
9555
  };
9359
9556
  for (let suffix = 2;; suffix++) {
9360
9557
  targetId = `${id}-${suffix}`;
9361
- target = join(presetRoot, targetId);
9558
+ target = join(moduleRoot, targetId);
9362
9559
  if (!presetPathExists(target)) break;
9363
9560
  }
9364
9561
  }
9365
- copyPresetDirectory(builtin, presetRoot, targetId);
9562
+ copyPresetDirectory(builtin, moduleRoot, targetId);
9366
9563
  return {
9367
9564
  ok: true,
9368
9565
  id: targetId
@@ -9374,14 +9571,14 @@ function cloneBuiltinPreset(id, autoSuffix = false, presetRoot = userPresetsDir(
9374
9571
  };
9375
9572
  }
9376
9573
  }
9377
- /** 复制用户预设目录为新预设(id 自动递增:<id>-copy / <id>-copy-2 / …)。
9574
+ /** 复制用户模块目录为新模块(id 自动递增:<id>-copy / <id>-copy-2 / …)。
9378
9575
  * 复制的是用户目录完整副本(preset.yml / agent.cordis.yml / prompt-configs /
9379
9576
  * 内容资产 / 覆盖文件),与「从内置模板新建」互补:后者还原模板,前者备份现状。 */
9380
- function duplicateUserPreset(id, presetRoot = userPresetsDir()) {
9577
+ function duplicateUserModule(id, moduleRoot = userModulesDir()) {
9381
9578
  try {
9382
9579
  assertPresetId(id);
9383
- const root = resolve(presetRoot);
9384
- const source = assertPresetDirectory(root, id);
9580
+ const root = resolve(moduleRoot);
9581
+ const source = assertModuleDirectory(root, id);
9385
9582
  let targetId = `${id}-copy`;
9386
9583
  let target = join(root, targetId);
9387
9584
  for (let suffix = 2; presetPathExists(target); suffix++) {
@@ -9400,8 +9597,8 @@ function duplicateUserPreset(id, presetRoot = userPresetsDir()) {
9400
9597
  };
9401
9598
  }
9402
9599
  }
9403
- /** 在系统文件管理器中打开预设目录(尽力而为:无桌面环境时打开失败也返回路径供 UI 展示)。 */
9404
- function openPresetLocation(id, presetRoot = userPresetsDir()) {
9600
+ /** 在系统文件管理器中打开模块目录(尽力而为:无桌面环境时打开失败也返回路径供 UI 展示)。 */
9601
+ function openModuleLocation(id, moduleRoot = userModulesDir()) {
9405
9602
  const isBareId = typeof id === "string" && /^[a-z0-9][a-z0-9-]*$/.test(id);
9406
9603
  const isCardPath = typeof id === "string" && /^\/\.[a-zA-Z0-9_-]+\/[^/\\]+$/.test(id);
9407
9604
  if (!isBareId && !isCardPath) return {
@@ -9409,8 +9606,8 @@ function openPresetLocation(id, presetRoot = userPresetsDir()) {
9409
9606
  message: `非法预设 id:${id}`,
9410
9607
  path: ""
9411
9608
  };
9412
- const dir = resolve(join(presetRoot, id));
9413
- const rootResolved = resolve(presetRoot);
9609
+ const dir = resolve(join(moduleRoot, id));
9610
+ const rootResolved = resolve(moduleRoot);
9414
9611
  if (dir !== rootResolved && !dir.startsWith(rootResolved + sep)) return {
9415
9612
  ok: false,
9416
9613
  message: `预设路径越界:${id}`,
@@ -9422,7 +9619,7 @@ function openPresetLocation(id, presetRoot = userPresetsDir()) {
9422
9619
  path: dir
9423
9620
  };
9424
9621
  try {
9425
- if (isBareId) assertPresetDirectory(presetRoot, id);
9622
+ if (isBareId) assertModuleDirectory(moduleRoot, id);
9426
9623
  const command = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
9427
9624
  spawn(command, [dir], {
9428
9625
  detached: true,
@@ -9445,24 +9642,41 @@ function deleteYamlPath(doc, path) {
9445
9642
  if (doc.hasIn(path)) doc.deleteIn(path);
9446
9643
  }
9447
9644
  /**
9448
- * 保存预设参数:写 layerSettings(merge)/ promptConfigs(整体替换)。
9645
+ * 保存模块参数:写 layerSettings(merge)/ promptConfigs(整体替换)。
9449
9646
  * parseDocument 保留注释与未知键(preset.yml 模板含大量注释);空值键删除('' / [],
9450
9647
  * 回落模板/引擎默认;0 与 false 照常写入——语义与函数内注释、docs §3 一致)。
9451
9648
  */
9452
- function savePresetParams(presetRoot, templateName, params, promptConfigs, variables, variablesEnabled) {
9453
- const file = join(assertPresetDirectory(presetRoot, templateName), "preset.yml");
9649
+ function saveModuleParams(moduleRoot, templateName, params, promptConfigs, variables, variablesEnabled) {
9650
+ const file = join(assertModuleDirectory(moduleRoot, templateName), MODULE_DEFINITION_FILE);
9454
9651
  if (!existsSync(file)) throw new Error(`preset ${templateName} 无 preset.yml`);
9455
9652
  const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
9456
9653
  readPresetLayerSettings(doc.toJS());
9654
+ const previousLegacy = readLegacyPromptParams(doc.toJS());
9655
+ const legacyOverrides = {};
9457
9656
  if (params !== void 0) for (const [key, value] of Object.entries(params)) {
9458
9657
  if (value === void 0 || value === null || key.trim().length === 0) continue;
9459
9658
  const isEmpty = value === "" || Array.isArray(value) && value.length === 0;
9460
- const path = engineParamPath(key);
9659
+ if (isLegacyPromptParam(key)) legacyOverrides[key] = value;
9660
+ const path = isLegacyPromptParam(key) ? legacyPromptParamPath(key) : engineParamPath(key);
9461
9661
  if (isEmpty) deleteYamlPath(doc, path);
9462
9662
  else doc.setIn(path, value);
9463
9663
  }
9464
- if (promptConfigs !== void 0) {
9465
- const cleaned = promptConfigs.map((config) => {
9664
+ const source = doc.toJS();
9665
+ const legacy = resolveLegacyPromptConfigs(source, {
9666
+ moduleDir: join(moduleRoot, templateName),
9667
+ overrides: legacyOverrides,
9668
+ partial: Object.keys(previousLegacy).length === 0
9669
+ });
9670
+ const legacyWrite = Object.keys(legacyOverrides).length > 0 && legacy.consumedKeys.length > 0;
9671
+ if (promptConfigs !== void 0 || legacyWrite) {
9672
+ const cleaned = (promptConfigs === void 0 ? legacy.configs : Object.keys(legacyOverrides).length === 0 ? promptConfigs : resolveLegacyPromptConfigs({
9673
+ ...source,
9674
+ promptConfigs
9675
+ }, {
9676
+ moduleDir: join(moduleRoot, templateName),
9677
+ overrides: legacyOverrides,
9678
+ partial: Object.keys(previousLegacy).length === 0
9679
+ }).configs).map((config) => {
9466
9680
  if (config === null || typeof config !== "object" || Array.isArray(config)) return config;
9467
9681
  const vars = config.variables;
9468
9682
  if (vars === null || typeof vars !== "object" || Array.isArray(vars)) return config;
@@ -9473,7 +9687,16 @@ function savePresetParams(presetRoot, templateName, params, promptConfigs, varia
9473
9687
  return next;
9474
9688
  });
9475
9689
  doc.setIn(["promptConfigs"], cleaned);
9476
- }
9690
+ const accepted = resolveLegacyPromptConfigs({
9691
+ ...source,
9692
+ promptConfigs: cleaned
9693
+ }, {
9694
+ moduleDir: join(moduleRoot, templateName),
9695
+ overrides: legacyOverrides
9696
+ });
9697
+ for (const key of accepted.consumedKeys) deleteYamlPath(doc, legacyPromptParamPath(key));
9698
+ for (const warning of accepted.warnings) console.warn(`prompt-tool: ${warning}`);
9699
+ } else for (const warning of legacy.warnings) console.warn(`prompt-tool: ${warning}`);
9477
9700
  if (variables !== void 0) {
9478
9701
  const kept = Object.fromEntries(Object.entries(variables).filter(([key, value]) => key.trim().length > 0 && typeof value === "string"));
9479
9702
  if (Object.keys(kept).length > 0) doc.setIn(["variables"], kept);
@@ -9484,34 +9707,34 @@ function savePresetParams(presetRoot, templateName, params, promptConfigs, varia
9484
9707
  else doc.setIn(["variablesEnabled"], false);
9485
9708
  }
9486
9709
  atomicWriteTextFile(file, doc.toString());
9487
- invalidatePresetSpec(join(presetRoot, templateName));
9710
+ invalidateModuleSpec(join(moduleRoot, templateName));
9488
9711
  }
9489
9712
  /**
9490
- * 保存激活预设的顶层 persona 段(官方 `@deepseek-ai/dsh-persona` 行 config 同构)。
9713
+ * 保存激活模块的顶层 persona 段(官方 `@deepseek-ai/dsh-persona` 行 config 同构)。
9491
9714
  * null = 删除该段(回落宿主部署人设);默认值不落键(见 personaRowConfig)。
9492
9715
  */
9493
- function savePresetPersona(presetRoot, templateName, persona) {
9494
- const file = join(assertPresetDirectory(presetRoot, templateName), "preset.yml");
9716
+ function savePresetPersona(moduleRoot, templateName, persona) {
9717
+ const file = join(assertModuleDirectory(moduleRoot, templateName), MODULE_DEFINITION_FILE);
9495
9718
  if (!existsSync(file)) throw new Error(`preset ${templateName} 无 preset.yml`);
9496
9719
  const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
9497
9720
  readPresetLayerSettings(doc.toJS());
9498
9721
  if (persona === null) doc.deleteIn(["persona"]);
9499
9722
  else doc.setIn(["persona"], personaRowConfig(persona));
9500
9723
  atomicWriteTextFile(file, doc.toString());
9501
- invalidatePresetSpec(join(presetRoot, templateName));
9724
+ invalidateModuleSpec(join(moduleRoot, templateName));
9502
9725
  }
9503
- /** 预设文件读-改-写(parseDocument 保留注释与未知键;mutate 内 setIn/deleteIn)。
9726
+ /** 模块文件读-改-写(parseDocument 保留注释与未知键;mutate 内 setIn/deleteIn)。
9504
9727
  * 角色卡库(characters)与世界书工具(world-book-tools)共用此入口,避免
9505
9728
  * 各自实现 parseDocument 往返。写盘走原子替换,失败保留旧文件。 */
9506
- function withPresetDoc(presetDir, mutate) {
9507
- const file = join(presetDir, "preset.yml");
9508
- if (!existsSync(file)) throw new Error(`${presetDir} 无 preset.yml`);
9729
+ function withPresetDoc(moduleDir, mutate) {
9730
+ const file = join(moduleDir, MODULE_DEFINITION_FILE);
9731
+ if (!existsSync(file)) throw new Error(`${moduleDir} 无 preset.yml`);
9509
9732
  const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
9510
9733
  readPresetLayerSettings(doc.toJS());
9511
9734
  mutate(doc);
9512
9735
  readPresetLayerSettings(doc.toJS());
9513
9736
  atomicWriteTextFile(file, doc.toString());
9514
- invalidatePresetSpec(presetDir);
9737
+ invalidateModuleSpec(moduleDir);
9515
9738
  }
9516
9739
  /** 向 preset.yml 的 modules 追加功能模块;空数组保持按需装配语义。 */
9517
9740
  function appendPresetModules(doc, additions) {
@@ -9525,18 +9748,18 @@ function appendPresetModules(doc, additions) {
9525
9748
  }
9526
9749
  doc.set("modules", modules);
9527
9750
  }
9528
- /** 删除具有合法身份的预设目录(预设根/<id>);隐藏备份不经公共接口删除。
9529
- * 仅作用于预设根(官方 USER_PRESET_DIR),包内置模板天然不受影响;路径越界与非法 id 拒绝。
9530
- * 删除后宿主 agent-presets 目录列表自然不再出现该预设(官方 roster 即目录列表)。 */
9531
- function removeUserPreset(id, presetRoot = userPresetsDir()) {
9751
+ /** 删除具有合法身份的模块目录(模块根/<id>);隐藏备份不经公共接口删除。
9752
+ * 仅作用于插件自有模块根,包内置模板不受影响;路径越界与非法 id 拒绝。
9753
+ * 模块不在宿主预设注册表里,删除后无需撤销官方登记;调用方按需重建当前模块。 */
9754
+ function removeUserPreset(id, moduleRoot = userModulesDir()) {
9532
9755
  try {
9533
- const target = assertPresetDirectory(presetRoot, id);
9756
+ const target = assertModuleDirectory(moduleRoot, id);
9534
9757
  assertPresetTree(target);
9535
9758
  rmSync(target, {
9536
9759
  recursive: true,
9537
9760
  force: true
9538
9761
  });
9539
- invalidatePresetSpec(target);
9762
+ invalidateModuleSpec(target);
9540
9763
  return { ok: true };
9541
9764
  } catch (error) {
9542
9765
  return {
@@ -9611,7 +9834,9 @@ function buildModuleConfigsFromParams(params, options = {}) {
9611
9834
  /**
9612
9835
  * 组合模块目录分工:
9613
9836
  * - source/local:本项目自有模块的唯一源文件;
9614
- * - library:由 rebuild-composition 原样生成的官方切块与官方预设变体,不含本地补丁。
9837
+ * - library:随包分发的官方切块与官方预设变体快照(版本化产物,不手工编辑);
9838
+ * 生成它的 `rebuild-composition` 脚本随内置模块目录 `modules/` 一并退场——输入源已不存在,
9839
+ * 快照本体保留为既成事实,不再重建。
9615
9840
  * 同名文件禁止同时存在,避免 source 与产物漂移。
9616
9841
  */
9617
9842
  function compositionModuleDirs() {
@@ -9647,7 +9872,7 @@ function assembleModules(spec) {
9647
9872
  * 行级 config 合并:仅支持 map 型 config 浅合并;
9648
9873
  * 数组型 config(如 delegation 组)按子行 id 嵌套合并;未声明模块原样保留。
9649
9874
  * 未声明 configs 时返回原文(零开销);parseDocument 往返保留注释。
9650
- * 本函数是参数桥产物落位组合行的唯一机制(含 delegation 组子行嵌套),不是预设覆盖通道。
9875
+ * 本函数是参数桥产物落位组合行的唯一机制(含 delegation 组子行嵌套),不是模块覆盖通道。
9651
9876
  */
9652
9877
  function applyModuleConfigs(raw, configs) {
9653
9878
  if (configs === void 0 || Object.keys(configs).length === 0) return raw;
@@ -9692,9 +9917,9 @@ function applyModuleConfigs(raw, configs) {
9692
9917
  return changed ? doc.toString() : raw;
9693
9918
  }
9694
9919
  /**
9695
- * 加载预设声明的组合(原始 token 文本,未渲染)。
9920
+ * 加载模块声明的组合(原始 token 文本,未渲染)。
9696
9921
  * - `modules:` 清单 → 引擎模块库按序装配;
9697
- * - `composition: ./xxx.yml` → 预设模板目录内组合文件(官方预设直用);
9922
+ * - `composition: ./xxx.yml` → 模块模板目录内组合文件(官方预设直用);
9698
9923
  * - `composition:` 内联文本或组合清单名。
9699
9924
  */
9700
9925
  function loadCompositionText(spec, templateDir, runtime = {}) {
@@ -9744,10 +9969,10 @@ function loadCompositionText(spec, templateDir, runtime = {}) {
9744
9969
  return raw;
9745
9970
  }
9746
9971
  /**
9747
- * 解析预设的模块事实,供 UI 卡片存在性和受控创建共用。
9972
+ * 解析模块的模块事实,供 UI 卡片存在性和受控创建共用。
9748
9973
  * 显式模块及当前参数/策略所需的装配是可编辑插件能力;官方组合 row 仅保留为运行事实。
9749
9974
  */
9750
- function resolvePresetModuleFacts(spec, templateDir, editable = false) {
9975
+ function resolveModuleFacts(spec, templateDir, editable = false) {
9751
9976
  const declaredModules = Array.isArray(spec.modules) ? [...new Set(spec.modules.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim()))] : null;
9752
9977
  const sourceMode = Array.isArray(spec.modules) ? "explicit" : typeof spec.composition === "string" && spec.composition.trim().length > 0 ? "composition" : templateDir !== void 0 && existsSync(join(templateDir, "agent.cordis.yml")) ? "official" : "unknown";
9753
9978
  const effectiveModules = sourceMode === "explicit" ? [...declaredModules ?? []] : null;
@@ -9825,13 +10050,14 @@ function resolvePresetModuleFacts(spec, templateDir, editable = false) {
9825
10050
  rowIds,
9826
10051
  sourceMode,
9827
10052
  editable: editable && sourceMode === "explicit",
9828
- effectiveConfigs
10053
+ effectiveConfigs,
10054
+ subagentToolPolicyEnabled: spec.subagentToolPolicy !== void 0 && spec.subagentToolPolicy !== null
9829
10055
  };
9830
10056
  }
9831
- /** 在内存候选文档中展开能力/recipe,校验成功后一次原子替换 preset.yml。 */
9832
- function createEngineCapabilityInPreset(presetDir, request) {
9833
- const file = join(presetDir, "preset.yml");
9834
- if (!existsSync(file)) throw new Error(`预设目录缺少 preset.yml:${presetDir}`);
10057
+ /** 在内存候选文档中展开能力/recipe,校验成功后一次原子替换 module.yml。 */
10058
+ function createEngineCapabilityInPreset(moduleDir, request) {
10059
+ const file = join(moduleDir, MODULE_DEFINITION_FILE);
10060
+ if (!existsSync(file)) throw new Error(`模块目录缺少 preset.yml:${moduleDir}`);
9835
10061
  const original = readFileSync(file, "utf8");
9836
10062
  const doc = parseDocument(original, { logLevel: "silent" });
9837
10063
  const source = doc.toJS();
@@ -9839,7 +10065,7 @@ function createEngineCapabilityInPreset(presetDir, request) {
9839
10065
  if (!Array.isArray(source.modules)) throw new Error("当前预设没有可编辑的 modules 数组;请先复制为插件用户预设");
9840
10066
  const capabilityIds = request.action === "create" ? [request.capabilityId] : engineRecipe(request.recipeId)?.capabilities ? [...engineRecipe(request.recipeId).capabilities] : [];
9841
10067
  if (capabilityIds.length === 0 || capabilityIds.some((id) => engineCapability(id) === void 0)) throw new Error(`未知引擎能力或 recipe:${request.action === "create" ? request.capabilityId : request.recipeId}`);
9842
- const currentFacts = resolvePresetModuleFacts(source, presetDir, true);
10068
+ const currentFacts = resolveModuleFacts(source, moduleDir, true);
9843
10069
  const declaredFacts = {
9844
10070
  ...currentFacts,
9845
10071
  effectiveModules: currentFacts.declaredModules
@@ -9864,7 +10090,7 @@ function createEngineCapabilityInPreset(presetDir, request) {
9864
10090
  sectionWritten = true;
9865
10091
  }
9866
10092
  const candidate = doc.toJS();
9867
- const rows = assertCompositionArray(renderComposition(candidate, {}, presetDir), candidate);
10093
+ const rows = assertCompositionArray(renderComposition(candidate, {}, moduleDir), candidate);
9868
10094
  const rowPaths = /* @__PURE__ */ new Map();
9869
10095
  const visit = (value, path) => {
9870
10096
  if (Array.isArray(value)) {
@@ -9893,7 +10119,7 @@ function createEngineCapabilityInPreset(presetDir, request) {
9893
10119
  capabilityIds
9894
10120
  };
9895
10121
  atomicWriteTextFile(file, doc.toString());
9896
- invalidatePresetSpec(presetDir);
10122
+ invalidateModuleSpec(moduleDir);
9897
10123
  return {
9898
10124
  changed: true,
9899
10125
  addedModules,
@@ -9903,9 +10129,9 @@ function createEngineCapabilityInPreset(presetDir, request) {
9903
10129
  /** 删除能力:模块声明、顶层数据段,以及该能力的**显式参数与行配置**一起移除。
9904
10130
  * 参数在 ⇒ 装配在(见 impliedModulesFromParams):留下参数会让移除立刻被隐含装配拉回来,
9905
10131
  * 所以"移除能力"必须是完整移除。未登记参数、其他能力的数据与未知字段一律不动。 */
9906
- function removeEngineCapabilityFromPreset(presetDir, capabilityId) {
9907
- const file = join(presetDir, "preset.yml");
9908
- if (!existsSync(file)) throw new Error(`预设目录缺少 preset.yml:${presetDir}`);
10132
+ function removeEngineCapabilityFromPreset(moduleDir, capabilityId) {
10133
+ const file = join(moduleDir, MODULE_DEFINITION_FILE);
10134
+ if (!existsSync(file)) throw new Error(`模块目录缺少 preset.yml:${moduleDir}`);
9909
10135
  const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
9910
10136
  const source = doc.toJS();
9911
10137
  const params = readPresetLayerSettings(source);
@@ -9929,9 +10155,9 @@ function removeEngineCapabilityFromPreset(presetDir, capabilityId) {
9929
10155
  for (const key of paramKeys) doc.deleteIn(engineParamPath(key));
9930
10156
  for (const rowId of configRows) doc.deleteIn(["moduleConfigs", rowId]);
9931
10157
  const candidate = doc.toJS();
9932
- assertCompositionArray(renderComposition(candidate, {}, presetDir), candidate);
10158
+ assertCompositionArray(renderComposition(candidate, {}, moduleDir), candidate);
9933
10159
  atomicWriteTextFile(file, doc.toString());
9934
- invalidatePresetSpec(presetDir);
10160
+ invalidateModuleSpec(moduleDir);
9935
10161
  return {
9936
10162
  changed: true,
9937
10163
  removedModules,
@@ -9939,7 +10165,7 @@ function removeEngineCapabilityFromPreset(presetDir, capabilityId) {
9939
10165
  };
9940
10166
  }
9941
10167
  /**
9942
- * 预设组合渲染完整链路:模块装配 → 参数桥(moduleConfigs + params)行级合并。
10168
+ * 模块组合渲染完整链路:模块装配 → 参数桥(moduleConfigs + params)行级合并。
9943
10169
  * 合并优先级:参数桥(params/UI 基础层) > moduleConfigs(模板/ST 行级直写) > 行默认。
9944
10170
  * moduleConfigs 仅补充参数桥未覆盖的行级 config(如 ST 导入的 tool-web.fetch),
9945
10171
  * 不再锁定覆盖 UI 可管理参数(旧作者锁定语义已移除)。
@@ -9979,107 +10205,259 @@ function assertCompositionArray(raw, spec) {
9979
10205
  if (!Array.isArray(parsed)) throw new Error(`generated agent.cordis.yml is not a YAML array (preset ${spec.id})`);
9980
10206
  return parsed;
9981
10207
  }
10208
+ /** 存储根配置文件名。 */
10209
+ const STORAGE_CONFIG_FILE = "config.yml";
10210
+ /**
10211
+ * 启用表文件位置。
10212
+ *
10213
+ * 以**模块根**为入口、上溯一级取存储根:模块级常量在 import 时按 `DSH_HOME` 冻结,
10214
+ * 而测试会在动态 import 之后才设 `DSH_HOME`,所以这里必须按传入的根现算
10215
+ * (与 `charactersDir(moduleRoot)` 同一条约定)。
10216
+ */
10217
+ function enableTablePath(moduleRoot) {
10218
+ return join(dirname(moduleRoot), STORAGE_CONFIG_FILE);
10219
+ }
10220
+ function readDocument(file) {
10221
+ if (!existsSync(file)) return void 0;
10222
+ const doc = parseDocument(readFileSync(file, "utf8"), { logLevel: "silent" });
10223
+ if (doc.errors.length > 0 || !isMap(doc.contents)) return void 0;
10224
+ return doc;
10225
+ }
10226
+ /**
10227
+ * 读启用表:返回启用的模块 id(按声明顺序,重复项去掉)。
10228
+ *
10229
+ * 文件缺失、解析失败、schemaVersion 低于 3、`enabled` 不是字符串数组——一律返回空表。
10230
+ * 空表意味着「没有模块参与装配」,这是明确状态,不是错误:旧版本的索引文件里
10231
+ * `enabled` 还没有启用语义,当成启用会让历史键意外参与装配。
10232
+ */
10233
+ function enabledModuleIds(moduleRoot) {
10234
+ const doc = readDocument(enableTablePath(moduleRoot));
10235
+ if (doc === void 0) return [];
10236
+ if (Number(doc.get("schemaVersion")) < 3) return [];
10237
+ const list = doc.toJS()?.enabled;
10238
+ if (!Array.isArray(list)) return [];
10239
+ return [...new Set(list.filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))];
10240
+ }
10241
+ /** 写启用表:保留既有注释与未知字段,只改 `schemaVersion` 与 `enabled`。 */
10242
+ function writeEnabled(moduleRoot, ids) {
10243
+ const file = enableTablePath(moduleRoot);
10244
+ const doc = readDocument(file);
10245
+ if (doc === void 0) {
10246
+ atomicWriteTextFile(file, [
10247
+ "# Prompt Tool 模块启用表:只记录哪些模块参与运行时装配。",
10248
+ "# 参数、提示词与素材在各模块自己的目录里(modules/<id>/);这里没有版本指针,也没有配装段。",
10249
+ "",
10250
+ `schemaVersion: 3`,
10251
+ ...ids.length === 0 ? ["enabled: []"] : ["enabled:", ...ids.map((id) => ` - ${id}`)],
10252
+ ""
10253
+ ].join("\n"));
10254
+ return;
10255
+ }
10256
+ doc.set("schemaVersion", 3);
10257
+ doc.set("enabled", [...ids]);
10258
+ atomicWriteTextFile(file, doc.toString());
10259
+ }
10260
+ /**
10261
+ * 解析**编辑目标**模块目录:请求声明了目标就按它定位,没声明则回退启用表首项。
10262
+ *
10263
+ * 这是「按请求定位编辑目标」的唯一入口——编辑器不必依赖某个全局单选:写请求带上
10264
+ * 模块 id 就写那个模块,不带则落到启用表第一项(= 装配顺序首项)。目标不存在时返回
10265
+ * 空串,由调用方按「无目标」拒绝,不静默改写到别的模块。
10266
+ */
10267
+ function resolveEditDir(moduleRoot, moduleId) {
10268
+ const target = moduleId !== void 0 && moduleId.trim().length > 0 ? moduleId.trim() : enabledModuleIds(moduleRoot)[0];
10269
+ if (target === void 0) return "";
10270
+ return moduleDirExists(moduleRoot, target) ? join(moduleRoot, target) : "";
10271
+ }
10272
+ /**
10273
+ * 启用或停用一个模块。幂等:已在表里再启用不产生第二条,不在表里再停用不报错。
10274
+ * 顺序即装配顺序,新模块追加在末尾(同键参数的覆盖顺序因此可预测)。
10275
+ */
10276
+ function setModuleEnabled(moduleRoot, id, enabled) {
10277
+ const current = enabledModuleIds(moduleRoot);
10278
+ const next = enabled ? current.includes(id) ? current : [...current, id] : current.filter((existing) => existing !== id);
10279
+ if (next.length === current.length && next.every((value, index) => value === current[index])) return;
10280
+ writeEnabled(moduleRoot, next);
10281
+ }
9982
10282
  //#endregion
9983
- //#region src/shared/managed-config-fields.ts
9984
- /** writer 可投影的字段白名单;是否确由参数投影由 ConfigFieldSources 决定。 */
9985
- const MANAGED_CONFIG_FIELDS = [{
9986
- configId: "near-anchor",
9987
- fields: [
9988
- {
9989
- path: "enabled",
9990
- sourceParam: "firstTurnAnchor"
9991
- },
9992
- {
9993
- path: "params.useCustom",
9994
- sourceParam: "firstTurnCustom"
9995
- },
9996
- {
9997
- path: "params.text",
9998
- sourceParam: "firstTurnText"
9999
- },
10000
- {
10001
- path: "params.buildPattern",
10002
- sourceParam: "buildPattern"
10003
- },
10004
- {
10005
- path: "params.complexPattern",
10006
- sourceParam: "complexPattern"
10007
- },
10008
- {
10009
- path: "params.firstTurnBuild",
10010
- sourceParam: "firstTurnBuild"
10011
- },
10012
- {
10013
- path: "params.firstTurnInspect",
10014
- sourceParam: "firstTurnInspect"
10015
- },
10016
- {
10017
- path: "params.firstTurnDeep",
10018
- sourceParam: "firstTurnDeep"
10019
- }
10020
- ]
10021
- }, {
10022
- configId: "router-guide",
10023
- fields: [
10024
- {
10025
- path: "enabled",
10026
- sourceParam: "guideEnabled"
10027
- },
10028
- {
10029
- path: "modelScope",
10030
- sourceParam: "guideCustom",
10031
- derived: true,
10032
- fallbackNote: "followsCustom"
10033
- },
10034
- {
10035
- path: "params.useCustom",
10036
- sourceParam: "guideCustom",
10037
- derived: true
10038
- },
10039
- {
10040
- path: "params.text",
10041
- sourceParam: "guideText"
10042
- },
10043
- {
10044
- path: "params.complexPattern",
10045
- sourceParam: "complexPattern"
10046
- },
10047
- {
10048
- path: "params.guideWeak",
10049
- sourceParam: "guideWeak"
10050
- },
10051
- {
10052
- path: "params.guideDeep",
10053
- sourceParam: "guideDeep"
10054
- }
10055
- ]
10056
- }];
10057
- /** 严格投影白名单;未知路径、参数键和附加属性都不进入客户端。 */
10058
- function readConfigFieldSources(configId, raw) {
10059
- const spec = MANAGED_CONFIG_FIELDS.find((entry) => entry.configId === configId);
10060
- if (spec === void 0 || raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
10061
- const record = raw;
10062
- if (record.configId !== configId || !Array.isArray(record.fields)) return void 0;
10283
+ //#region src/shared/module-config-order.ts
10284
+ const configIdentityKey = (entry) => JSON.stringify([entry.moduleId, entry.configId]);
10285
+ /** 不以启用表次序打破同号平局;排序不改变配置卡身份。 */
10286
+ function compareModuleConfigOrder(a, b) {
10287
+ const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
10288
+ return a.sequence - b.sequence || compare(a.moduleId, b.moduleId) || compare(a.configId, b.configId);
10289
+ }
10290
+ //#endregion
10291
+ //#region src/host/module-config-order.ts
10292
+ /** 已启用配置卡的排序:轻量序号归各自 module.yml,文件是可重建投影。 */
10293
+ var ModuleConfigOrderError = class extends Error {
10294
+ status;
10295
+ code;
10296
+ constructor(message, conflict = false) {
10297
+ super(message);
10298
+ this.status = conflict ? 409 : 400;
10299
+ this.code = conflict ? "module-config-order-conflict" : "module-config-order-invalid";
10300
+ }
10301
+ };
10302
+ function readConfigOrder(value) {
10303
+ if (value === void 0) return Object.create(null);
10304
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("configOrder 必须是配置卡序号对象");
10305
+ for (const [id, sequence] of Object.entries(value)) {
10306
+ assertSafeConfigId(id);
10307
+ if (typeof sequence !== "number" || !Number.isSafeInteger(sequence) || sequence < 0) throw new Error(`配置卡 ${id} 的序号必须是非负安全整数`);
10308
+ }
10309
+ return Object.assign(Object.create(null), value);
10310
+ }
10311
+ function readInput(root, moduleId) {
10312
+ const dir = assertModuleDirectory(root, moduleId);
10313
+ const raw = readFileSync(join(dir, MODULE_DEFINITION_FILE), "utf8");
10314
+ const doc = parseDocument(raw, { logLevel: "silent" });
10315
+ const saved = readConfigOrder(doc.toJS()?.configOrder);
10316
+ const spec = loadModuleSpec(dir);
10317
+ const directory = join(dir, MODULE_CONFIGS_DIR);
10318
+ const files = /* @__PURE__ */ new Map();
10319
+ if (existsSync(directory)) {
10320
+ assertPresetTree(directory);
10321
+ for (const [index, file] of promptConfigFileNames(readdirSync(directory, { withFileTypes: true })).entries()) {
10322
+ const source = readFileSync(join(directory, file), "utf8");
10323
+ const config = /\.json$/i.test(file) ? JSON.parse(source) : parsePromptConfigYaml(source);
10324
+ assertSafeConfigId(config.id);
10325
+ if (files.has(config.id)) throw new Error(`模块 ${moduleId} 存在重复配置卡:${config.id}`);
10326
+ const prefix = /^(\d+)-/.exec(file);
10327
+ files.set(config.id, {
10328
+ config,
10329
+ sequence: prefix === null ? index * 10 : Number(prefix[1])
10330
+ });
10331
+ }
10332
+ }
10333
+ const cards = Array.isArray(spec.promptConfigs) ? mergePromptConfigs(modelRequestConfigs(resolvePresetParams(spec, {})), spec.promptConfigs) : [...files.values()].map((entry) => entry.config);
10334
+ const seen = /* @__PURE__ */ new Set();
10063
10335
  return {
10064
- configId,
10065
- fields: spec.fields.flatMap(({ path }) => {
10066
- const source = record.fields.find((item) => item !== null && typeof item === "object" && item.path === path)?.source;
10067
- return source === "preset-param" || source === "prompt-config" ? [{
10068
- path,
10069
- source
10070
- }] : [];
10336
+ moduleId,
10337
+ dir,
10338
+ raw,
10339
+ doc,
10340
+ saved,
10341
+ entries: cards.map((card, index) => {
10342
+ assertSafeConfigId(card.id);
10343
+ if (seen.has(card.id)) throw new Error(`模块 ${moduleId} 存在重复配置卡:${card.id}`);
10344
+ seen.add(card.id);
10345
+ const sequence = saved[card.id] ?? files.get(card.id)?.sequence ?? index * 10;
10346
+ if (!Number.isSafeInteger(sequence) || sequence < 0) throw new Error(`配置卡 ${card.id} 的文件序号无效`);
10347
+ return {
10348
+ moduleId,
10349
+ configId: card.id,
10350
+ name: card.name ?? card.id,
10351
+ layer: card.layer ?? "pre-step",
10352
+ position: card.position ?? "after-user",
10353
+ sequence,
10354
+ enabled: card.enabled !== false,
10355
+ strategy: card.strategy ?? "static",
10356
+ ...card.audience === void 0 ? {} : { audience: card.audience },
10357
+ ...card.layer === "system-section" || card.layer === "runtime-context" ? { order: card.order ?? 0 } : {}
10358
+ };
10071
10359
  })
10072
10360
  };
10073
10361
  }
10074
- /** 来源只用于读回;无论客户端传了什么来源,都不能写入预设定义。 */
10075
- function stripConfigFieldSources(config) {
10076
- const { fieldSources: _fieldSources, ...definition } = config;
10077
- return definition;
10362
+ function readInputs(root, ids = enabledModuleIds(root)) {
10363
+ return [...new Set(ids)].sort().map((id) => readInput(root, id));
10364
+ }
10365
+ function snapshotOf(inputs) {
10366
+ return {
10367
+ revision: createHash("sha256").update(JSON.stringify(inputs.map((input) => [
10368
+ input.moduleId,
10369
+ input.raw,
10370
+ input.entries
10371
+ ]))).digest("hex"),
10372
+ entries: inputs.flatMap((input) => input.entries).sort(compareModuleConfigOrder)
10373
+ };
10374
+ }
10375
+ function readModuleConfigOrder(root, moduleId) {
10376
+ return snapshotOf(readInputs(root, moduleId === void 0 ? void 0 : [moduleId]));
10377
+ }
10378
+ function writeOrders(root, inputs, orders) {
10379
+ const changes = inputs.flatMap((input) => {
10380
+ const next = orders.get(input.moduleId);
10381
+ if (next === void 0 || Object.entries(next).every(([id, value]) => input.saved[id] === value)) return [];
10382
+ for (const [id, sequence] of Object.entries(next)) input.doc.setIn(["configOrder", id], sequence);
10383
+ return [{
10384
+ ...input,
10385
+ next: input.doc.toString()
10386
+ }];
10387
+ });
10388
+ const verify = (change) => {
10389
+ const dir = assertModuleDirectory(root, change.moduleId);
10390
+ if (dir !== change.dir || readFileSync(join(dir, "module.yml"), "utf8") !== change.raw) throw new ModuleConfigOrderError("模块版本已变化,请重新读取排序", true);
10391
+ };
10392
+ for (const change of changes) verify(change);
10393
+ const written = [];
10394
+ try {
10395
+ for (const change of changes) {
10396
+ atomicWriteTextFile(join(change.dir, MODULE_DEFINITION_FILE), change.next, { beforeReplace: () => verify(change) });
10397
+ invalidateModuleSpec(change.dir);
10398
+ written.push(change);
10399
+ }
10400
+ } catch (error) {
10401
+ for (const change of written.reverse()) {
10402
+ const file = join(change.dir, MODULE_DEFINITION_FILE);
10403
+ if (readFileSync(file, "utf8") === change.next) {
10404
+ atomicWriteTextFile(file, change.raw, { beforeReplace: () => {
10405
+ assertModuleDirectory(root, change.moduleId);
10406
+ if (readFileSync(file, "utf8") !== change.next) throw new Error("排序恢复时模块再次变化");
10407
+ } });
10408
+ invalidateModuleSpec(change.dir);
10409
+ }
10410
+ }
10411
+ throw error;
10412
+ }
10413
+ return changes.map((change) => change.moduleId);
10414
+ }
10415
+ /** 新卡或与其他启用模块碰号的卡接在尾部;已保存且无冲突的顺序保持。 */
10416
+ function appendModuleConfigOrder(root, moduleId) {
10417
+ const others = readInputs(root, enabledModuleIds(root).filter((id) => id !== moduleId));
10418
+ const input = readInput(root, moduleId);
10419
+ const used = new Set(others.flatMap((item) => item.entries.map((entry) => entry.sequence)));
10420
+ let next = -10;
10421
+ for (const value of used) next = Math.max(next, value);
10422
+ for (const value of Object.values(input.saved)) next = Math.max(next, value);
10423
+ next += 10;
10424
+ const assigned = Object.create(null);
10425
+ const collision = input.entries.some((entry) => input.saved[entry.configId] !== void 0 && used.has(input.saved[entry.configId]));
10426
+ for (const entry of [...input.entries].sort(compareModuleConfigOrder)) {
10427
+ let sequence = input.saved[entry.configId];
10428
+ if (collision || sequence === void 0 || used.has(sequence)) {
10429
+ if (!Number.isSafeInteger(next)) throw new Error("配置卡排序序号超出安全整数范围");
10430
+ sequence = next;
10431
+ next += 10;
10432
+ }
10433
+ used.add(sequence);
10434
+ assigned[entry.configId] = sequence;
10435
+ }
10436
+ return writeOrders(root, [input], /* @__PURE__ */ new Map([[moduleId, assigned]]));
10437
+ }
10438
+ /** 默认重排启用集合;指定模块只交换其现有槽位,不接受客户端序号、正文或路径。 */
10439
+ function saveModuleConfigOrder(root, expectedRevision, entries, moduleId) {
10440
+ const inputs = readInputs(root, moduleId === void 0 ? void 0 : [moduleId]);
10441
+ const current = snapshotOf(inputs);
10442
+ if (expectedRevision !== current.revision) throw new ModuleConfigOrderError("模块版本已变化,请重新读取排序", true);
10443
+ if (!Array.isArray(entries) || entries.length !== current.entries.length) throw new ModuleConfigOrderError("排序配置卡集合已变化", true);
10444
+ const known = new Set(current.entries.map(configIdentityKey));
10445
+ const orders = /* @__PURE__ */ new Map();
10446
+ for (const [index, entry] of entries.entries()) {
10447
+ if (entry === null || typeof entry !== "object" || typeof entry.moduleId !== "string" || typeof entry.configId !== "string" || Object.keys(entry).some((key) => key !== "moduleId" && key !== "configId") || !known.delete(configIdentityKey(entry))) throw new ModuleConfigOrderError("排序包含未知或重复配置卡");
10448
+ let values = orders.get(entry.moduleId);
10449
+ if (values === void 0) {
10450
+ values = Object.create(null);
10451
+ orders.set(entry.moduleId, values);
10452
+ }
10453
+ values[entry.configId] = moduleId === void 0 ? index * 10 : current.entries[index].sequence;
10454
+ }
10455
+ return writeOrders(root, inputs, orders);
10078
10456
  }
10079
10457
  //#endregion
10080
- //#region src/host/preset-triggers.ts
10458
+ //#region src/host/module-triggers.ts
10081
10459
  /** 触发器只归属 preset.yml;内容版本覆盖完整定义,写盘复用现有 YAML Document 通道。 */
10082
- /** 声明数据以实际 triggers.yml 为基准;允许根与注册层注入的 presetRoot 一致。 */
10460
+ /** 声明数据以实际 triggers.yml 为基准;允许根与注册层注入的 moduleRoot 一致。 */
10083
10461
  function triggerPromptConfigOptions(directory, strategy) {
10084
10462
  const templatePresetRoot = pathToFileURL(dirname(directory) + "/");
10085
10463
  const templateBaseUrl = pathToFileURL(join(directory, "triggers.yml"));
@@ -10089,9 +10467,9 @@ function triggerPromptConfigOptions(directory, strategy) {
10089
10467
  strategyDir: typeof strategy !== "string" || strategy.length === 0 ? void 0 : isAbsolute(strategy) ? pathToFileURL(strategy).href : new URL(strategy, templateBaseUrl).href
10090
10468
  };
10091
10469
  }
10092
- function readPresetTriggers(directory) {
10093
- const dir = assertPresetDirectory(dirname(directory), basename(directory));
10094
- const source = readFileSync(join(dir, "preset.yml"), "utf8");
10470
+ function readModuleTriggers(directory) {
10471
+ const dir = assertModuleDirectory(dirname(directory), basename(directory));
10472
+ const source = readFileSync(join(dir, MODULE_DEFINITION_FILE), "utf8");
10095
10473
  const triggers = parseDocument(source, { logLevel: "silent" }).toJS().triggers ?? [];
10096
10474
  if (!Array.isArray(triggers)) throw new TypeError("preset.yml 的 triggers 必须是数组");
10097
10475
  return {
@@ -10100,8 +10478,8 @@ function readPresetTriggers(directory) {
10100
10478
  };
10101
10479
  }
10102
10480
  /** 同步读改写之间不让出事件循环;版本不符不调用写盘,也不丢弃外部字段和注释。 */
10103
- function savePresetTriggers(directory, triggers, expectedRevision) {
10104
- if (readPresetTriggers(directory).revision !== expectedRevision) return false;
10481
+ function saveModuleTriggers(directory, triggers, expectedRevision) {
10482
+ if (readModuleTriggers(directory).revision !== expectedRevision) return false;
10105
10483
  withPresetDoc(directory, (doc) => {
10106
10484
  if (triggers.length === 0) doc.delete("triggers");
10107
10485
  else doc.set("triggers", triggers);
@@ -10197,16 +10575,16 @@ function buildWorldBookEntry(input) {
10197
10575
  };
10198
10576
  }
10199
10577
  const isWorldBook = (config) => config !== null && typeof config === "object" && !Array.isArray(config) && config.strategy === "world-book";
10200
- /** 当前预设全部世界书条目(保持文件顺序)。 */
10201
- function listWorldBookEntries(presetDir) {
10202
- const spec = loadPresetSpec(presetDir);
10578
+ /** 当前模块全部世界书条目(保持文件顺序)。 */
10579
+ function listWorldBookEntries(moduleDir) {
10580
+ const spec = loadModuleSpec(moduleDir);
10203
10581
  return Array.isArray(spec.promptConfigs) ? spec.promptConfigs.filter(isWorldBook) : [];
10204
10582
  }
10205
10583
  /** 新增或更新一条世界书条目(按 id 定位;不存在则追加)。返回写入后条目总数。 */
10206
- function upsertWorldBookEntry(presetDir, entry) {
10584
+ function upsertWorldBookEntry(moduleDir, entry) {
10207
10585
  if (entry === null || typeof entry !== "object" || typeof entry.id !== "string" || entry.id.length === 0) throw new TypeError("世界书条目必须含非空字符串 id");
10208
10586
  let count = 0;
10209
- withPresetDoc(presetDir, (doc) => {
10587
+ withPresetDoc(moduleDir, (doc) => {
10210
10588
  const current = doc.toJS();
10211
10589
  const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
10212
10590
  const existing = configs.findIndex((config) => String(config.id ?? "") === entry.id);
@@ -10218,10 +10596,10 @@ function upsertWorldBookEntry(presetDir, entry) {
10218
10596
  return count;
10219
10597
  }
10220
10598
  /** 删除一条世界书条目;不存在抛错。返回删除后条目总数。 */
10221
- function deleteWorldBookEntry(presetDir, id) {
10599
+ function deleteWorldBookEntry(moduleDir, id) {
10222
10600
  let count = 0;
10223
10601
  let removed = false;
10224
- withPresetDoc(presetDir, (doc) => {
10602
+ withPresetDoc(moduleDir, (doc) => {
10225
10603
  const current = doc.toJS();
10226
10604
  const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
10227
10605
  const kept = configs.filter((config) => !(isWorldBook(config) && String(config.id ?? "") === id));
@@ -10429,7 +10807,7 @@ function stOrderSelectionState(card, options = {}) {
10429
10807
  };
10430
10808
  return { needsSelection: false };
10431
10809
  }
10432
- /** SillyTavern JSON 预设卡片 → 本项目 PresetSpec(导入端点直接消费)。 */
10810
+ /** SillyTavern JSON 预设卡片 → 本项目 ModuleSpec(导入端点直接消费)。 */
10433
10811
  function convertStToPreset(card, baseName, options = {}) {
10434
10812
  return convertStToPresetWithReport(card, baseName, options).spec;
10435
10813
  }
@@ -11413,7 +11791,7 @@ function classify(raw, target) {
11413
11791
  function prepareImport(input, target, choices = {}) {
11414
11792
  const files = normalizeAssetFiles(input);
11415
11793
  const sourceDigest = assetSourceDigest(files);
11416
- const named = files.filter((file) => /^(preset|converted)\.ya?ml$/i.test(file.path));
11794
+ const named = files.filter((file) => /^(?:module|preset|converted)\.ya?ml$/i.test(file.path));
11417
11795
  if (named.length > 1) throw new Error(`存在多个定义候选:${named.map((file) => file.path).join("、")}`);
11418
11796
  const topFiles = files.filter((file) => !file.path.includes("/"));
11419
11797
  const structured = topFiles.filter((file) => /\.(json|ya?ml)$/i.test(file.path));
@@ -11454,7 +11832,7 @@ function prepareImport(input, target, choices = {}) {
11454
11832
  validateCharacterSpec(raw);
11455
11833
  selfContained = true;
11456
11834
  } catch {}
11457
- if (selfContained && fragment && !/^preset\.ya?ml$/i.test(file.path) && !/^converted\.ya?ml$/i.test(file.path) && choices.sourceKind === void 0) return {
11835
+ if (selfContained && fragment && !/^(?:module|preset)\.ya?ml$/i.test(file.path) && !/^converted\.ya?ml$/i.test(file.path) && choices.sourceKind === void 0) return {
11458
11836
  state: "needs-kind-selection",
11459
11837
  kinds: ["native-preset", "native-character"],
11460
11838
  sourceName: file.path
@@ -11531,7 +11909,7 @@ function prepareImport(input, target, choices = {}) {
11531
11909
  const avatar = first.avatar ?? (avatarFile === void 0 ? void 0 : decodeAssetFile(avatarFile));
11532
11910
  if (avatar !== void 0 && !isPngBuffer(avatar)) throw new Error("avatar.png 必须提供显式编码的 PNG 原始字节");
11533
11911
  const preparedFiles = [{
11534
- path: "preset.yml",
11912
+ path: MODULE_DEFINITION_FILE,
11535
11913
  content: yaml,
11536
11914
  encoding: "utf8"
11537
11915
  }, ...resources];
@@ -11555,9 +11933,11 @@ function prepareImport(input, target, choices = {}) {
11555
11933
  }
11556
11934
  //#endregion
11557
11935
  //#region src/host/characters.ts
11558
- /** 角色卡库:独立于预设根的素材+参数存储(预设根下点前缀目录,官方 discovery 跳过)。
11559
- * 角色卡参数(converted.yml = convertStToPreset 产物)不直接生成预设,而是
11560
- * 由用户按需「导入到当前预设」合并进激活预设 preset.yml(promptConfigs 带
11936
+ /** 角色卡库:独立于模块根的素材+参数存储(**存储根**下点前缀目录,官方 discovery 跳过)。
11937
+ * 注意区分两种语义:`moduleRoot` 形参指的是**模块根**(`<存储根>/modules`),角色卡库
11938
+ * 在它的上一级(`<存储根>/.characters`)——两者是兄弟,不是父子。
11939
+ * 角色卡参数(converted.yml = convertStToPreset 产物)不直接生成模块,而是
11940
+ * 由用户按需「导入到当前模块」合并进激活模块 module.yml(promptConfigs 带
11561
11941
  * chara-<cardId>- 前缀防冲突,params 合并,meta.importedCharacters 记录来源),
11562
11942
  * 并可一键移除。 */
11563
11943
  /** 合并写盘时按九层顺序排序(数组序 = 引擎序):层序只由共享契约提供,这里不再维护第二份。 */
@@ -11603,15 +11983,24 @@ function recordMemory(doc, cardId, config) {
11603
11983
  contentHash: contentHash(config)
11604
11984
  });
11605
11985
  }
11986
+ /**
11987
+ * 并入条目的前缀集合:**写入统一用 `module-<id>-`**,读取端同时接受历史的 `chara-<id>-`。
11988
+ *
11989
+ * 不迁移老数据:用户已并入的条目 id 就长成 `chara-…`,改写它们等于重写用户的 module.yml;
11990
+ * 而「按前缀差集撤销」的移除逻辑必须认得两种前缀,否则老条目会撤不掉、新条目会重复并入。
11991
+ */
11992
+ function importPrefixes(id) {
11993
+ return [`module-${id}-`, `chara-${id}-`];
11994
+ }
11606
11995
  function availableMemoryId(configs, cardId) {
11607
- const base = `chara-${cardId}-memory`;
11996
+ const base = `module-${cardId}-memory`;
11608
11997
  const used = new Set(configs.map((config) => config.id));
11609
11998
  let id = base;
11610
11999
  for (let suffix = 2; used.has(id); suffix += 1) id = `${base}-${suffix}`;
11611
12000
  return id;
11612
12001
  }
11613
12002
  /** 分享副本:只排除本机生成证明仍匹配的记忆;旧项与已编辑项须明确选择。 */
11614
- function projectCharacterMemories(source, choices = {}, presetRoot) {
12003
+ function projectCharacterMemories(source, choices = {}, moduleRoot) {
11615
12004
  const doc = source.clone();
11616
12005
  const spec = doc.toJS();
11617
12006
  const records = memoryRecords(spec.meta);
@@ -11626,12 +12015,12 @@ function projectCharacterMemories(source, choices = {}, presetRoot) {
11626
12015
  excluded.add(index);
11627
12016
  continue;
11628
12017
  }
11629
- const cardId = imported.find((card) => id === `chara-${card}-memory` || id.startsWith(`chara-${card}-memory-`)) ?? proof?.characterId;
12018
+ const cardId = imported.find((card) => importPrefixes(card).some((base) => id === `${base}memory` || id.startsWith(`${base}memory-`))) ?? proof?.characterId;
11630
12019
  if (cardId === void 0) continue;
11631
- if (presetRoot !== void 0 && validCardId(cardId)) {
11632
- const original = loadConverted(cardDir(presetRoot, cardId));
11633
- if (original?.promptConfigs?.some((entry) => isRecord$1(entry) && `chara-${cardId}-${String(entry.id)}` === id)) continue;
11634
- const memory = original === void 0 ? void 0 : buildCharacterMemoryEntry(original, readCharacterMemory(presetRoot, cardId));
12020
+ if (moduleRoot !== void 0 && validCardId(cardId)) {
12021
+ const original = loadImportSource(moduleRoot, cardId);
12022
+ if (original?.promptConfigs?.some((entry) => isRecord$1(entry) && importPrefixes(cardId).some((base) => `${base}${String(entry.id)}` === id))) continue;
12023
+ const memory = original === void 0 ? void 0 : buildCharacterMemoryEntry(original, readCharacterMemory(moduleRoot, cardId));
11635
12024
  if (memory !== void 0 && contentHash({
11636
12025
  ...memory,
11637
12026
  id
@@ -11694,13 +12083,16 @@ function characterModuleStillNeeded(module, context) {
11694
12083
  case "world-book-tools": return context.configs.some((config) => config.strategy === "world-book");
11695
12084
  case "session-var-tools": return context.configs.some((config) => isRecord$1(config.params) && config.params.stMacros === true);
11696
12085
  case "tool-config-engine": return context.customTools;
11697
- case "character-tools": return context.importedCharacters.length > 0 || context.configs.some((config) => String(config.id ?? "").startsWith("chara-"));
12086
+ case "character-tools": return context.importedCharacters.length > 0 || context.configs.some((config) => {
12087
+ const id = String(config.id ?? "");
12088
+ return id.startsWith("module-") || id.startsWith("chara-");
12089
+ });
11698
12090
  default: return true;
11699
12091
  }
11700
12092
  }
11701
12093
  /** 其他已导入卡「引入过」或「声明过」的模块:移除本卡时不得夺走别人要用的模块。
11702
12094
  * 声明这一路必须读卡库的 converted.yml——第二张卡声明同一模块时差集为空、不会留下引入记录。 */
11703
- function modulesClaimedByOtherCards(presetRoot, importedCards, meta, cardId) {
12095
+ function modulesClaimedByOtherCards(moduleRoot, importedCards, meta, cardId) {
11704
12096
  const claimed = /* @__PURE__ */ new Set();
11705
12097
  for (const [id, modules] of Object.entries(recordedCharacterModules(meta))) {
11706
12098
  if (id === cardId || !importedCards.includes(id)) continue;
@@ -11708,7 +12100,7 @@ function modulesClaimedByOtherCards(presetRoot, importedCards, meta, cardId) {
11708
12100
  }
11709
12101
  for (const id of importedCards) {
11710
12102
  if (id === cardId || !validCardId(id)) continue;
11711
- const spec = loadConverted(cardDir(presetRoot, id));
12103
+ const spec = loadImportSource(moduleRoot, id);
11712
12104
  if (spec === void 0) continue;
11713
12105
  for (const module of declaredCharacterModules(spec)) claimed.add(module);
11714
12106
  }
@@ -11719,22 +12111,22 @@ function readPresetModules(doc) {
11719
12111
  const source = doc.toJS();
11720
12112
  return Array.isArray(source.modules) ? source.modules.filter((item) => typeof item === "string") : [];
11721
12113
  }
11722
- /** 角色卡记忆变更后同步已导入预设的 chara-<id>-memory 注入条目
11723
- * (world_book note 写入卡记忆后调用;未导入当前预设的卡返回 synced=false)。 */
11724
- function syncImportedCharacterMemory(presetRoot, templateName, cardId) {
12114
+ /** 角色卡记忆变更后同步已导入模块的 chara-<id>-memory 注入条目
12115
+ * (world_book note 写入卡记忆后调用;未导入当前模块的卡返回 synced=false)。 */
12116
+ function syncImportedCharacterMemory(moduleRoot, templateName, cardId) {
11725
12117
  if (!validCardId(cardId)) return {
11726
12118
  ok: false,
11727
12119
  message: `非法角色卡 id:${cardId}`
11728
12120
  };
11729
12121
  let synced = false;
11730
12122
  try {
11731
- const spec = loadConverted(cardDir(presetRoot, cardId));
12123
+ const spec = loadImportSource(moduleRoot, cardId);
11732
12124
  if (spec === void 0) return {
11733
12125
  ok: false,
11734
12126
  message: `角色卡 ${cardId} 不存在或参数损坏`
11735
12127
  };
11736
- const memory = readCharacterMemory(presetRoot, cardId);
11737
- withPresetDoc(join(presetRoot, templateName), (doc) => {
12128
+ const memory = readCharacterMemory(moduleRoot, cardId);
12129
+ withPresetDoc(join(moduleRoot, templateName), (doc) => {
11738
12130
  const current = doc.toJS();
11739
12131
  if (!(Array.isArray(current.meta?.importedCharacters) ? current.meta.importedCharacters.map(String) : []).includes(cardId)) return;
11740
12132
  const configs = Array.isArray(current.promptConfigs) ? current.promptConfigs : [];
@@ -11774,25 +12166,55 @@ function syncImportedCharacterMemory(presetRoot, templateName, cardId) {
11774
12166
  };
11775
12167
  }
11776
12168
  }
11777
- /** 角色卡库根:预设根下的点前缀目录(官方 roster 扫描跳过,与 .engine 同机制)。 */
11778
- function charactersDir(presetRoot) {
11779
- return join(presetRoot, ".characters");
12169
+ /**
12170
+ * 角色卡库根:**存储根**下的点前缀目录(`<存储根>/.characters`)。
12171
+ *
12172
+ * 本模块形参 `moduleRoot` 的语义是**模块根**(`<存储根>/modules`),角色卡库与模块目录
12173
+ * 是兄弟:从模块根上溯一级即存储根。**必须由参数推导、不读模块级常量**——常量在
12174
+ * import 时按 `DSH_HOME` 冻结,测试内改 `DSH_HOME` 对它无效,会直接写进真实
12175
+ * `DSH_HOME`(2026-10-02 实测踩过:5 个用例把夹具角色卡写进了活的数据目录)。
12176
+ * 官方 roster 不扫描它(点前缀,与 `.engine` 同机制)。
12177
+ */
12178
+ function charactersDir(moduleRoot) {
12179
+ return join(dirname(moduleRoot), ".characters");
11780
12180
  }
11781
- function cardDir(presetRoot, id) {
11782
- return join(charactersDir(presetRoot), id);
12181
+ function cardDir(moduleRoot, id) {
12182
+ return join(charactersDir(moduleRoot), id);
11783
12183
  }
11784
- function loadConverted(dir) {
11785
- const file = join(dir, "converted.yml");
12184
+ /** 读一个 ModuleSpec 形状的定义文件(`converted.yml` 与 `module.yml` 同构)。 */
12185
+ function loadSpecFile(file) {
11786
12186
  assertNoLinks(file);
11787
12187
  if (!presetPathExists(file)) return void 0;
11788
12188
  const parsed = parse(readFileSync(file, "utf8"), { logLevel: "silent" });
11789
12189
  if (!isRecord$1(parsed)) throw new Error(`角色定义不是对象:${file}`);
11790
- return parsed;
12190
+ const spec = parsed;
12191
+ const legacy = resolveLegacyPromptConfigs(spec, { moduleDir: dirname(file) });
12192
+ return legacy.active ? {
12193
+ ...spec,
12194
+ promptConfigs: legacy.configs,
12195
+ legacyParamWarnings: legacy.warnings
12196
+ } : spec;
12197
+ }
12198
+ function loadConverted(dir) {
12199
+ return loadSpecFile(join(dir, "converted.yml"));
11791
12200
  }
11792
- /** 追加角色卡本地记忆(.characters/<id>/memory.md,跟随角色卡跨预设)。 */
11793
- function appendCharacterMemory(presetRoot, cardId, note) {
12201
+ /**
12202
+ * 并入与移除的**源**:模块优先(`<模块根>/<id>/module.yml`),回退角色卡库
12203
+ * (`.characters/<id>/converted.yml`)。
12204
+ *
12205
+ * 模块是统一身份——并入因此成为**模块间**能力,而不是只为角色卡存在的一套;
12206
+ * 角色卡库保留为素材来源与历史形态,所以仍作回退,已有卡片不迁移。
12207
+ * 一个入口、一套前缀(`module-<id>-`),不另立第二套机制。
12208
+ */
12209
+ function loadImportSource(moduleRoot, id) {
12210
+ const moduleFile = join(moduleRoot, id, MODULE_DEFINITION_FILE);
12211
+ if (presetPathExists(moduleFile)) return loadSpecFile(moduleFile);
12212
+ return loadConverted(cardDir(moduleRoot, id));
12213
+ }
12214
+ /** 追加角色卡本地记忆(.characters/<id>/memory.md,跟随角色卡跨模块)。 */
12215
+ function appendCharacterMemory(moduleRoot, cardId, note) {
11794
12216
  assertPresetId(cardId);
11795
- appendMemoryFile(join(cardDir(presetRoot, cardId), "memory.md"), note, "# 角色记忆");
12217
+ appendMemoryFile(join(cardDir(moduleRoot, cardId), "memory.md"), note, "# 角色记忆");
11796
12218
  }
11797
12219
  /** 检查现有祖先和最终文件,不能通过角色目录 junction 或单文件链接越界。 */
11798
12220
  function assertNoLinks(path) {
@@ -11812,9 +12234,9 @@ function appendMemoryFile(file, note, header = "# 本地记忆") {
11812
12234
  else writeFileSync(file, `${header}\n${line}`, "utf8");
11813
12235
  }
11814
12236
  /** 读角色卡本地记忆文本(无记忆返回空串)。 */
11815
- function readCharacterMemory(presetRoot, cardId) {
12237
+ function readCharacterMemory(moduleRoot, cardId) {
11816
12238
  assertPresetId(cardId);
11817
- const file = join(cardDir(presetRoot, cardId), "memory.md");
12239
+ const file = join(cardDir(moduleRoot, cardId), "memory.md");
11818
12240
  assertNoLinks(file);
11819
12241
  return presetPathExists(file) ? readFileSync(file, "utf8").trim() : "";
11820
12242
  }
@@ -11858,19 +12280,19 @@ function previewCharacterCard(files, options = {}) {
11858
12280
  };
11859
12281
  }
11860
12282
  }
11861
- function persistCharacterCard(presetRoot, converted, sourceText, avatar, convertedYaml = stringify(converted, { lineWidth: 0 })) {
12283
+ function persistCharacterCard(moduleRoot, converted, sourceText, avatar, convertedYaml = stringify(converted, { lineWidth: 0 })) {
11862
12284
  if (!validCardId(converted.id)) return {
11863
12285
  ok: false,
11864
12286
  message: `非法角色卡 id:${converted.id}`
11865
12287
  };
11866
- const parent = charactersDir(presetRoot);
11867
- const dir = cardDir(presetRoot, converted.id);
12288
+ const parent = charactersDir(moduleRoot);
12289
+ const dir = cardDir(moduleRoot, converted.id);
11868
12290
  let tmp;
11869
12291
  let installed = false;
11870
12292
  try {
11871
12293
  assertNoLinks(dir);
11872
12294
  const before = characterDirectoryDigest(dir);
11873
- if (presetPathExists(join(dir, "memory.md"))) readCharacterMemory(presetRoot, converted.id);
12295
+ if (presetPathExists(join(dir, "memory.md"))) readCharacterMemory(moduleRoot, converted.id);
11874
12296
  mkdirSync(parent, { recursive: true });
11875
12297
  tmp = mkdtempSync(join(parent, `.${converted.id}.tmp-`));
11876
12298
  if (before !== void 0) {
@@ -11950,14 +12372,14 @@ function characterDirectoryDigest(dir) {
11950
12372
  return hash.digest("hex");
11951
12373
  }
11952
12374
  /** 角色卡入库:PNG 原图(可选)+ 角色卡 JSON → 转换参数存 converted.yml。 */
11953
- function importCharacterCard(presetRoot, files, options = {}) {
12375
+ function importCharacterCard(moduleRoot, files, options = {}) {
11954
12376
  try {
11955
12377
  const prepared = prepareImport(files, "character", importChoices(options));
11956
12378
  if (prepared.state !== "ready") return {
11957
12379
  ok: false,
11958
12380
  message: "请先选择角色内容类型或提示顺序组"
11959
12381
  };
11960
- return persistCharacterCard(presetRoot, prepared.spec, prepared.sourceText ?? prepared.yaml, prepared.avatar, prepared.yaml);
12382
+ return persistCharacterCard(moduleRoot, prepared.spec, prepared.sourceText ?? prepared.yaml, prepared.avatar, prepared.yaml);
11961
12383
  } catch (error) {
11962
12384
  return {
11963
12385
  ok: false,
@@ -11966,10 +12388,10 @@ function importCharacterCard(presetRoot, files, options = {}) {
11966
12388
  }
11967
12389
  }
11968
12390
  /** 原始文件流导入:识别 PNG 魔数后在 host 侧提取角色卡,避免客户端 base64 膨胀 JSON。 */
11969
- function importCharacterCardFile(presetRoot, filePath, fileName = basename(filePath), options = {}) {
12391
+ function importCharacterCardFile(moduleRoot, filePath, fileName = basename(filePath), options = {}) {
11970
12392
  try {
11971
12393
  const buffer = readFileSync(filePath);
11972
- return importCharacterCard(presetRoot, [{
12394
+ return importCharacterCard(moduleRoot, [{
11973
12395
  path: basename(fileName),
11974
12396
  content: buffer.toString("base64"),
11975
12397
  encoding: "base64"
@@ -11981,16 +12403,16 @@ function importCharacterCardFile(presetRoot, filePath, fileName = basename(fileP
11981
12403
  };
11982
12404
  }
11983
12405
  }
11984
- /** 角色卡库清单 + 各卡在当前预设的导入状态。 */
11985
- function listCharacterCards(presetRoot, activeTemplate) {
11986
- const root = charactersDir(presetRoot);
12406
+ /** 角色卡库清单 + 各卡在当前模块的导入状态。 */
12407
+ function listCharacterCards(moduleRoot, activeTemplate) {
12408
+ const root = charactersDir(moduleRoot);
11987
12409
  let importedIds;
11988
12410
  try {
11989
- const presetFile = activeTemplate !== void 0 && activeTemplate.length > 0 ? join(presetRoot, activeTemplate, "preset.yml") : "";
11990
- const list = (existsSync(presetFile) ? parseDocument(readFileSync(presetFile, "utf8"), { logLevel: "silent" }).toJS().meta : void 0)?.importedCharacters;
12411
+ const moduleFile = activeTemplate !== void 0 && activeTemplate.length > 0 ? join(moduleRoot, activeTemplate, MODULE_DEFINITION_FILE) : "";
12412
+ const list = (existsSync(moduleFile) ? parseDocument(readFileSync(moduleFile, "utf8"), { logLevel: "silent" }).toJS().meta : void 0)?.importedCharacters;
11991
12413
  importedIds = new Set(Array.isArray(list) ? list.map(String) : []);
11992
12414
  } catch (error) {
11993
- throw new Error(`读取当前预设角色状态失败:${String(error)}`);
12415
+ throw new Error(`读取当前模块角色状态失败:${String(error)}`);
11994
12416
  }
11995
12417
  if (!presetPathExists(root)) return [];
11996
12418
  assertNoLinks(root);
@@ -12007,12 +12429,12 @@ function listCharacterCards(presetRoot, activeTemplate) {
12007
12429
  }).sort((a, b) => a.id.localeCompare(b.id));
12008
12430
  }
12009
12431
  /** 删除角色卡库条目。 */
12010
- function deleteCharacterCard(presetRoot, id) {
12432
+ function deleteCharacterCard(moduleRoot, id) {
12011
12433
  if (!validCardId(id)) return {
12012
12434
  ok: false,
12013
12435
  message: `非法角色卡 id:${id}`
12014
12436
  };
12015
- const dir = cardDir(presetRoot, id);
12437
+ const dir = cardDir(moduleRoot, id);
12016
12438
  if (!existsSync(dir)) return {
12017
12439
  ok: false,
12018
12440
  message: `角色卡 ${id} 不存在`
@@ -12031,17 +12453,17 @@ function deleteCharacterCard(presetRoot, id) {
12031
12453
  };
12032
12454
  }
12033
12455
  }
12034
- /** 角色卡参数导入当前预设:promptConfigs 合并(chara-<id>- 前缀防冲突)、
12456
+ /** 角色卡参数导入当前模块:promptConfigs 合并(chara-<id>- 前缀防冲突)、
12035
12457
  * params 合并(角色卡覆盖同键)、meta.importedCharacters 记录来源。 */
12036
- function applyCharacterToPreset(presetRoot, templateName, cardId) {
12458
+ function applyCharacterToPreset(moduleRoot, templateName, cardId) {
12037
12459
  if (!validCardId(cardId)) return {
12038
12460
  ok: false,
12039
12461
  message: `非法角色卡 id:${cardId}`
12040
12462
  };
12041
- const prefix = `chara-${cardId}-`;
12463
+ const prefix = `module-${cardId}-`;
12042
12464
  let personaOpened = false;
12043
12465
  try {
12044
- const spec = loadConverted(cardDir(presetRoot, cardId));
12466
+ const spec = loadImportSource(moduleRoot, cardId);
12045
12467
  if (spec === void 0) return {
12046
12468
  ok: false,
12047
12469
  message: `角色卡 ${cardId} 不存在或参数损坏`
@@ -12049,7 +12471,7 @@ function applyCharacterToPreset(presetRoot, templateName, cardId) {
12049
12471
  validateCharacterSpec(spec);
12050
12472
  const hasSystemSections = (spec.promptConfigs ?? []).some((config) => isRecord$1(config) && config.layer === "system-section");
12051
12473
  let count = 0;
12052
- withPresetDoc(join(presetRoot, templateName), (doc) => {
12474
+ withPresetDoc(join(moduleRoot, templateName), (doc) => {
12053
12475
  const current = doc.toJS();
12054
12476
  if (hasSystemSections) {
12055
12477
  const persona = current.persona;
@@ -12074,7 +12496,7 @@ function applyCharacterToPreset(presetRoot, templateName, cardId) {
12074
12496
  }];
12075
12497
  });
12076
12498
  for (const [key, value] of Object.entries(readPresetLayerSettings(spec))) doc.setIn(engineParamPath(key), value);
12077
- const memory = readCharacterMemory(presetRoot, cardId);
12499
+ const memory = readCharacterMemory(moduleRoot, cardId);
12078
12500
  const memoryEntry = buildCharacterMemoryEntry(spec, memory);
12079
12501
  const memoryConfig = memoryEntry === void 0 ? void 0 : {
12080
12502
  ...memoryEntry,
@@ -12128,21 +12550,21 @@ function applyCharacterToPreset(presetRoot, templateName, cardId) {
12128
12550
  };
12129
12551
  }
12130
12552
  }
12131
- /** 从当前预设移除角色卡参数:删前缀 promptConfigs、删该卡声明的 params 键、
12553
+ /** 从当前模块移除角色卡参数:删前缀 promptConfigs、删该卡声明的 params 键、
12132
12554
  * meta.importedCharacters 除名。 */
12133
- function removeCharacterFromPreset(presetRoot, templateName, cardId) {
12555
+ function removeCharacterFromPreset(moduleRoot, templateName, cardId) {
12134
12556
  if (!validCardId(cardId)) return {
12135
12557
  ok: false,
12136
12558
  message: `非法角色卡 id:${cardId}`
12137
12559
  };
12138
- const prefix = `chara-${cardId}-`;
12560
+ const prefixes = importPrefixes(cardId);
12139
12561
  try {
12140
- const spec = loadConverted(cardDir(presetRoot, cardId));
12562
+ const spec = loadImportSource(moduleRoot, cardId);
12141
12563
  let removed = 0;
12142
- withPresetDoc(join(presetRoot, templateName), (doc) => {
12564
+ withPresetDoc(join(moduleRoot, templateName), (doc) => {
12143
12565
  const current = doc.toJS();
12144
12566
  const kept = (Array.isArray(current.promptConfigs) ? current.promptConfigs : []).filter((config) => {
12145
- const isCard = config !== null && typeof config === "object" && String(config.id ?? "").startsWith(prefix);
12567
+ const isCard = config !== null && typeof config === "object" && prefixes.some((base) => String(config.id ?? "").startsWith(base));
12146
12568
  if (isCard) removed += 1;
12147
12569
  return !isCard;
12148
12570
  });
@@ -12165,7 +12587,7 @@ function removeCharacterFromPreset(presetRoot, templateName, cardId) {
12165
12587
  const records = recordedCharacterModules(current.meta);
12166
12588
  const recorded = Object.values(records).flat();
12167
12589
  if (recorded.length > 0) {
12168
- const others = modulesClaimedByOtherCards(presetRoot, remainingCards, current.meta, cardId);
12590
+ const others = modulesClaimedByOtherCards(moduleRoot, remainingCards, current.meta, cardId);
12169
12591
  const after = doc.toJS();
12170
12592
  const context = {
12171
12593
  configs: kept.filter(isRecord$1),
@@ -13466,27 +13888,25 @@ function officialChannelText(text, label, registry, warnOnce) {
13466
13888
  }
13467
13889
  return result.text;
13468
13890
  }
13469
- /** 文本型层分组:merged 模式按位置分组,否则每条独立。 */
13891
+ /** 只合并排序后同来源、同官方档位和位置的连续 merged 卡,不跨过穿插段。 */
13470
13892
  function textLayerGroups(configs) {
13471
- const sorted = [...configs].sort((a, b) => a.order - b.order);
13893
+ const sorted = [...configs].sort(compareTextPlacement);
13472
13894
  const groups = [];
13473
- const index = /* @__PURE__ */ new Map();
13474
13895
  for (const config of sorted) {
13475
- const key = config.mergeMode === "merged" ? `merged:${config.position ?? ""}` : void 0;
13476
- if (key === void 0) {
13477
- groups.push([config]);
13478
- continue;
13479
- }
13480
- let at = index.get(key);
13481
- if (at === void 0) {
13482
- at = groups.length;
13483
- index.set(key, at);
13484
- groups.push([]);
13485
- }
13486
- groups[at].push(config);
13896
+ const previous = groups.at(-1);
13897
+ const base = previous?.[0];
13898
+ if (config.mergeMode === "merged" && base?.mergeMode === "merged" && config.sourceModuleId === base.sourceModuleId && config.position === base.position && config.order === base.order) previous.push(config);
13899
+ else groups.push([config]);
13487
13900
  }
13488
13901
  return groups;
13489
13902
  }
13903
+ /** 官方同 order 按 name 排序:默认名编码安全整数序号,显式名仍由作者拥有。 */
13904
+ function textRegistrationName(config, field) {
13905
+ const explicit = config.params?.[field];
13906
+ if (typeof explicit === "string" && explicit.length > 0) return explicit;
13907
+ if (config.sourceModuleId === void 0) return config.id;
13908
+ return `prompt-tool:${String(config.sequence).padStart(16, "0")}:${config.sourceModuleId}:${config.id}`;
13909
+ }
13490
13910
  /** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
13491
13911
  function wireSystemSections(ctx, configs, registry, warnOnce) {
13492
13912
  const disposers = [];
@@ -13503,7 +13923,7 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
13503
13923
  if (!dynamic && groupText.length === 0) continue;
13504
13924
  const text = dynamic ? (context) => group.filter((config) => matchesAgentScope(config, context?.agent)).map((config) => configText(config, registry, warnOnce, context)).filter((item) => item.length > 0).join("\n\n") : groupText;
13505
13925
  keepDisposer(ctx, systemPrompt.section({
13506
- name: typeof base.params?.sectionName === "string" && base.params.sectionName.length > 0 ? base.params.sectionName : base.id,
13926
+ name: textRegistrationName(base, "sectionName"),
13507
13927
  order: base.order,
13508
13928
  text,
13509
13929
  ...base.params?.complete === true ? { complete: true } : {}
@@ -13533,7 +13953,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
13533
13953
  const text = dynamic ? render : render();
13534
13954
  if (!dynamic && text.length === 0) continue;
13535
13955
  keepDisposer(ctx, systemPrompt.context({
13536
- name: typeof base.params?.contextName === "string" && base.params.contextName.length > 0 ? base.params.contextName : base.id,
13956
+ name: textRegistrationName(base, "contextName"),
13537
13957
  order: base.order,
13538
13958
  text
13539
13959
  }), `${name$1}: context ${base.id}`);
@@ -13541,7 +13961,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
13541
13961
  warnOnce(`${name$1}: runtime-context config ${base.id} failed: ${String(error?.message ?? error)}`);
13542
13962
  }
13543
13963
  }
13544
- const placeholders = configs.filter(needsResolver).sort((a, b) => a.order - b.order);
13964
+ const placeholders = configs.filter(needsResolver).sort(compareTextPlacement);
13545
13965
  if (placeholders.length === 0) return disposers;
13546
13966
  const slotVariable = `pt_runtime_${newMessageId("context").replace(/[^a-z0-9_]/gi, "_").toLowerCase()}`;
13547
13967
  const slotText = `{{${slotVariable}}}`;
@@ -13552,7 +13972,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
13552
13972
  }, `${name$1}: runtime-context lifecycle`);
13553
13973
  keepDisposer(ctx, systemPrompt.variable(slotVariable, () => ""), `${name$1}: runtime-context placeholder`);
13554
13974
  for (const config of placeholders) try {
13555
- const contextName = typeof config.params?.contextName === "string" && config.params.contextName.length > 0 ? config.params.contextName : config.id;
13975
+ const contextName = textRegistrationName(config, "contextName");
13556
13976
  keepDisposer(ctx, systemPrompt.context({
13557
13977
  name: contextName,
13558
13978
  order: config.order,
@@ -13631,18 +14051,17 @@ function applyAgentRequestParams(params, base) {
13631
14051
  }
13632
14052
  /** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
13633
14053
  function wireAgentRequests(ctx, configs, warnOnce) {
13634
- const disposers = [];
13635
- for (const config of configs) disposers.push(ctx.on("agent/request", async (payload, next) => {
13636
- const base = await next();
13637
- try {
13638
- if (!matchesAgentScope(config, payload?.agent)) return base;
13639
- return applyAgentRequestParams(config.params, base);
14054
+ if (configs.length === 0) return [];
14055
+ const ordered = configs.some((config) => config.sequence !== void 0) ? configs : [...configs].reverse();
14056
+ return [ctx.on("agent/request", async (payload, next) => {
14057
+ let result = await next();
14058
+ for (const config of ordered) try {
14059
+ if (matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result);
13640
14060
  } catch (error) {
13641
14061
  warnOnce(`${name$1}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`);
13642
- return base;
13643
14062
  }
13644
- }));
13645
- return disposers;
14063
+ return result;
14064
+ })];
13646
14065
  }
13647
14066
  /** 把流替换为提示词配置文本的最小合法 chunk 序列。 */
13648
14067
  async function* replacedStream(text) {
@@ -13815,23 +14234,28 @@ function createTurnStopBudget() {
13815
14234
  function wireTurnStops(ctx, configs, warnOnce) {
13816
14235
  const disposers = [];
13817
14236
  if (configs.length === 0) return disposers;
13818
- const budget = createTurnStopBudget();
13819
- for (const config of configs) disposers.push(ctx.on("agent/turn-stopping", ({ agent, turn } = {}) => {
13820
- try {
13821
- const session = agent?.session;
13822
- if (session?.id === void 0 || typeof agent.steer !== "function") return;
13823
- if (!matchesAgentScope(config, agent)) return;
13824
- const entry = budget.entry(session.id, turn);
13825
- if (!budget.available(entry, turn)) return;
13826
- if (!conditionHit(config, { assistantText: lastAssistantText(session) })) return;
13827
- const text = layerText(config, agent, warnOnce);
13828
- if (text.length === 0) return;
13829
- budget.claim(entry, turn);
13830
- agent.steer(pluginMessage(`turn-stop-${config.id}`, text, `turn-stop ${config.id}`));
13831
- } catch (error) {
13832
- warnOnce(`${name$1}: turn-stop config ${config.id} failed: ${String(error?.message ?? error)}`);
13833
- }
13834
- }));
14237
+ const budgets = /* @__PURE__ */ new Map();
14238
+ for (const config of configs) {
14239
+ const source = config.sourceModuleId ?? "";
14240
+ if (!budgets.has(source)) budgets.set(source, createTurnStopBudget());
14241
+ const budget = budgets.get(source);
14242
+ disposers.push(ctx.on("agent/turn-stopping", ({ agent, turn } = {}) => {
14243
+ try {
14244
+ const session = agent?.session;
14245
+ if (session?.id === void 0 || typeof agent.steer !== "function") return;
14246
+ if (!matchesAgentScope(config, agent)) return;
14247
+ const entry = budget.entry(session.id, turn);
14248
+ if (!budget.available(entry, turn)) return;
14249
+ if (!conditionHit(config, { assistantText: lastAssistantText(session) })) return;
14250
+ const text = layerText(config, agent, warnOnce);
14251
+ if (text.length === 0) return;
14252
+ budget.claim(entry, turn);
14253
+ agent.steer(pluginMessage(`turn-stop-${config.id}`, text, `turn-stop ${config.id}`));
14254
+ } catch (error) {
14255
+ warnOnce(`${name$1}: turn-stop config ${config.id} failed: ${String(error?.message ?? error)}`);
14256
+ }
14257
+ }));
14258
+ }
13835
14259
  return disposers;
13836
14260
  }
13837
14261
  /** 沿官方持久 session 血缘找根会话,不根据当前 UI 会话或预设名称猜测。 */
@@ -13934,6 +14358,7 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
13934
14358
  * 变量注册走 keepDisposer(随 ctx fiber 释放),不在本函数的回收面内。
13935
14359
  */
13936
14360
  function wireLayers(ctx, configs, warnOnce) {
14361
+ configs = [...configs].sort(compareConfigSequence);
13937
14362
  const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === "system-section" || config.layer === "runtime-context"), warnOnce);
13938
14363
  const registered = [
13939
14364
  wireSystemSections(ctx, configs.filter((config) => config.layer === "system-section"), registry, warnOnce),
@@ -14482,22 +14907,18 @@ async function runPreStepBatch(options) {
14482
14907
  } catch (error) {
14483
14908
  warnOnce(`${name}: config ${String(config?.id ?? "<unknown>")} failed, skipping: ${String(error && error.message || error)}`);
14484
14909
  }
14485
- due.sort((a, b) => a.config.order - b.config.order);
14910
+ due.sort((a, b) => compareConfigSequence(a.config, b.config));
14486
14911
  const orderedGroups = [];
14487
- const groupIndex = /* @__PURE__ */ new Map();
14912
+ const lastAtPosition = /* @__PURE__ */ new Map();
14488
14913
  for (const entry of due) {
14489
- const group = entry.config.mergeMode === "merged" ? mergedIdentity(entry.config) : void 0;
14490
- if (group === void 0) {
14491
- orderedGroups.push([entry]);
14492
- continue;
14493
- }
14494
- let index = groupIndex.get(group);
14495
- if (index === void 0) {
14496
- index = orderedGroups.length;
14497
- groupIndex.set(group, index);
14498
- orderedGroups.push([]);
14914
+ const previous = lastAtPosition.get(entry.config.position);
14915
+ const base = previous?.[0].config;
14916
+ if (entry.config.mergeMode === "merged" && base?.mergeMode === "merged" && entry.config.sourceModuleId === base.sourceModuleId) previous.push(entry);
14917
+ else {
14918
+ const group = [entry];
14919
+ orderedGroups.push(group);
14920
+ lastAtPosition.set(entry.config.position, group);
14499
14921
  }
14500
- orderedGroups[index].push(entry);
14501
14922
  }
14502
14923
  const planned = [];
14503
14924
  for (const group of orderedGroups) {
@@ -14569,11 +14990,10 @@ async function runPreStepBatch(options) {
14569
14990
  * @param options.officialInstructions 本 mount 的组合是否仍挂着官方指令加载行
14570
14991
  * (负责人冲突事实;协调器据此拒绝同时注入文件正文)。
14571
14992
  */
14572
- function applyPromptConfigs(ctx, configs, options = {}) {
14993
+ function effectivePromptConfigs(configs) {
14573
14994
  const list = configs.filter((config) => config !== void 0 && config !== null);
14574
- const sourceId = typeof options.sourceId === "string" && options.sourceId.length > 0 ? options.sourceId : `preset:${String(list[0]?.id ?? "unknown")}`;
14575
14995
  const claimedGroups = /* @__PURE__ */ new Set();
14576
- const effectiveList = list.filter((config) => {
14996
+ return list.filter((config) => {
14577
14997
  if (config.enabled === false) return false;
14578
14998
  if (config.group !== void 0 && config.exclusive === true) {
14579
14999
  if (claimedGroups.has(config.group)) return false;
@@ -14581,6 +15001,28 @@ function applyPromptConfigs(ctx, configs, options = {}) {
14581
15001
  }
14582
15002
  return true;
14583
15003
  });
15004
+ }
15005
+ /** 多模块仍各自拥有来源/互斥组;只有非 pre-step 层汇总接线,避免模块块状执行。 */
15006
+ function applyPromptConfigSources(ctx, sources, options = {}) {
15007
+ const selected = sources.map((source) => ({
15008
+ ...source,
15009
+ configs: effectivePromptConfigs(source.configs)
15010
+ }));
15011
+ const releases = selected.map((source) => applyPromptConfigs(ctx, source.configs, {
15012
+ ...options,
15013
+ ...source,
15014
+ layers: false
15015
+ }));
15016
+ const releaseLayers = wireLayers(ctx, selected.flatMap((source) => source.configs).filter((config) => config.layer !== "pre-step"), createWarnOnce(ctx, name));
15017
+ return () => {
15018
+ for (const release of releases) release();
15019
+ releaseLayers();
15020
+ };
15021
+ }
15022
+ function applyPromptConfigs(ctx, configs, options = {}) {
15023
+ const list = configs.filter((config) => config !== void 0 && config !== null);
15024
+ const sourceId = typeof options.sourceId === "string" && options.sourceId.length > 0 ? options.sourceId : `preset:${String(list[0]?.id ?? "unknown")}`;
15025
+ const effectiveList = effectivePromptConfigs(list);
14584
15026
  const injectedMemo = /* @__PURE__ */ new Map();
14585
15027
  const main = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false });
14586
15028
  const withSubagents = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: true });
@@ -14617,7 +15059,7 @@ function applyPromptConfigs(ctx, configs, options = {}) {
14617
15059
  injectedMemo.clear();
14618
15060
  };
14619
15061
  keepDisposer(ctx, release);
14620
- const releaseLayers = wireLayers(ctx, effectiveList.filter((config) => config.layer !== "pre-step"), warnOnce);
15062
+ const releaseLayers = options.layers === false ? () => {} : wireLayers(ctx, effectiveList.filter((config) => config.layer !== "pre-step"), warnOnce);
14621
15063
  let registeredCoordinator = coordinatorOf(ctx);
14622
15064
  if (registeredCoordinator !== void 0) registration = registeredCoordinator.registerPreset(ctx, sourceId, source) ?? null;
14623
15065
  const preStepDisposer = ctx.on("agent/pre-step", async (payload, next) => {
@@ -16031,17 +16473,17 @@ function compileDeclarations(specs, context = {}) {
16031
16473
  //#endregion
16032
16474
  //#region src/host/write-preset.ts
16033
16475
  /**
16034
- * write-preset — 单一参数 YAML 驱动的官方预设目录物化器。
16476
+ * write-preset — 单一参数 YAML 驱动的模块目录物化器。
16035
16477
  *
16036
- * 用户只需编写 preset/<template>/preset.yml(纯参数):
16478
+ * 用户只需编写 modules/<template>/module.yml(纯参数):
16037
16479
  * meta + content + modules(模块清单)+ params(直读参数)+ 可选 promptConfigs 覆盖。
16038
16480
  * 默认提示词配置与组合 token 都由引擎按 params 生成,参数文件不含任何模板语法。
16039
- * 输出 = 官方对齐布局:presetDir/<template>/(预设目录,agent.cordis.yml 组合本体
16481
+ * 输出 = 官方对齐布局:moduleDir/<template>/(模块目录,agent.cordis.yml 组合本体
16040
16482
  * 直接可挂载)。共享引擎自阶段 2 起由插件包提供(组合行引用包内说明符),不再物化。
16041
16483
  */
16042
16484
  const ENGINE_DIR = packageEngineDir();
16043
16485
  const RENDER_STAMP = `# prompt-tool:render v5`;
16044
- /** 剥离文本中的预设级变量引用({{key}} → 空串);内置变量({{DSH_HOME}} 等)保留。
16486
+ /** 剥离文本中的模块级变量引用({{key}} → 空串);内置变量({{DSH_HOME}} 等)保留。
16045
16487
  * 模板变量插值停用时由 writePreset 调用,避免 {{key}} 残留导致官方渲染 unknown variable。 */
16046
16488
  function stripVariableRefs(text, keys) {
16047
16489
  return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (whole, key) => keys.has(key) ? "" : whole);
@@ -16050,6 +16492,28 @@ function stripVariableRefs(text, keys) {
16050
16492
  * ST 导入的设置 dump(SPresetSettings 等)动辄数百 KB 且 enabled=false——
16051
16493
  * 注入与展示都不需要正文,全量落盘只拖慢 rebuild 与引擎启动扫描。 */
16052
16494
  const DISABLED_TEXT_SLIM_THRESHOLD = 32768;
16495
+ /** 按模块身份重建:定义与正文均读取该模块,不叠加工作台当前编辑态。 */
16496
+ function materializeModule(id, options) {
16497
+ if (enabledModuleIds(options.moduleDir).includes(id)) appendModuleConfigOrder(options.moduleDir, id);
16498
+ const dir = resolveModuleDir(id, options.moduleDir);
16499
+ const spec = loadModuleSpec(dir);
16500
+ const content = (file, fallback) => {
16501
+ try {
16502
+ return readFileSync(join(dir, file), "utf8") || fallback;
16503
+ } catch {
16504
+ return fallback;
16505
+ }
16506
+ };
16507
+ return writePreset(content("preset.md", asString(spec.content?.presetText)), {
16508
+ moduleDir: options.moduleDir,
16509
+ presetOrder: options.presetOrder,
16510
+ presetTemplate: id,
16511
+ outputId: id,
16512
+ promptConfigs: [],
16513
+ agentsInstructionText: content("agents.md", asString(spec.content?.agentsText)),
16514
+ warn: options.warn
16515
+ });
16516
+ }
16053
16517
  /** Windows 瞬时文件锁(杀软/宿主短读)下的重试:rename/写文件失败最多重试 3 次、间隔 400ms。 */
16054
16518
  function withLockRetry(action, retries = 3) {
16055
16519
  for (let attempt = 0;; attempt++) try {
@@ -16092,19 +16556,16 @@ function syncDirInPlace(srcDir, destDir) {
16092
16556
  /**
16093
16557
  * 写盘用的 runtime 参数对象。
16094
16558
  *
16095
- * **导出供测试固化其归一化行为**(`test/shared/engine-param-schema.test.mjs` 的特性断言):
16096
- * 这些逐键归一化**不是** `ENGINE_PARAM_DEFINITIONS` 类型信息的重复拷贝——规则取决于运行时的
16097
- * 需要,且与 `index.ts` 的 `reloadPresetParams` **不同**(例如 `firstTurnAnchor` 在这里
16098
- * 保留调用方显式的 `false`,读回时却布尔化成 `params.x === true`)。所以它必须被逐键钉住,
16099
- * 不能被「按 kind 统一驱动」取代。
16559
+ * 保留外部调用方的显式覆盖语义;普通模块重建不再回传定义中的参数副本。
16560
+ * 未提供与显式 false / 0 / 空字符串不同,不能用编辑器默认值补齐。
16100
16561
  */
16101
16562
  function runtimeOf(options, prompt) {
16102
16563
  /** 未提供的参数保持 undefined:resolvePresetParams 跳过 undefined 键,
16103
- * 预设 preset.yml 的 params/model 段才是缺省值来源。写成 false/''/true 会把
16564
+ * 模块 module.yml 的 params/model 段才是缺省值来源。写成 false/''/true 会把
16104
16565
  * 「调用方没给」冒充成「调用方要求」,导入与离线物化时覆盖作者定义。 */
16105
16566
  const providedBoolean = (value) => typeof value === "boolean" ? value : void 0;
16106
16567
  return {
16107
- ...Object.fromEntries(ENGINE_PARAM_KEYS.map((key) => [key, options[key]])),
16568
+ ...Object.fromEntries(WRITER_PARAM_KEYS.map((key) => [key, options[key]])),
16108
16569
  promptText: prompt,
16109
16570
  firstTurnAnchor: providedBoolean(options.firstTurnAnchor),
16110
16571
  firstTurnCustom: providedBoolean(options.firstTurnCustom),
@@ -16127,7 +16588,7 @@ function runtimeOf(options, prompt) {
16127
16588
  firstTurnWord: typeof options.firstTurnWord === "string" && options.firstTurnWord.length > 0 ? options.firstTurnWord : void 0
16128
16589
  };
16129
16590
  }
16130
- /** 手写/导入预设恢复路径:与保存方完整编译同源,但坏定义仍逐条告警跳过。 */
16591
+ /** 手写/导入模块恢复路径:与保存方完整编译同源,但坏定义仍逐条告警跳过。 */
16131
16592
  function materializeCustomTool(tool, warn) {
16132
16593
  try {
16133
16594
  return compileCustomTool(tool);
@@ -16136,18 +16597,18 @@ function materializeCustomTool(tool, warn) {
16136
16597
  return;
16137
16598
  }
16138
16599
  }
16139
- /** 把任意单一参数预设模板物化到生成目录;全部失败 fail loud。 */
16600
+ /** 把任意单一参数模块模板物化到生成目录;全部失败 fail loud。 */
16140
16601
  function writePreset(prompt, options) {
16141
- const presetDir = options.presetDir.trim().length > 0 ? options.presetDir : DEFAULT_PRESET_DIR;
16142
- const templateName = typeof options.presetTemplate === "string" && options.presetTemplate.trim().length > 0 ? options.presetTemplate.trim() : DEFAULT_PRESET_ID;
16602
+ const moduleDir = options.moduleDir.trim().length > 0 ? options.moduleDir : MODULES_DIR;
16603
+ const templateName = typeof options.presetTemplate === "string" && options.presetTemplate.trim().length > 0 ? options.presetTemplate.trim() : DEFAULT_MODULE_ID;
16143
16604
  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-]*$/`);
16144
16605
  const outputId = typeof options.outputId === "string" && options.outputId.trim().length > 0 ? options.outputId.trim() : templateName;
16145
16606
  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-]*$/`);
16146
16607
  assertPresetId(templateName);
16147
16608
  assertPresetId(outputId);
16148
- const templateDir = options.sourceDir ?? resolvePresetDir(templateName, presetDir);
16609
+ const templateDir = options.sourceDir ?? resolveModuleDir(templateName, moduleDir);
16149
16610
  assertPresetTree(templateDir);
16150
- let spec = loadPresetSpec(templateDir);
16611
+ const spec = loadModuleSpec(templateDir, { legacyParams: false });
16151
16612
  if (Array.isArray(spec.meta?.stWarnings)) {
16152
16613
  for (const warning of spec.meta.stWarnings) if (typeof warning === "string") options.warn?.(`prompt-tool: ST 导入兼容提示:${warning}`);
16153
16614
  }
@@ -16165,26 +16626,32 @@ function writePreset(prompt, options) {
16165
16626
  }
16166
16627
  const runtime = runtimeOf(options, prompt);
16167
16628
  const params = resolvePresetParams(spec, runtime);
16168
- const targetDir = assertPresetDirectory(presetDir, outputId, true);
16169
- mkdirSync(presetDir, { recursive: true });
16170
- const tmpDir = mkdtempSync(join(presetDir, `.${outputId}.tmp-`));
16629
+ const legacy = resolveLegacyPromptConfigs(spec, {
16630
+ moduleDir: templateDir,
16631
+ prompt,
16632
+ overrides: runtime,
16633
+ promptConfigs: options.promptConfigs
16634
+ });
16635
+ for (const warning of legacy.warnings) (options.warn ?? console.warn)(`prompt-tool: ${warning}`);
16636
+ const targetDir = assertModuleDirectory(moduleDir, outputId, true);
16637
+ mkdirSync(moduleDir, { recursive: true });
16638
+ const tmpDir = mkdtempSync(join(moduleDir, `.${outputId}.tmp-`));
16171
16639
  const outDir = tmpDir;
16172
16640
  try {
16173
- const composition = renderComposition(Array.isArray(options.promptConfigs) && options.promptConfigs.length > 0 ? {
16641
+ const composition = renderComposition({
16174
16642
  ...spec,
16175
- promptConfigs: options.promptConfigs
16176
- } : spec, runtime, templateDir);
16643
+ promptConfigs: legacy.configs
16644
+ }, runtime, templateDir);
16177
16645
  assertCompositionArray(composition, spec);
16178
16646
  const subComposition = rewritePresetEngineReferences(composition, outputId, engineModuleFileNames(ENGINE_DIR));
16179
16647
  writeFileSync(join(outDir, "agent.cordis.yml"), `${RENDER_STAMP}\n${subComposition}`, "utf8");
16180
16648
  const meta = spec.meta !== null && typeof spec.meta === "object" ? spec.meta : {};
16181
- const sourceYamlPath = options.sourceDir !== void 0 || !existsSync(join(targetDir, "preset.yml")) ? join(templateDir, "preset.yml") : join(targetDir, "preset.yml");
16649
+ const sourceYamlPath = options.sourceDir !== void 0 || !existsSync(join(targetDir, "module.yml")) ? join(templateDir, MODULE_DEFINITION_FILE) : join(targetDir, MODULE_DEFINITION_FILE);
16182
16650
  const existingPresetYaml = existsSync(sourceYamlPath) ? readFileSync(sourceYamlPath, "utf8") : void 0;
16183
16651
  if (existingPresetYaml !== void 0 && existingPresetYaml.trim().length > 0) {
16184
16652
  const doc = parseDocument(existingPresetYaml, { logLevel: "silent" });
16185
16653
  if (doc.errors.length > 0) throw new Error(`invalid preset.yml: ${doc.errors[0].message}`);
16186
16654
  doc.set("id", outputId);
16187
- doc.setIn(["order"], options.presetOrder);
16188
16655
  const ensureMetaKey = (key, value) => {
16189
16656
  if (value === void 0 || value === null) return;
16190
16657
  const current = doc.get(key);
@@ -16193,18 +16660,15 @@ function writePreset(prompt, options) {
16193
16660
  ensureMetaKey("name", typeof spec.name === "string" && spec.name.length > 0 ? spec.name : void 0);
16194
16661
  ensureMetaKey("description", typeof spec.description === "string" && spec.description.length > 0 ? spec.description : void 0);
16195
16662
  ensureMetaKey("meta", Object.keys(meta).length > 0 ? meta : void 0);
16196
- writeFileSync(join(outDir, "preset.yml"), doc.toString(), "utf8");
16197
- } else writeFileSync(join(outDir, "preset.yml"), stringify({
16198
- ...meta,
16199
- order: options.presetOrder
16200
- }) + "\n", "utf8");
16663
+ writeFileSync(join(outDir, MODULE_DEFINITION_FILE), doc.toString(), "utf8");
16664
+ } else writeFileSync(join(outDir, MODULE_DEFINITION_FILE), stringify(meta) + "\n", "utf8");
16201
16665
  if (prompt.trim().length > 0) writeFileSync(join(outDir, "preset.md"), prompt, "utf8");
16202
16666
  else if (existsSync(join(templateDir, "preset.md"))) cpSync(join(templateDir, "preset.md"), join(outDir, "preset.md"));
16203
16667
  if (typeof options.agentsInstructionText === "string" && options.agentsInstructionText.trim().length > 0) writeFileSync(join(outDir, "agents.md"), options.agentsInstructionText, "utf8");
16204
16668
  else if (existsSync(join(templateDir, "agents.md"))) cpSync(join(templateDir, "agents.md"), join(outDir, "agents.md"));
16205
16669
  const templateEntries = readdirSync(templateDir, { withFileTypes: true });
16206
16670
  for (const entry of templateEntries) {
16207
- if (entry.name === "preset.yml" || entry.name === "agent.cordis.yml" || entry.name === "preset.md" || entry.name === "agents.md") continue;
16671
+ if (entry.name === "module.yml" || entry.name === "agent.cordis.yml" || entry.name === "preset.md" || entry.name === "agents.md") continue;
16208
16672
  const source = join(templateDir, entry.name);
16209
16673
  const target = join(outDir, entry.name);
16210
16674
  if (entry.isDirectory()) cpSync(source, target, {
@@ -16213,84 +16677,24 @@ function writePreset(prompt, options) {
16213
16677
  });
16214
16678
  else cpSync(source, target, { force: true });
16215
16679
  }
16216
- const promptConfigsDir = join(outDir, "prompt-configs");
16680
+ const promptConfigsDir = join(outDir, MODULE_CONFIGS_DIR);
16217
16681
  rmSync(promptConfigsDir, {
16218
16682
  recursive: true,
16219
16683
  force: true
16220
16684
  });
16221
16685
  mkdirSync(promptConfigsDir, { recursive: true });
16222
- const templateConfigs = Array.isArray(spec.promptConfigs) ? spec.promptConfigs : [];
16223
- let templateDefaults;
16224
- if (templateConfigs.length > 0) templateDefaults = templateConfigs.map((config) => {
16225
- const next = {
16226
- ...config,
16227
- params: { ...config.params }
16228
- };
16229
- if (next.id === "near-anchor") {
16230
- next.enabled = params.firstTurnAnchor === true;
16231
- next.params = {
16232
- ...next.params,
16233
- useCustom: params.firstTurnCustom === true,
16234
- text: asString(params.firstTurnText),
16235
- buildPattern: asString(params.buildPattern),
16236
- complexPattern: asString(params.complexPattern),
16237
- firstTurnBuild: asString(params.firstTurnBuild),
16238
- firstTurnInspect: asString(params.firstTurnInspect),
16239
- firstTurnDeep: asString(params.firstTurnDeep)
16240
- };
16241
- } else if (next.id === "router-guide") {
16242
- const guideEnabled = params.guideEnabled === true;
16243
- next.enabled = guideEnabled;
16244
- const useCustom = guideEnabled && params.guideCustom === true;
16245
- next.modelScope = useCustom ? "all" : "flash";
16246
- next.params = {
16247
- ...next.params,
16248
- useCustom,
16249
- text: asString(params.guideText),
16250
- complexPattern: asString(params.complexPattern),
16251
- guideWeak: asString(params.guideWeak),
16252
- guideDeep: asString(params.guideDeep)
16253
- };
16254
- }
16255
- return next;
16256
- });
16257
- else templateDefaults = [];
16258
- const merged = mergePromptConfigs(modelRequestConfigs(params), templateDefaults, options.promptConfigs);
16686
+ const merged = mergePromptConfigs(modelRequestConfigs(params), legacy.configs);
16259
16687
  const presetVariables = {};
16260
16688
  for (const [key, value] of Object.entries(spec.variables ?? {})) if (typeof value === "string") presetVariables[key] = value;
16261
16689
  const variablesEnabled = spec.variablesEnabled !== false;
16262
16690
  const presetVariableKeys = new Set(Object.keys(presetVariables));
16263
16691
  if (variablesEnabled && presetVariableKeys.size > 0) writeFileSync(join(promptConfigsDir, "variables.yml"), stringify(presetVariables), "utf8");
16692
+ const configOrder = readConfigOrder(spec.configOrder);
16264
16693
  for (const [index, source] of merged.entries()) {
16265
16694
  const config = {
16266
16695
  ...source,
16267
16696
  params: source.params !== void 0 && source.params !== null ? { ...source.params } : source.params
16268
16697
  };
16269
- if (config.id === "prompt-injector") {
16270
- delete config.text;
16271
- config.texts = [];
16272
- config.enabled = params.injectPrompt !== false && prompt.trim().length > 0;
16273
- const explicitWord = asString(config.params?.firstTurnWord ?? params.firstTurnWord, "");
16274
- const signalWord = (text) => {
16275
- const sentence = /the exact sentence:\s*([A-Za-z]+)/i.exec(text);
16276
- if (sentence !== null && sentence[1] !== void 0) return sentence[1].toLowerCase();
16277
- const first = /^\P{L}*([\p{L}]+)/u.exec(text.trim());
16278
- return first !== null && first[1] !== void 0 ? first[1].toLowerCase() : void 0;
16279
- };
16280
- const derivedWords = [...new Set([
16281
- signalWord(asString(params.firstTurnText)),
16282
- signalWord(asString(params.firstTurnBuild)),
16283
- signalWord(asString(params.firstTurnInspect)),
16284
- signalWord(asString(params.firstTurnDeep))
16285
- ].filter((word) => word !== void 0))];
16286
- const anchorWords = explicitWord.length > 0 ? [explicitWord] : derivedWords;
16287
- config.params = {
16288
- ...config.params,
16289
- text: prompt,
16290
- firstTurnWord: explicitWord,
16291
- anchorWords
16292
- };
16293
- }
16294
16698
  if (!variablesEnabled && presetVariableKeys.size > 0) {
16295
16699
  const strip = (item) => stripVariableRefs(item, presetVariableKeys);
16296
16700
  config.texts = (config.texts ?? []).map(strip);
@@ -16305,16 +16709,8 @@ function writePreset(prompt, options) {
16305
16709
  delete config.text;
16306
16710
  config.texts = [];
16307
16711
  }
16308
- const binding = MANAGED_CONFIG_FIELDS.find((entry) => entry.configId === config.id);
16309
- const fieldSources = binding === void 0 ? void 0 : {
16310
- configId: config.id,
16311
- fields: binding.fields.map(({ path }) => ({
16312
- path,
16313
- source: templateDefaults.includes(source) ? "preset-param" : "prompt-config"
16314
- }))
16315
- };
16316
- const provenance = fieldSources === void 0 ? "" : "\n" + stringify({ fieldSources });
16317
- writeFileSync(join(promptConfigsDir, configFileName(index * 10, config.id)), renderPromptConfigYaml(config) + provenance, "utf8");
16712
+ const sequence = configOrder[config.id] ?? index * 10;
16713
+ writeFileSync(join(promptConfigsDir, configFileName(sequence, config.id)), renderPromptConfigYaml(config), "utf8");
16318
16714
  }
16319
16715
  const customToolsDir = join(outDir, "custom-tools");
16320
16716
  rmSync(customToolsDir, {
@@ -16344,7 +16740,7 @@ function writePreset(prompt, options) {
16344
16740
  rmSync(triggersFile, { force: true });
16345
16741
  if (Array.isArray(spec.triggers) && spec.triggers.length > 0) writeFileSync(triggersFile, stringify(spec.triggers, { lineWidth: 0 }), "utf8");
16346
16742
  if (options.materializeOnly) return outDir;
16347
- const backupDir = join(presetDir, `.${templateName}.bak-${Date.now().toString(36)}`);
16743
+ const backupDir = join(moduleDir, `.${templateName}.bak-${Date.now().toString(36)}`);
16348
16744
  let oldMoved = false;
16349
16745
  let inPlace = false;
16350
16746
  if (existsSync(targetDir)) try {
@@ -16384,10 +16780,17 @@ function writePreset(prompt, options) {
16384
16780
  }
16385
16781
  }
16386
16782
  //#endregion
16387
- //#region src/host/preset-package.ts
16388
- /** 预设交换:有界资源、完整候选与可恢复提交,Web 和 CLI 共用。 */
16783
+ //#region src/host/module-package.ts
16784
+ /** 模块交换:有界资源、完整候选与可恢复提交,Web 和 CLI 共用。 */
16389
16785
  const MANIFEST = "prompt-tool-package.json";
16390
16786
  const PACKAGE_VERSION = 1;
16787
+ /**
16788
+ * 包内定义文件名:写入用新名,**读取接受历史名**。
16789
+ *
16790
+ * 包是可分享的外部产物,用户手里可能有旧版导出的包(清单 `definition: preset.yml`);
16791
+ * 导入是信任边界,拒绝它等于把用户已有的包判为损坏。
16792
+ */
16793
+ const DEFINITION_NAMES = [MODULE_DEFINITION_FILE, "preset.yml"];
16391
16794
  const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
16392
16795
  const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16393
16796
  var AssetTransferError = class extends Error {
@@ -16407,7 +16810,7 @@ function assertTransferId(id) {
16407
16810
  function ownedFile(path) {
16408
16811
  const root = path.split("/")[0];
16409
16812
  if (root.startsWith(".") || [
16410
- "prompt-configs",
16813
+ "configs",
16411
16814
  "custom-tools",
16412
16815
  "subagent-tools",
16413
16816
  "skills"
@@ -16442,7 +16845,7 @@ async function expandPresetSource(input) {
16442
16845
  const manifest = files.find((file) => file.path === MANIFEST);
16443
16846
  if (manifest !== void 0) {
16444
16847
  const value = JSON.parse(decodeAssetFile(manifest).toString("utf8"));
16445
- if (!isRecord(value) || value.version !== PACKAGE_VERSION || value.definition !== "preset.yml" || !isRecord(value.files)) throw new Error("不支持或损坏的预设包清单");
16848
+ if (!isRecord(value) || value.version !== PACKAGE_VERSION || typeof value.definition !== "string" || !DEFINITION_NAMES.includes(value.definition) || !isRecord(value.files)) throw new Error("不支持或损坏的预设包清单");
16446
16849
  if (isRecord(value.requires) && typeof value.requires.renderVersion === "number" && value.requires.renderVersion > 5) throw new Error("预设包需要较新的 Prompt Tool");
16447
16850
  const entries = files.filter((file) => file !== manifest);
16448
16851
  if (Object.keys(value.files).length !== entries.length) throw new Error("包清单与文件集合不一致");
@@ -16450,7 +16853,7 @@ async function expandPresetSource(input) {
16450
16853
  files = entries;
16451
16854
  }
16452
16855
  const yaml = files.filter((file) => !file.path.includes("/") && /\.ya?ml$/i.test(file.path) && file.path !== "agent.cordis.yml");
16453
- const definition = yaml.find((file) => /^preset\.ya?ml$/i.test(file.path)) ?? (yaml.length === 1 ? yaml[0] : void 0);
16856
+ const definition = yaml.find((file) => /^(?:module|preset)\.ya?ml$/i.test(file.path)) ?? (yaml.length === 1 ? yaml[0] : void 0);
16454
16857
  if (definition !== void 0) {
16455
16858
  const doc = parseDocument(decodeAssetFile(definition).toString("utf8"), { logLevel: "silent" });
16456
16859
  const value = doc.errors.length === 0 ? doc.toJS({ maxAliasCount: 100 }) : void 0;
@@ -16483,7 +16886,7 @@ function presetImportPreview(root, files, request) {
16483
16886
  });
16484
16887
  if (prepared.state !== "ready") return prepared;
16485
16888
  const id = targetId(root, request.targetId ?? prepared.spec.id, request);
16486
- const version = directoryVersionOf(assertPresetDirectory(root, id, true));
16889
+ const version = directoryVersionOf(assertModuleDirectory(root, id, true));
16487
16890
  const sourceDigest = assetSourceDigest(files);
16488
16891
  return {
16489
16892
  prepared,
@@ -16572,7 +16975,7 @@ async function installPresetPackage(root, files, request) {
16572
16975
  let moved = false;
16573
16976
  const backup = join(stage, "previous");
16574
16977
  try {
16575
- for (const file of preview.prepared.files.filter((file) => !["preset.yml", MANIFEST].includes(file.path))) {
16978
+ for (const file of preview.prepared.files.filter((file) => file.path !== MANIFEST && !DEFINITION_NAMES.includes(file.path))) {
16576
16979
  const path = join(source, safeAssetPath(file.path));
16577
16980
  mkdirSync(dirname(path), { recursive: true });
16578
16981
  writeFileSync(path, decodeAssetFile(file), { flag: "wx" });
@@ -16581,10 +16984,10 @@ async function installPresetPackage(root, files, request) {
16581
16984
  setPresetDefinitionId(doc, preview.summary.targetId);
16582
16985
  doc.set("name", preview.summary.targetName);
16583
16986
  const spec = doc.toJS();
16584
- writeFileSync(join(source, "preset.yml"), doc.toString(), "utf8");
16987
+ writeFileSync(join(source, MODULE_DEFINITION_FILE), doc.toString(), "utf8");
16585
16988
  checkCandidate(spec, source, root, preview.summary.targetId);
16586
16989
  generated = writePreset(existsSync(join(source, "preset.md")) ? readFileSync(join(source, "preset.md"), "utf8") : "", {
16587
- presetDir: root,
16990
+ moduleDir: root,
16588
16991
  presetTemplate: preview.summary.targetId,
16589
16992
  outputId: preview.summary.targetId,
16590
16993
  sourceDir: source,
@@ -16610,7 +17013,7 @@ async function installPresetPackage(root, files, request) {
16610
17013
  }
16611
17014
  throw error;
16612
17015
  }
16613
- invalidatePresetSpec(target);
17016
+ invalidateModuleSpec(target);
16614
17017
  try {
16615
17018
  rmSync(stage, {
16616
17019
  recursive: true,
@@ -16687,7 +17090,7 @@ function dependencyErrors(files, spec) {
16687
17090
  if (typeof config.templateFile === "string") {
16688
17091
  const prefix = `../${spec.id}/`;
16689
17092
  if (!config.templateFile.startsWith(prefix)) errors.add(`外部模板:${config.templateFile}`);
16690
- else check("./" + config.templateFile.slice(prefix.length), "preset.yml");
17093
+ else check("./" + config.templateFile.slice(prefix.length), MODULE_DEFINITION_FILE);
16691
17094
  }
16692
17095
  }
16693
17096
  for (const file of files) {
@@ -16715,11 +17118,11 @@ function dependencyErrors(files, spec) {
16715
17118
  }
16716
17119
  async function exportPresetPackage(root, request) {
16717
17120
  assertTransferId(request.id);
16718
- const dir = resolvePresetDir(request.id, root);
17121
+ const dir = resolveModuleDir(request.id, root);
16719
17122
  const before = directoryVersionOf(dir);
16720
17123
  if (before === null) throw new Error("预设不存在");
16721
17124
  const files = collectFiles(dir);
16722
- const definition = files.find((file) => file.path === "preset.yml");
17125
+ const definition = files.find((file) => file.path === MODULE_DEFINITION_FILE);
16723
17126
  if (!definition) throw new Error("缺少预设定义");
16724
17127
  const original = parseDocument(definition.bytes.toString("utf8"));
16725
17128
  if (original.errors.length > 0) throw new Error(original.errors[0].message);
@@ -16742,7 +17145,7 @@ async function exportPresetPackage(root, request) {
16742
17145
  name: spec.name || request.id,
16743
17146
  content: "",
16744
17147
  revision,
16745
- filename: `${request.id}.${mode === "zip" ? "zip" : "preset.yml"}`,
17148
+ filename: `${request.id}.${mode === "zip" ? "zip" : MODULE_DEFINITION_FILE}`,
16746
17149
  files: files.map((file) => ({
16747
17150
  path: file.path,
16748
17151
  bytes: file.bytes.length
@@ -16765,7 +17168,7 @@ async function exportPresetPackage(root, request) {
16765
17168
  const plugin = JSON.parse(readFileSync(join(dirname(packageEngineDir()), "package.json"), "utf8"));
16766
17169
  const manifest = Buffer.from(JSON.stringify({
16767
17170
  version: PACKAGE_VERSION,
16768
- definition: "preset.yml",
17171
+ definition: MODULE_DEFINITION_FILE,
16769
17172
  requires: {
16770
17173
  promptTool: plugin.version,
16771
17174
  renderVersion: 5
@@ -16789,4 +17192,4 @@ async function exportPresetPackage(root, request) {
16789
17192
  };
16790
17193
  }
16791
17194
  //#endregion
16792
- export { ensurePresetSeed as $, USER_SKILLS_DIR as $t, decodeAssetFile as A, createWarnOnce as At, MAX_ASSET_BYTES as B, ENGINE_CAPABILITIES as Bt, listCharacterCards as C, KNOWN_AUDIENCES as Ct, removeCharacterFromPreset as D, createEpochPromotion as Dt, recordedCharacterModules as E, KNOWN_PROMOTIONS as Et, stPresetId as F, assertPresetId as Ft, stripConfigFieldSources as G, isEngineCapabilityPresent as Gt, savePresetTriggers as H, ENGINE_RECIPES as Ht, buildWorldBookEntry as I, canonicalPresetRoot as It, asString as J, WRITER_PARAM_KEYS as Jt, appendPresetModules as K, validateCustomToolIdentities as Kt, deleteWorldBookEntry as L, presetPathExists as Lt, convertStToPreset as M, PresetLayerSettingsError as Mt, mergeStPresets as N, PRESET_ENGINE_PREFIX as Nt, requiredCharacterModules as O, isSuccessfulCompactionEnd as Ot, processStText as P, assertPresetDirectory as Pt, duplicateUserPreset as Q, DSH_HOME as Qt, listWorldBookEntries as R, DEFAULT_PRESET_ID as Rt, importCharacterCardFile as S, renderPromptConfigYaml as St, readCharacterMemory as T, KNOWN_POSITIONS as Tt, triggerPromptConfigOptions as U, engineCapability as Ut, readPresetTriggers as V, ENGINE_EDITOR_GROUP_MAP as Vt, readConfigFieldSources as W, engineRecipe as Wt, cloneBuiltinPreset as X, validateEngineParamValues as Xt, buildModuleConfigsFromParams as Y, moduleParamFallbacks as Yt, createEngineCapabilityInPreset as Z, DEFAULT_PRESET_DIR as Zt, characterModuleStillNeeded as _, withPresetDoc as _t, writePreset as a, openPresetLocation as at, deleteCharacterCard as b, configFileName as bt, lastWorldBookDiagnostics as c, removePresetModule as ct, computePreviewRevision as d, resolvePresetDir as dt, invalidatePresetSpec as et, directoryVersionOf as f, resolvePresetModuleFacts as ft, applyCharacterToPreset as g, userPresetsDir as gt, appendMemoryFile as h, savePresetPersona as ht, presetImportPreview as i, loadPresetSpec as it, prepareImport as j, sessionEvents as jt, syncImportedCharacterMemory as k, PROMOTE_EVENTS as kt, validateCustomTools as l, removeUserPreset as lt, appendCharacterMemory as m, savePresetParams as mt, exportPresetPackage as n, listPresets as nt, confirmDelivered as o, packageEngineDir as ot, CHARACTER_MODULES_KEY as p, resolvePresetParams as pt, applyModuleConfigs as q, ENGINE_PARAM_KEYS as qt, installPresetPackage as r, loadPresetContent as rt, runPreStepBatch as s, removeEngineCapabilityFromPreset as st, expandPresetSource as t, listBuiltinTemplates as tt, safeAssetPath as u, renderComposition as ut, charactersDir as v, atomicWriteTextFile as vt, previewCharacterCard as w, KNOWN_MODEL_SCOPES as wt, importCharacterCard as x, listPromptConfigSpecs as xt, declaredCharacterModules as y, assertSafeConfigId as yt, upsertWorldBookEntry as z, readPersonaSpec as zt };
17195
+ export { setModuleEnabled as $, moduleParamFallbacks as $t, requiredCharacterModules as A, createPromptConfigs as At, listWorldBookEntries as B, presetPathExists as Bt, importCharacterCard as C, userModulesDir as Ct, readCharacterMemory as D, configFileName as Dt, previewCharacterCard as E, assertSafeConfigId as Et, mergeStPresets as F, createWarnOnce as Ft, triggerPromptConfigOptions as G, ENGINE_RECIPES as Gt, MAX_ASSET_BYTES as H, readPersonaSpec as Ht, processStText as I, PresetLayerSettingsError as It, readConfigOrder as J, isEngineCapabilityPresent as Jt, ModuleConfigOrderError as K, engineCapability as Kt, stPresetId as L, assertModuleDirectory as Lt, decodeAssetFile as M, compareConfigSequence as Mt, prepareImport as N, createEpochPromotion as Nt, recordedCharacterModules as O, listPromptConfigSpecs as Ot, convertStToPreset as P, PROMOTE_EVENTS as Pt, resolveEditDir as Q, WRITER_PARAM_KEYS as Qt, buildWorldBookEntry as R, assertPresetId as Rt, deleteCharacterCard as S, savePresetPersona as St, listCharacterCards as T, atomicWriteTextFile as Tt, readModuleTriggers as U, ENGINE_CAPABILITIES as Ut, upsertWorldBookEntry as V, DEFAULT_MODULE_ID as Vt, saveModuleTriggers as W, ENGINE_EDITOR_GROUP_MAP as Wt, saveModuleConfigOrder as X, ENGINE_PARAM_DEFINITIONS as Xt, readModuleConfigOrder as Y, validateCustomToolIdentities as Yt, enabledModuleIds as Z, ENGINE_PARAM_KEYS as Zt, appendMemoryFile as _, renderComposition as _t, materializeModule as a, MODULE_DEFINITION_FILE as an, duplicateUserModule as at, charactersDir as b, resolvePresetParams as bt, confirmDelivered as c, listBuiltinTemplates as ct, validateCustomTools as d, moduleDirExists as dt, validateEngineParamValues as en, appendPresetModules as et, safeAssetPath as f, openModuleLocation as ft, appendCharacterMemory as g, removeUserPreset as gt, CHARACTER_MODULES_KEY as h, removePresetModule as ht, presetImportPreview as i, MODULE_CONFIGS_DIR as in, createEngineCapabilityInPreset as it, syncImportedCharacterMemory as j, loadPromptConfigFiles as jt, removeCharacterFromPreset as k, renderPromptConfigYaml as kt, runPreStepBatch as l, listModules as lt, directoryVersionOf as m, removeEngineCapabilityFromPreset as mt, exportPresetPackage as n, DSH_HOME as nn, buildModuleConfigsFromParams as nt, writePreset as o, USER_SKILLS_DIR as on, ensurePresetSeed as ot, computePreviewRevision as p, packageEngineDir as pt, appendModuleConfigOrder as q, engineRecipe as qt, installPresetPackage as r, MODULES_DIR as rn, cloneBuiltinPreset as rt, applyPromptConfigSources as s, invalidateModuleSpec as st, expandPresetSource as t, LEGACY_PROMPT_PARAM_KEYS as tn, applyModuleConfigs as tt, lastWorldBookDiagnostics as u, loadModuleSpec as ut, applyCharacterToPreset as v, resolveModuleDir as vt, importCharacterCardFile as w, withPresetDoc as wt, declaredCharacterModules as x, saveModuleParams as xt, characterModuleStillNeeded as y, resolveModuleFacts as yt, deleteWorldBookEntry as z, canonicalPresetRoot as zt };