dsh-plugin-prompt-tool 1.0.0 → 2.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 (95) hide show
  1. package/README.md +38 -29
  2. package/engine/actions/append-context.mjs +49 -0
  3. package/engine/actions/assembly.mjs +150 -0
  4. package/engine/actions/catalog.mjs +92 -0
  5. package/engine/actions/content.mjs +205 -0
  6. package/engine/actions/decision.mjs +54 -0
  7. package/engine/actions/examples.mjs +11 -0
  8. package/engine/actions/guard.mjs +80 -0
  9. package/engine/actions/inbox-prepend.mjs +37 -0
  10. package/engine/actions/index.mjs +100 -0
  11. package/engine/actions/inject-text.mjs +22 -0
  12. package/engine/actions/pre-step-filter.mjs +55 -0
  13. package/engine/actions/registration.mjs +168 -0
  14. package/engine/actions/request-params.mjs +32 -0
  15. package/engine/actions/sdk-strip.mjs +30 -0
  16. package/engine/actions/shared.mjs +71 -0
  17. package/engine/actions.mjs +2 -931
  18. package/engine/branch.mjs +44 -0
  19. package/engine/compaction-epoch.mjs +31 -13
  20. package/engine/compositions/source/local/dev-tool-search.yml +11 -0
  21. package/engine/compositions/source/local/instruction-hint.yml +22 -1
  22. package/engine/compositions/source/local/rule-engine.yml +6 -0
  23. package/engine/condition.mjs +16 -3
  24. package/engine/conditions/anchor.mjs +32 -0
  25. package/engine/conditions/availability.mjs +47 -0
  26. package/engine/conditions/composite.mjs +83 -0
  27. package/engine/conditions/count.mjs +140 -0
  28. package/engine/conditions/evaluation.mjs +41 -0
  29. package/engine/conditions/examples.mjs +11 -0
  30. package/engine/conditions/index.mjs +85 -0
  31. package/engine/conditions/names.mjs +21 -0
  32. package/engine/conditions/phase.mjs +54 -0
  33. package/engine/conditions/preset.mjs +94 -0
  34. package/engine/conditions/scope.mjs +18 -0
  35. package/engine/conditions/session.mjs +22 -0
  36. package/engine/conditions/source.mjs +63 -0
  37. package/engine/conditions/subject.mjs +121 -0
  38. package/engine/conditions/text.mjs +44 -0
  39. package/engine/conditions/values.mjs +32 -0
  40. package/engine/dev-tool-search.mjs +213 -0
  41. package/engine/executor.mjs +55 -6
  42. package/engine/fillers.mjs +23 -16
  43. package/engine/instruction-hint.mjs +34 -22
  44. package/engine/layers.mjs +135 -49
  45. package/engine/order.mjs +4 -1
  46. package/engine/predicates.mjs +2 -753
  47. package/engine/rule-engine.mjs +40 -0
  48. package/engine/rule-runtime.mjs +110 -0
  49. package/engine/rule-spec.mjs +187 -0
  50. package/engine/schema.mjs +16 -5
  51. package/engine/shared.mjs +1 -1
  52. package/engine/strategies.mjs +2 -235
  53. package/engine/subagent-tool-policy-core.mjs +7 -7
  54. package/engine/subagent-tool-policy.mjs +20 -10
  55. package/engine/tool-config-engine.mjs +53 -32
  56. package/engine/trigger-editor-meta.mjs +2 -21
  57. package/engine/trigger-spec.mjs +49 -133
  58. package/lib/client.js +10446 -10820
  59. package/lib/client.js.map +1 -1
  60. package/lib/index.d.mts +82 -139
  61. package/lib/index.mjs +826 -1087
  62. package/lib/migrate-rules.d.mts +1 -0
  63. package/lib/migrate-rules.mjs +39 -0
  64. package/lib/module-package-CHPFweqK.mjs +2 -0
  65. package/lib/module-package-Cnd1_mwx.mjs +2872 -0
  66. package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
  67. package/lib/preset-transfer.d.mts +2 -2
  68. package/lib/preset-transfer.mjs +3 -2
  69. package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
  70. package/modules/ponytail/module.yml +381 -147
  71. package/modules/skill-surface/module.yml +74 -0
  72. package/modules/tool-surface/module.yml +67 -0
  73. package/package.json +5 -1
  74. package/templates/10-pre-step.yml +26 -22
  75. package/templates/14-first-turn-anchor.yml +25 -21
  76. package/templates/15-guide-auto.yml +34 -19
  77. package/templates/16-custom-fallback.yml +27 -15
  78. package/templates/18-placeholder-env-facts.yml +23 -14
  79. package/templates/20-system-section.yml +16 -10
  80. package/templates/30-runtime-context.yml +13 -7
  81. package/templates/40-agent-request.yml +11 -8
  82. package/templates/50-llm-stream.yml +14 -6
  83. package/templates/60-tool-pipeline.yml +15 -9
  84. package/templates/65-turn-stop.yml +19 -12
  85. package/templates/66-subagent-start.yml +12 -7
  86. package/templates/70-subagent-maintenance.yml +35 -29
  87. package/templates/75-subagent-end.yml +10 -5
  88. package/templates/80-tool-surface.yml +66 -0
  89. package/templates/policies/legacy-defaults.yml +34 -0
  90. package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
  91. package/engine/prompt-config-engine.mjs +0 -104
  92. package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
  93. package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
  94. package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
  95. package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
@@ -0,0 +1,2872 @@
1
+ import { A as packageEngineDir, Ct as assertModuleTree, D as loadModuleSpec, Et as setModuleDefinitionId, F as resolveModuleDir, Ft as validateCustomToolIdentities, G as assertCanonicalRuleSource, H as editModuleRules, J as rulePromptConfigOptions, K as ensureModuleSlices, P as renderComposition, Q as injectionConfigSpec, St as assertModuleId, Tt as modulePathExists, U as readModuleRules, Ut as MODULES_DIR, V as withModuleDefinition, W as ModuleRulesError, Wt as MODULE_DEFINITION_FILE, X as withModuleLock, Y as validateModuleDefinitionText, Z as compileRules, bt as readModuleLayerSettings, d as assertSafeConfigId, dt as prepareStText, ft as renderStText, g as appendModuleCapabilities, gt as atomicWriteTextFile, i as promptConfigToRule, n as convertLegacyModuleRules, p as enabledModuleIds, q as loadModuleDefinition, s as appendModuleConfigOrder, v as assertCompositionArray, w as invalidateModuleSpec, wt as canonicalModulesRoot, xt as assertModuleDirectory, yt as engineParamPath } from "./rules-migration-DJ6vASB5.mjs";
2
+ import { basename, dirname, join, posix, resolve } from "node:path";
3
+ import { appendFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { YAMLMap, YAMLSeq, parse, parseDocument, stringify } from "yaml";
5
+ import { createHash } from "node:crypto";
6
+ import { inflateSync } from "node:zlib";
7
+ import { Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } from "@zip.js/zip.js";
8
+ import { parameterSchemaSpecToJsonSchema, valueSchemaSpecToJsonSchema } from "@deepseek-ai/dsh-tools";
9
+ //#region src/shared/preset-ids.ts
10
+ /**
11
+ * 包内模块与用户副本共用同名 ID;避免与宿主官方预设重名。
12
+ *
13
+ * 内置模块随包分发在仓库根的 `modules/`。旧的内置预设 `pt-*` 已整体退场——它们是官方
14
+ * 预设形态的遗留,重构后不再读取也不再维护;这里指向当前随包分发的内置模块作为兜底。
15
+ */
16
+ const DEFAULT_MODULE_ID = "ponytail";
17
+ //#endregion
18
+ //#region src/host/rule-content.ts
19
+ /** 资产工具只投影注入动作的内容;不读取旧定义或创建第二份配置源。 */
20
+ function ruleInjections(rules = []) {
21
+ return rules.flatMap((rule) => rule.then.flatMap((action) => action.kind === "inject-text" ? [{
22
+ rule,
23
+ action,
24
+ config: injectionConfigSpec(rule, action)
25
+ }] : []));
26
+ }
27
+ /** 只替换指定注入动作载荷,保留同一卡的判断与其他动作。 */
28
+ function mapRuleInjections(rule, map) {
29
+ return {
30
+ ...rule,
31
+ then: rule.then.map((action) => action.kind === "inject-text" ? {
32
+ ...action,
33
+ config: map(injectionConfigSpec(rule, action), action)
34
+ } : action)
35
+ };
36
+ }
37
+ //#endregion
38
+ //#region src/shared/asset-transfer.ts
39
+ /** 载体和解码后的资源集合使用同一上限。 */
40
+ const MAX_ASSET_BYTES = 67108864;
41
+ //#endregion
42
+ //#region src/host/character-png.ts
43
+ const PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
44
+ const MAX_PNG_CARD_BYTES = 16777216;
45
+ function isPngBuffer(buffer) {
46
+ return buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE);
47
+ }
48
+ /** host、上传入口与 CLI 共用;完整 chunk 边界及解压上限在读取时检查。 */
49
+ function decodePngCharacterCard(buffer) {
50
+ if (!isPngBuffer(buffer)) throw new Error("不是有效的 PNG 文件");
51
+ const chunks = [];
52
+ let offset = PNG_SIGNATURE.length;
53
+ let ended = false;
54
+ while (offset < buffer.length) {
55
+ if (offset + 12 > buffer.length) throw new Error("PNG chunk 越界");
56
+ const length = buffer.readUInt32BE(offset);
57
+ const dataStart = offset + 8;
58
+ const dataEnd = dataStart + length;
59
+ if (dataEnd + 4 > buffer.length) throw new Error("PNG chunk 越界");
60
+ const type = buffer.toString("latin1", offset + 4, offset + 8);
61
+ if (type === "tEXt") {
62
+ const separator = buffer.indexOf(0, dataStart);
63
+ if (separator > dataStart && separator < dataEnd) chunks.push({
64
+ keyword: buffer.toString("latin1", dataStart, separator).toLowerCase(),
65
+ text: buffer.toString("latin1", separator + 1, dataEnd)
66
+ });
67
+ }
68
+ if (type === "IEND") {
69
+ ended = true;
70
+ break;
71
+ }
72
+ offset = dataEnd + 4;
73
+ }
74
+ if (!ended) throw new Error("PNG 缺少完整 IEND chunk");
75
+ const card = chunks.find((chunk) => chunk.keyword === "ccv3") ?? chunks.find((chunk) => chunk.keyword === "chara");
76
+ if (card === void 0) throw new Error("PNG 不含角色卡数据(无 chara/ccv3 tEXt chunk)");
77
+ if (card.text.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(card.text)) throw new Error("PNG 角色卡 base64 非法");
78
+ const encoded = Buffer.from(card.text, "base64");
79
+ if (encoded.length > MAX_PNG_CARD_BYTES) throw new Error("PNG 角色卡数据超限");
80
+ let decoded;
81
+ try {
82
+ decoded = inflateSync(encoded, { maxOutputLength: MAX_PNG_CARD_BYTES });
83
+ } catch (error) {
84
+ if (error.code === "ERR_BUFFER_TOO_LARGE") throw new Error("PNG 角色卡解压超限");
85
+ decoded = encoded;
86
+ }
87
+ const jsonText = new TextDecoder("utf-8", { fatal: true }).decode(decoded);
88
+ JSON.parse(jsonText);
89
+ return {
90
+ jsonText,
91
+ avatar: buffer
92
+ };
93
+ }
94
+ //#endregion
95
+ //#region src/host/worldbook.ts
96
+ /** 世界书条目 CRUD 服务:promptConfigs world-book 策略配置的增删改查。
97
+ * 与角色卡导入(characters.applyCharacterToPreset)共用同一存储(preset.yml
98
+ * promptConfigs),模型工具(world_book_*)与未来 bridge 端点同源,
99
+ * 不各自实现 parseDocument 往返。 */
100
+ /**
101
+ * 世界书条目结构工厂(能力归一)。
102
+ * strategy/layer/position 固定值与 params 键集单一权威——ST 导入(sillytavern.ts)
103
+ * 与模型工具(world_book_upsert)共用,不再各自手写条目结构。
104
+ */
105
+ function buildWorldBookEntry(input) {
106
+ return {
107
+ ...input.id !== void 0 ? { id: input.id } : {},
108
+ name: input.name,
109
+ strategy: "world-book",
110
+ order: typeof input.order === "number" ? input.order : 100,
111
+ text: input.text,
112
+ layer: "pre-step",
113
+ position: "before-all",
114
+ ...input.enabled !== void 0 ? { enabled: input.enabled } : {},
115
+ params: {
116
+ constant: input.constant,
117
+ ...input.keys !== void 0 && input.keys.length > 0 ? { keys: input.keys } : {},
118
+ ...input.secondaryKeys !== void 0 && input.secondaryKeys.length > 0 ? { secondaryKeys: input.secondaryKeys } : {},
119
+ ...input.caseSensitive === true ? { caseSensitive: true } : {},
120
+ ...input.wholeWords === true ? { wholeWords: true } : {},
121
+ ...typeof input.selectiveLogic === "number" ? { selectiveLogic: input.selectiveLogic } : {}
122
+ }
123
+ };
124
+ }
125
+ const isWorldBook = (config) => config !== null && typeof config === "object" && !Array.isArray(config) && config.strategy === "world-book";
126
+ /** 当前模块全部世界书条目(保持文件顺序)。 */
127
+ function listWorldBookEntries(moduleDir) {
128
+ return ruleInjections(loadModuleSpec(moduleDir).rules).flatMap(({ rule, config }) => isWorldBook(config) ? [{
129
+ ...config,
130
+ enabled: rule.enabled !== false && config.enabled !== false
131
+ }] : []);
132
+ }
133
+ /** 新增或更新一条世界书条目(按 id 定位;不存在则追加)。返回写入后条目总数。 */
134
+ function upsertWorldBookEntry(moduleDir, entry) {
135
+ if (entry === null || typeof entry !== "object" || typeof entry.id !== "string" || entry.id.length === 0) throw new TypeError("世界书条目必须含非空字符串 id");
136
+ const snapshot = readModuleRules(moduleDir);
137
+ const matches = ruleInjections(snapshot.rules).filter(({ config }) => isWorldBook(config) && config.id === entry.id);
138
+ if (matches.length > 1) throw new Error(`世界书条目 ${entry.id} 的动作身份重复,无法安全更新`);
139
+ const existing = matches[0];
140
+ const rule = existing === void 0 ? promptConfigToRule({
141
+ ...entry,
142
+ id: entry.id
143
+ }) : {
144
+ ...existing.rule,
145
+ then: existing.rule.then.map((action) => action.id === existing.action.id ? {
146
+ ...action,
147
+ config: entry
148
+ } : action)
149
+ };
150
+ return ruleInjections(editModuleRules(moduleDir, {
151
+ expectedRevisions: snapshot.revisions,
152
+ edits: [{
153
+ previousId: existing?.rule.id ?? null,
154
+ rule,
155
+ settingsChanged: existing === void 0
156
+ }]
157
+ }).rules).filter(({ config }) => isWorldBook(config)).length;
158
+ }
159
+ /** 删除一条世界书条目;不存在抛错。返回删除后条目总数。 */
160
+ function deleteWorldBookEntry(moduleDir, id) {
161
+ const snapshot = readModuleRules(moduleDir);
162
+ const matches = ruleInjections(snapshot.rules).filter(({ config }) => isWorldBook(config) && config.id === id);
163
+ if (matches.length === 0) throw new Error(`世界书条目 ${id} 不存在`);
164
+ if (matches.length > 1) throw new Error(`世界书条目 ${id} 的动作身份重复,无法安全删除`);
165
+ const { rule, action } = matches[0];
166
+ const actions = rule.then.filter((item) => item.id !== action.id);
167
+ return ruleInjections(editModuleRules(moduleDir, {
168
+ expectedRevisions: snapshot.revisions,
169
+ edits: [{
170
+ previousId: rule.id,
171
+ rule: actions.length > 0 ? {
172
+ ...rule,
173
+ then: actions
174
+ } : null,
175
+ settingsChanged: false
176
+ }]
177
+ }).rules).filter(({ config }) => isWorldBook(config)).length;
178
+ }
179
+ //#endregion
180
+ //#region src/host/sillytavern.ts
181
+ /**
182
+ * SillyTavern 预设 → 本项目单文件预设转换引擎(纯函数,无文件 IO)。
183
+ *
184
+ * 按需转换原则:只按注入层级映射 SillyTavern 实际内容,不注入本项目默认内容。
185
+ * - prompts[] → promptConfigs:role=system → system-section(可拼接);
186
+ * 其余 → pre-step(role/position/dedupe 按 ST 语义);
187
+ * - 角色卡正文(chara_card_v3:data 内层;旧版顶层直存)→ promptConfigs:
188
+ * description/personality/scenario 拼接为「角色设定」system-section,
189
+ * system_prompt / post_history_instructions → system-section,
190
+ * first_mes → pre-step 开场白(dedupe=session 每会话一次);
191
+ * - 采样参数(temperature/openai_max_tokens/reasoning_effort)**不转译**:
192
+ * 模型参数由「模型设置」UI 统一管理(预设级 params.model*,writePreset 渲染
193
+ * agent-request patch),ST 卡固化值会覆盖用户在模型设置里的设置,故剥离;
194
+ * - modules 按需组装:prompt-config-engine 始终,system-section 注入需要
195
+ * persona(complete: false 允许 system-section 生效),世界书条目需要 world-book-tools。
196
+ */
197
+ /** 转换器版本:报告用它解释本次生成使用了哪一版语义(语义调整时同步递增)。
198
+ * v2:pre-step 角色统一降级为 user(原角色只作来源元数据)+ 选组优先级修正常量事实。
199
+ * v3:世界书触发键(keys/secondaryKeys)的未定义宏登记为空占位并产出可见诊断。 */
200
+ const ST_CONVERTER_VERSION = "st-to-rules/4";
201
+ /** 报告的展示上限:只截断观测数据,不改变转换结果。 */
202
+ const REPORT_ENTRY_LIMIT = 500;
203
+ const REPORT_DIAGNOSTIC_LIMIT = 200;
204
+ /**
205
+ * pre-step 只接受 user:宿主把本批消息逐条写成 `user/message` 事件,事件校验要求
206
+ * `role === 'user'`。ST 侧的 assistant/system 角色在导入期统一降级为 user,原角色
207
+ * 保留在 stSource.role / stWorldBook.role,条目按 degraded 记录并带同一原因码。
208
+ */
209
+ const ST_PRE_STEP_ROLE = "user";
210
+ const ST_ROLE_DOWNGRADE_CODE = "assistant-role-downgrade";
211
+ /**
212
+ * `enable_web_search: false` 时被拒绝的 web 工具名。
213
+ * 三个声明式触发器(assembly / sdk-strip / guard)共用这一份名单——呈现过滤与执行
214
+ * 边界必须同一判据,不得各写一份(见 engine/actions.mjs 的 (5) 类说明)。
215
+ */
216
+ const ST_WEB_TOOLS = ["web_search", "web_fetch"];
217
+ /** ST marker prompts(marker: true):content 不发送给模型(仅标记注入位置,ST
218
+ * 以运行时内容填充该位置);SPresetSettings 是旧版 ST 的预设设置 dump(正则
219
+ * 脚本/扩展配置/ToolBindings,动辄数百 KB)。两者转换时整体丢弃并计数进
220
+ * meta.stDroppedMarkers——防止设置 dump 与位置占位污染 promptConfigs 与注入。 */
221
+ /** 显式文本求值工具;导入本身仅 prepare,避免执行禁用卡的副作用。 */
222
+ function processStText(text, cardName, variables) {
223
+ return renderStText(prepareStText(text, cardName), {
224
+ variables,
225
+ local: variables
226
+ }).trim();
227
+ }
228
+ /**
229
+ * 合并多个转换结果为一个预设,并返回「来源 + 生成配置下标 → 最终 id」的同一份映射。
230
+ *
231
+ * 报告必须消费这份映射,而不是按后缀规则另行推测:后缀分配只发生在这里一次。
232
+ */
233
+ function mergeStModulesWithReport(specs) {
234
+ const rules = [];
235
+ const seen = /* @__PURE__ */ new Set();
236
+ const idMap = /* @__PURE__ */ new Map();
237
+ for (const [sourceIndex, spec] of specs.entries()) {
238
+ const perSource = /* @__PURE__ */ new Map();
239
+ for (const [entryIndex, config] of (spec.rules ?? []).entries()) {
240
+ if (config === null || typeof config !== "object" || Array.isArray(config)) continue;
241
+ const entry = config;
242
+ const base = String(entry.id ?? "");
243
+ if (base.length === 0) continue;
244
+ let id = base;
245
+ for (let suffix = 2; seen.has(id); suffix++) id = `${base}-${suffix}`;
246
+ seen.add(id);
247
+ perSource.set(entryIndex, id);
248
+ rules.push(mapRuleInjections({
249
+ ...entry,
250
+ id
251
+ }, (payload) => {
252
+ const next = {
253
+ ...payload,
254
+ variables: {
255
+ ...spec.variablesEnabled === false ? {} : spec.variables,
256
+ ...payload.variables
257
+ }
258
+ };
259
+ if (id !== base) next.id = id;
260
+ return next;
261
+ }));
262
+ }
263
+ idMap.set(sourceIndex, perSource);
264
+ }
265
+ const params = {};
266
+ const layerSettings = {};
267
+ for (const spec of specs) {
268
+ readModuleLayerSettings(spec);
269
+ Object.assign(params, spec.params ?? {});
270
+ for (const [layer, fields] of Object.entries(spec.layerSettings ?? {})) layerSettings[layer] = {
271
+ ...layerSettings[layer],
272
+ ...fields
273
+ };
274
+ }
275
+ const variables = Object.fromEntries(specs.filter((spec) => spec.variablesEnabled !== false).flatMap((spec) => Object.entries(spec.variables ?? {})));
276
+ const warnings = [...new Set(specs.flatMap((spec) => Array.isArray(spec.meta?.stWarnings) ? spec.meta.stWarnings.filter((value) => typeof value === "string") : []))];
277
+ const modules = [];
278
+ for (const spec of specs) for (const name of spec.modules ?? []) if (!modules.includes(name)) modules.push(name);
279
+ const managementModules = [
280
+ "character-tools",
281
+ ...ruleInjections(rules).some(({ config }) => config.strategy === "world-book") ? ["world-book-tools"] : [],
282
+ "session-var-tools",
283
+ "tool-config-engine"
284
+ ];
285
+ for (const name of managementModules) if (!modules.includes(name)) modules.push(name);
286
+ const moduleConfigs = {};
287
+ for (const spec of specs) for (const [key, value] of Object.entries(spec.moduleConfigs ?? {})) moduleConfigs[key] = {
288
+ ...moduleConfigs[key],
289
+ ...value
290
+ };
291
+ const persona = specs.find((spec) => spec.persona !== void 0)?.persona;
292
+ const stripSuffix = (name) => name.replace(/(SillyTavern 转换)$/, "");
293
+ return {
294
+ spec: {
295
+ id: specs.length > 1 ? specs.map((item) => item.id).join("-") : specs[0].id,
296
+ name: specs.map((item) => stripSuffix(item.name)).join(" × ") + "(SillyTavern 合并)",
297
+ version: "1.0.0",
298
+ engineCompat: ">=0.4.2",
299
+ meta: {
300
+ source: "sillytavern",
301
+ ...warnings.length > 0 ? { stWarnings: warnings } : {}
302
+ },
303
+ ...Object.keys(params).length > 0 ? { params } : {},
304
+ ...Object.keys(layerSettings).length > 0 ? { layerSettings } : {},
305
+ ...Object.keys(variables).length > 0 ? { variables } : {},
306
+ ...persona === void 0 ? {} : { persona },
307
+ modules,
308
+ moduleConfigs,
309
+ rules
310
+ },
311
+ idMap
312
+ };
313
+ }
314
+ /** 合并多个转换结果为一个预设(角色卡 × 响应预设 → 单预设)。 */
315
+ function mergeStModules(specs) {
316
+ return mergeStModulesWithReport(specs).spec;
317
+ }
318
+ /**
319
+ * 选组优先级(唯一实现,转换与预览共用):
320
+ * 请求显式选择 → 文件内 character_id → 全局组 100001 → 仅有一组时回退。
321
+ * 显式选择必须命中,命中失败不回落;重复 character_id 无法明确对应,一律拒绝。
322
+ */
323
+ function resolveStOrder(record, options) {
324
+ if (!Array.isArray(record.prompt_order)) return { groups: [] };
325
+ const groups = record.prompt_order.filter((entry) => entry !== null && typeof entry === "object" && Array.isArray(entry.order));
326
+ const summaries = (list) => list.map((entry) => ({
327
+ characterId: String(entry.character_id ?? ""),
328
+ entries: entry.order.length
329
+ }));
330
+ if (groups.length === 0) return { groups };
331
+ const explicit = typeof options.characterId === "string" && options.characterId.length > 0 ? options.characterId : void 0;
332
+ const fileCharacterId = record.character_id === void 0 || record.character_id === null ? void 0 : String(record.character_id);
333
+ const find = (id) => id === void 0 ? void 0 : groups.find((entry) => String(entry.character_id) === id);
334
+ let group = find(explicit);
335
+ if (explicit !== void 0 && group === void 0) return {
336
+ groups,
337
+ needsSelection: summaries(groups),
338
+ error: `SillyTavern prompt_order 不包含 character_id ${JSON.stringify(explicit)} 的顺序表`
339
+ };
340
+ group ??= find(fileCharacterId);
341
+ group ??= find("100001");
342
+ if (group === void 0 && groups.length === 1) [group] = groups;
343
+ if (group === void 0) return {
344
+ groups,
345
+ ...groups.length > 1 ? { needsSelection: summaries(groups) } : {}
346
+ };
347
+ const picked = String(group.character_id ?? "");
348
+ if (groups.filter((entry) => String(entry.character_id ?? "") === picked).length > 1) return {
349
+ groups,
350
+ error: `SillyTavern prompt_order 中 character_id ${JSON.stringify(picked)} 重复,无法明确对应顺序表`
351
+ };
352
+ return {
353
+ groups,
354
+ group
355
+ };
356
+ }
357
+ /**
358
+ * 预览选组状态:多组且按优先级无法明确选择时给出候选,让 UI 先让用户选组再重新预览;
359
+ * 候选状态不代表已转换,调用方不得据此启用确认或提交。
360
+ */
361
+ function stOrderSelectionState(card, options = {}) {
362
+ const resolved = resolveStOrder(card !== null && typeof card === "object" ? card : {}, options);
363
+ if (resolved.error !== void 0) return {
364
+ needsSelection: false,
365
+ error: resolved.error
366
+ };
367
+ if (resolved.needsSelection !== void 0) return {
368
+ needsSelection: true,
369
+ candidates: resolved.needsSelection
370
+ };
371
+ return { needsSelection: false };
372
+ }
373
+ /** SillyTavern JSON 预设卡片 → 本项目 ModuleSpec(导入端点直接消费)。 */
374
+ function convertStToModule(card, baseName, options = {}) {
375
+ return convertStToModuleWithReport(card, baseName, options).spec;
376
+ }
377
+ /**
378
+ * 同源转换 + 结构化报告:预览与实际提交共用这一个纯函数实现,
379
+ * 报告只是派生元数据(不进 module.yml、不作写入凭证)。
380
+ */
381
+ function convertStToModuleWithReport(card, baseName, options = {}) {
382
+ const record = card !== null && typeof card === "object" ? card : {};
383
+ const prompts = Array.isArray(record.prompts) ? record.prompts.filter((item) => item !== null && typeof item === "object") : [];
384
+ const orderIndex = /* @__PURE__ */ new Map();
385
+ const orderEnabled = /* @__PURE__ */ new Map();
386
+ const orderGroups = [];
387
+ let ordered = false;
388
+ {
389
+ const resolved = resolveStOrder(record, options);
390
+ if (resolved.error !== void 0) throw new TypeError(resolved.error);
391
+ if (resolved.needsSelection !== void 0) throw new TypeError("SillyTavern prompt_order 包含多个角色,请提供 character_id 或导出全局预设");
392
+ const { groups, group } = resolved;
393
+ for (const entry of groups) orderGroups.push({
394
+ characterId: String(entry.character_id ?? ""),
395
+ selected: entry === group,
396
+ entries: entry.order.length
397
+ });
398
+ if (Array.isArray(record.prompt_order)) {
399
+ const order = group === void 0 ? record.prompt_order : group.order;
400
+ ordered = group !== void 0 || order.some((entry) => entry !== null && typeof entry === "object" && typeof entry.identifier === "string");
401
+ let rank = 0;
402
+ for (const entry of order) {
403
+ if (entry === null || typeof entry !== "object") continue;
404
+ const id = typeof entry.identifier === "string" ? entry.identifier : "";
405
+ if (id.length === 0) continue;
406
+ if (!orderIndex.has(id)) orderIndex.set(id, rank);
407
+ orderEnabled.set(id, entry.enabled === true);
408
+ rank += 1;
409
+ }
410
+ }
411
+ }
412
+ const configs = [];
413
+ /** module.yml 顶层触发器声明(`enable_web_search: false` 的 web 拒绝名单在此登记)。 */
414
+ const triggers = [];
415
+ const worldBookSources = /* @__PURE__ */ new Map();
416
+ const droppedMarkers = [];
417
+ const diagnostics = [];
418
+ const reportEntries = [];
419
+ const diagnosticKeys = /* @__PURE__ */ new Set();
420
+ const warnings = /* @__PURE__ */ new Set();
421
+ const classifications = {
422
+ equivalent: 0,
423
+ degraded: 0,
424
+ unsupported: 0,
425
+ excluded: 0
426
+ };
427
+ let needsReview = 0;
428
+ let reportTruncated = false;
429
+ let sourceInputs = 0;
430
+ const note = (code, message, extra = {}, severity = "warning") => {
431
+ const key = JSON.stringify([code, extra.entryId]);
432
+ if (diagnosticKeys.has(key)) return;
433
+ diagnosticKeys.add(key);
434
+ if (severity === "warning") {
435
+ needsReview += 1;
436
+ warnings.add(message);
437
+ }
438
+ if (diagnostics.length >= REPORT_DIAGNOSTIC_LIMIT) {
439
+ reportTruncated = true;
440
+ return;
441
+ }
442
+ diagnostics.push({
443
+ code,
444
+ severity,
445
+ message,
446
+ ...extra.entryId !== void 0 ? { entryId: extra.entryId } : {},
447
+ ...extra.field !== void 0 ? { field: extra.field } : {}
448
+ });
449
+ };
450
+ const recordEntry = (entry) => {
451
+ classifications[entry.classification] += 1;
452
+ if (reportEntries.length >= REPORT_ENTRY_LIMIT) {
453
+ reportTruncated = true;
454
+ return;
455
+ }
456
+ let targetIndex;
457
+ if (entry.targetId !== void 0) {
458
+ for (let index = configs.length - 1; index >= 0; index--) if (configs[index]?.id === entry.targetId) {
459
+ targetIndex = index;
460
+ break;
461
+ }
462
+ }
463
+ reportEntries.push(targetIndex === void 0 ? entry : {
464
+ ...entry,
465
+ targetIndex
466
+ });
467
+ };
468
+ /** info 级诊断:进入报告但**不**改变既有 stWarnings 表现。 */
469
+ const noteInfo = (code, message, extra = {}) => {
470
+ note(code, message, extra, "info");
471
+ };
472
+ let systemSectionCount = 0;
473
+ const body = record.data !== null && typeof record.data === "object" ? record.data : record;
474
+ const extensions = body.extensions !== null && typeof body.extensions === "object" ? body.extensions : void 0;
475
+ if (extensions && Object.keys(extensions).some((key) => /helper|script|regex|tavern/i.test(key))) note("st-extension-scripts", "ST 扩展脚本与正则不执行,依赖它们的界面或状态更新需要单独适配", { field: "extensions" });
476
+ const bodyText = (key) => typeof body[key] === "string" ? body[key].trim() : "";
477
+ const cardName = typeof record.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof body.name === "string" && body.name.trim().length > 0 ? body.name.trim() : "";
478
+ const variables = {};
479
+ if (cardName && (body.description !== void 0 || body.first_mes !== void 0 || body.character_book !== void 0)) {
480
+ variables.char = cardName;
481
+ variables.charifnotgroup = cardName;
482
+ variables.charIfNotGroup = cardName;
483
+ }
484
+ const clean = (text) => prepareStText(text, cardName);
485
+ for (const key of [
486
+ "description",
487
+ "personality",
488
+ "scenario",
489
+ "persona"
490
+ ]) {
491
+ const value = bodyText(key);
492
+ if (value.length > 0) variables[key] = clean(value);
493
+ }
494
+ const creatorNotes = bodyText("creator_notes");
495
+ if (creatorNotes.length > 0) variables.creator_notes = clean(creatorNotes);
496
+ const characterDefinitionClean = clean([
497
+ bodyText("description"),
498
+ bodyText("personality"),
499
+ bodyText("scenario")
500
+ ].filter((text) => text.length > 0).join("\n\n"));
501
+ if (characterDefinitionClean.length > 0) {
502
+ sourceInputs += 1;
503
+ configs.push({
504
+ id: "character-definition",
505
+ name: "角色设定",
506
+ strategy: "static",
507
+ order: -30,
508
+ text: characterDefinitionClean,
509
+ layer: "system-section",
510
+ mergeMode: "merged"
511
+ });
512
+ recordEntry({
513
+ sourceId: "description/personality/scenario",
514
+ sourceIndex: 0,
515
+ targetId: "character-definition",
516
+ layer: "system-section",
517
+ order: -30,
518
+ classification: "equivalent",
519
+ codes: []
520
+ });
521
+ systemSectionCount += 1;
522
+ }
523
+ const systemPromptClean = clean(bodyText("system_prompt"));
524
+ if (systemPromptClean.length > 0) {
525
+ sourceInputs += 1;
526
+ configs.push({
527
+ id: "system-prompt",
528
+ name: "系统提示",
529
+ strategy: "static",
530
+ order: -20,
531
+ text: systemPromptClean,
532
+ layer: "system-section",
533
+ mergeMode: "merged"
534
+ });
535
+ recordEntry({
536
+ sourceId: "system_prompt",
537
+ sourceIndex: 0,
538
+ targetId: "system-prompt",
539
+ layer: "system-section",
540
+ order: -20,
541
+ classification: "equivalent",
542
+ codes: []
543
+ });
544
+ systemSectionCount += 1;
545
+ }
546
+ const postHistoryClean = clean(bodyText("post_history_instructions"));
547
+ if (postHistoryClean.length > 0) {
548
+ sourceInputs += 1;
549
+ configs.push({
550
+ id: "post-history-instructions",
551
+ name: "后续指令",
552
+ strategy: "static",
553
+ order: -10,
554
+ text: postHistoryClean,
555
+ layer: "system-section",
556
+ mergeMode: "merged"
557
+ });
558
+ noteInfo("st-post-history", "ST post_history_instructions 放入 system-section,不等于历史后覆盖", {
559
+ entryId: "post-history-instructions",
560
+ field: "post_history_instructions"
561
+ });
562
+ recordEntry({
563
+ sourceId: "post_history_instructions",
564
+ sourceIndex: 0,
565
+ targetId: "post-history-instructions",
566
+ layer: "system-section",
567
+ order: -10,
568
+ classification: "degraded",
569
+ codes: ["post-history-position"]
570
+ });
571
+ systemSectionCount += 1;
572
+ }
573
+ const examples = bodyText("mes_example");
574
+ if (examples.length > 0) {
575
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
576
+ const marker = new RegExp(`(?:^|\\n)\\s*(\\{\\{user\\}\\}|\\{\\{char\\}\\}|user|assistant${cardName ? `|${escape(cardName)}` : ""})\\s*:\\s*`, "gi");
577
+ let index = 0;
578
+ let exampleDowngraded = false;
579
+ for (const block of examples.split(/<START>/gi).filter((text) => text.trim().length > 0)) {
580
+ const turns = [...block.matchAll(marker)];
581
+ const parts = turns.length > 0 ? turns.map((turn, at) => ({
582
+ role: /^(?:\{\{user\}\}|user)$/i.test(turn[1]) ? "user" : "assistant",
583
+ text: block.slice(turn.index + turn[0].length, turns[at + 1]?.index)
584
+ })) : [{
585
+ role: "user",
586
+ text: block
587
+ }];
588
+ for (const part of parts) {
589
+ const text = clean(part.text);
590
+ if (!text) continue;
591
+ sourceInputs += 1;
592
+ const entryIndex = ++index;
593
+ const targetId = `dialogue-example-${entryIndex}`;
594
+ const order = -60 + entryIndex / 1e3;
595
+ const downgraded = part.role === "assistant";
596
+ exampleDowngraded ||= downgraded;
597
+ configs.push({
598
+ id: targetId,
599
+ name: `示例对话 ${entryIndex}`,
600
+ strategy: "static",
601
+ text,
602
+ layer: "pre-step",
603
+ role: "user",
604
+ position: "before-all",
605
+ dedupe: "session",
606
+ order,
607
+ params: { stSource: {
608
+ field: "mes_example",
609
+ role: part.role
610
+ } }
611
+ });
612
+ recordEntry({
613
+ sourceId: `mes_example[${entryIndex}]`,
614
+ sourceIndex: entryIndex - 1,
615
+ targetId,
616
+ layer: "pre-step",
617
+ order,
618
+ role: "user",
619
+ position: "before-all",
620
+ classification: downgraded ? "degraded" : "equivalent",
621
+ codes: downgraded ? [ST_ROLE_DOWNGRADE_CODE] : []
622
+ });
623
+ }
624
+ }
625
+ if (exampleDowngraded) noteInfo("st-example-role", "DSH pre-step 只接受 user 角色:示例对话的 assistant 轮次降级为 user,原角色保留在 stSource.role", {
626
+ entryId: "mes_example",
627
+ field: "mes_example"
628
+ });
629
+ }
630
+ const book = body.character_book ?? (body.entries !== void 0 ? body : void 0);
631
+ if (book !== null && typeof book === "object" && !Array.isArray(book)) {
632
+ const rawEntries = book.entries;
633
+ const entryList = Array.isArray(rawEntries) ? rawEntries : rawEntries !== null && typeof rawEntries === "object" ? Object.values(rawEntries).filter((e) => e !== null && typeof e === "object" && !Array.isArray(e)) : [];
634
+ for (const [index, entry] of entryList.entries()) {
635
+ if (entry === null || typeof entry !== "object") continue;
636
+ const content = clean(typeof entry.content === "string" ? entry.content : "");
637
+ sourceInputs += 1;
638
+ const sourceId = String(entry.id ?? entry.uid ?? index);
639
+ if (content.length === 0) {
640
+ recordEntry({
641
+ sourceId,
642
+ sourceIndex: index,
643
+ classification: "excluded",
644
+ codes: ["empty-content"]
645
+ });
646
+ continue;
647
+ }
648
+ const comment = typeof entry.comment === "string" && entry.comment.trim().length > 0 ? entry.comment.trim() : `世界书 ${String(entry.id ?? entry.uid ?? index)}`;
649
+ const rawKeys = entry.keys ?? entry.key;
650
+ const keys = Array.isArray(rawKeys) ? rawKeys.map(String).filter((key) => key.trim().length > 0) : [];
651
+ const rawSecondary = entry.secondary_keys ?? entry.keysecondary;
652
+ const secondaryKeys = Array.isArray(rawSecondary) ? rawSecondary.map(String).filter((key) => key.trim().length > 0) : [];
653
+ const constant = entry.constant === true || entry.add_always === true;
654
+ const enabled = entry.disable !== void 0 ? entry.disable !== true : entry.enabled !== false;
655
+ const stOrder = typeof entry.insertion_order === "number" ? entry.insertion_order : typeof entry.order === "number" ? entry.order : 100;
656
+ const ext = entry.extensions !== null && typeof entry.extensions === "object" && !Array.isArray(entry.extensions) ? entry.extensions : {};
657
+ const option = (...names) => {
658
+ for (const scope of [ext, entry]) for (const name of names) if (scope[name] !== void 0) return scope[name];
659
+ };
660
+ const position = option("position") ?? (entry.position === "after_char" ? 1 : 0);
661
+ const stWorldBook = {
662
+ selective: entry.selective === true,
663
+ position,
664
+ depth: option("depth") ?? 4,
665
+ role: option("role") ?? 0,
666
+ scanDepth: option("scan_depth", "scanDepth") ?? book.scan_depth ?? 2
667
+ };
668
+ if (book.recursive_scanning === true) stWorldBook.recursive = true;
669
+ for (const [target, source] of Object.entries({
670
+ probability: "probability",
671
+ useProbability: "useProbability",
672
+ group: "group",
673
+ groupOverride: "group_override",
674
+ groupWeight: "group_weight",
675
+ sticky: "sticky",
676
+ cooldown: "cooldown",
677
+ delay: "delay",
678
+ recursive: "recursive_scanning",
679
+ excludeRecursion: "exclude_recursion",
680
+ preventRecursion: "prevent_recursion",
681
+ delayUntilRecursion: "delay_until_recursion",
682
+ useGroupScoring: "use_group_scoring",
683
+ matchCharacterDescription: "match_character_description",
684
+ matchCharacterPersonality: "match_character_personality",
685
+ matchScenario: "match_scenario",
686
+ matchPersonaDescription: "match_persona_description",
687
+ matchCreatorNotes: "match_creator_notes",
688
+ matchCharacterDepthPrompt: "match_character_depth_prompt"
689
+ })) {
690
+ const value = option(source, target, ...target === "useProbability" ? ["use_probability"] : []);
691
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") stWorldBook[target] = value;
692
+ }
693
+ const sourceRole = stWorldBook.role;
694
+ const rawFilter = entry.characterFilter;
695
+ const filterSource = rawFilter !== null && typeof rawFilter === "object" && !Array.isArray(rawFilter) ? rawFilter : void 0;
696
+ const filterNames = Array.isArray(filterSource?.names) ? filterSource.names : [];
697
+ const filterTags = Array.isArray(filterSource?.tags) ? filterSource.tags : [];
698
+ if (filterSource !== void 0) stWorldBook.characterFilter = {
699
+ ...filterSource,
700
+ ...Array.isArray(filterSource.names) ? { names: [...filterSource.names] } : {},
701
+ ...Array.isArray(filterSource.tags) ? { tags: [...filterSource.tags] } : {}
702
+ };
703
+ const entryCodes = [];
704
+ if (filterNames.length > 0 || filterTags.length > 0) {
705
+ entryCodes.push("character-filter-unsupported");
706
+ note("st-worldbook-character-filter", "ST 角色/标签过滤在本项目不受支持:该条目会对所有角色生效", {
707
+ entryId: sourceId,
708
+ field: "characterFilter"
709
+ });
710
+ }
711
+ if (position === 4) {
712
+ entryCodes.push("depth-collapsed");
713
+ note("st-worldbook-depth", "ST 世界书深度位置无法映射到持久历史:保留 position/depth/role,降级为当前消息批末尾", {
714
+ entryId: sourceId,
715
+ field: "position"
716
+ });
717
+ } else if (![
718
+ 0,
719
+ 1,
720
+ "before_char",
721
+ "after_char"
722
+ ].includes(position)) {
723
+ entryCodes.push("position-downgraded");
724
+ note("st-worldbook-position", "ST 世界书特殊插入点暂不可用:保留来源位置,降级为当前消息批头部", {
725
+ entryId: sourceId,
726
+ field: "position"
727
+ });
728
+ }
729
+ if (sourceRole === 0) {
730
+ entryCodes.push("system-role-downgrade");
731
+ note("st-worldbook-role", "DSH pre-step 不接受 system 角色:世界书 system 消息降级为 user,原角色保留在 stWorldBook", {
732
+ entryId: sourceId,
733
+ field: "role"
734
+ });
735
+ } else if (sourceRole === 2) {
736
+ entryCodes.push(ST_ROLE_DOWNGRADE_CODE);
737
+ noteInfo("st-worldbook-role-assistant", "DSH pre-step 只接受 user 角色:世界书 assistant 消息降级为 user,原角色保留在 stWorldBook.role", {
738
+ entryId: sourceId,
739
+ field: "role"
740
+ });
741
+ }
742
+ const rawAutomationId = option("automation_id", "automationId");
743
+ const rawOutletName = option("outlet_name", "outletName");
744
+ const automationId = typeof rawAutomationId === "string" ? rawAutomationId.trim() : "";
745
+ const outletName = typeof rawOutletName === "string" ? rawOutletName.trim() : "";
746
+ if (automationId.length > 0) stWorldBook.automationId = rawAutomationId;
747
+ if (outletName.length > 0) stWorldBook.outletName = rawOutletName;
748
+ if (option("vectorized") === true || outletName.length > 0 || Array.isArray(option("triggers")) && option("triggers").length > 0) {
749
+ entryCodes.push("unsupported-controls");
750
+ note("st-worldbook-controls", "ST 世界书向量、outlet 或生成类型控制需要宿主专门适配", {
751
+ entryId: sourceId,
752
+ field: "extensions"
753
+ });
754
+ }
755
+ if (automationId.length > 0) {
756
+ entryCodes.push("automation-dependent");
757
+ note("st-worldbook-automation", keys.length === 0 && secondaryKeys.length === 0 && constant !== true ? "该条目依赖 STscript 自动化触发,本项目不执行自动化;它没有主键也非常驻,因此不会自动注入" : "该条目依赖 STscript 自动化触发,本项目不执行自动化", {
758
+ entryId: sourceId,
759
+ field: "automationId"
760
+ });
761
+ }
762
+ if (enabled === false) entryCodes.push("disabled");
763
+ const worldConfig = buildWorldBookEntry({
764
+ id: `lore-${sourceId}`,
765
+ name: comment,
766
+ enabled,
767
+ order: stOrder,
768
+ text: content,
769
+ constant,
770
+ keys: keys.length > 0 ? keys : void 0,
771
+ secondaryKeys: secondaryKeys.length > 0 ? secondaryKeys : void 0,
772
+ ...option("case_sensitive", "caseSensitive") === true ? { caseSensitive: true } : {},
773
+ ...option("match_whole_words", "matchWholeWords") === true ? { wholeWords: true } : {},
774
+ ...typeof option("selectiveLogic", "selective_logic") === "number" ? { selectiveLogic: option("selectiveLogic", "selective_logic") } : {}
775
+ });
776
+ configs.push({
777
+ ...worldConfig,
778
+ role: ST_PRE_STEP_ROLE,
779
+ position: position === 4 ? "after-all" : "before-all",
780
+ params: {
781
+ ...worldConfig.params,
782
+ stWorldBook
783
+ }
784
+ });
785
+ worldBookSources.set(String(worldConfig.id), sourceId);
786
+ recordEntry({
787
+ sourceId,
788
+ sourceIndex: index,
789
+ targetId: `lore-${sourceId}`,
790
+ layer: "pre-step",
791
+ order: stOrder,
792
+ role: ST_PRE_STEP_ROLE,
793
+ position: position === 4 ? "after-all" : "before-all",
794
+ classification: entryCodes.includes("unsupported-controls") ? "unsupported" : entryCodes.some((code) => code !== "disabled") ? "degraded" : "equivalent",
795
+ codes: entryCodes
796
+ });
797
+ }
798
+ }
799
+ const depthPromptRaw = extensions?.depth_prompt;
800
+ const depthPromptSource = depthPromptRaw !== null && typeof depthPromptRaw === "object" && !Array.isArray(depthPromptRaw) ? depthPromptRaw : void 0;
801
+ const depthPromptText = typeof depthPromptSource?.prompt === "string" ? clean(depthPromptSource.prompt) : "";
802
+ if (depthPromptText.length > 0) {
803
+ variables.depth_prompt = depthPromptText;
804
+ sourceInputs += 1;
805
+ const order = -50;
806
+ configs.push({
807
+ id: "st-depth-prompt",
808
+ name: "角色深度提示词",
809
+ strategy: "static",
810
+ enabled: false,
811
+ order,
812
+ text: depthPromptText,
813
+ layer: "pre-step",
814
+ mergeMode: "merged",
815
+ role: ST_PRE_STEP_ROLE,
816
+ position: "before-all",
817
+ dedupe: "none",
818
+ params: { stSource: {
819
+ field: "extensions.depth_prompt",
820
+ depth: depthPromptSource?.depth ?? 4,
821
+ role: depthPromptSource?.role ?? 0
822
+ } }
823
+ });
824
+ recordEntry({
825
+ sourceId: "extensions.depth_prompt",
826
+ sourceIndex: 0,
827
+ targetId: "st-depth-prompt",
828
+ layer: "pre-step",
829
+ order,
830
+ role: ST_PRE_STEP_ROLE,
831
+ position: "before-all",
832
+ classification: "degraded",
833
+ codes: ["depth-prompt-group-only"]
834
+ });
835
+ noteInfo("st-depth-prompt", "ST 角色深度提示词只在群聊自动注入:本项目保留为默认禁用的 pre-step 配置,可由用户手动启用", {
836
+ entryId: "st-depth-prompt",
837
+ field: "extensions.depth_prompt"
838
+ });
839
+ }
840
+ const firstMes = clean(bodyText("first_mes"));
841
+ if (firstMes.length > 0) {
842
+ sourceInputs += 1;
843
+ configs.push({
844
+ id: "first-mes",
845
+ name: "开场白",
846
+ strategy: "static",
847
+ order: -40,
848
+ text: firstMes,
849
+ layer: "pre-step",
850
+ mergeMode: "merged",
851
+ role: ST_PRE_STEP_ROLE,
852
+ position: "before-all",
853
+ dedupe: "session",
854
+ params: { stSource: {
855
+ field: "first_mes",
856
+ role: "assistant"
857
+ } }
858
+ });
859
+ recordEntry({
860
+ sourceId: "first_mes",
861
+ sourceIndex: 0,
862
+ targetId: "first-mes",
863
+ layer: "pre-step",
864
+ order: -40,
865
+ role: ST_PRE_STEP_ROLE,
866
+ position: "before-all",
867
+ classification: "degraded",
868
+ codes: [ST_ROLE_DOWNGRADE_CODE]
869
+ });
870
+ noteInfo("st-first-mes-role", "DSH pre-step 只接受 user 角色:ST 开场白降级为 user,原角色保留在 stSource.role", {
871
+ entryId: "first_mes",
872
+ field: "first_mes"
873
+ });
874
+ }
875
+ const alternateGreetings = Array.isArray(body.alternate_greetings) ? body.alternate_greetings.map((item) => typeof item === "string" ? item : "").map(clean).filter((item) => item.length > 0) : [];
876
+ if (alternateGreetings.length > 0) noteInfo("st-alternate-greetings-role", "DSH pre-step 只接受 user 角色:备用开场白降级为 user,原角色保留在 stSource.role", {
877
+ entryId: "alternate_greetings",
878
+ field: "alternate_greetings"
879
+ });
880
+ for (const [index, greeting] of alternateGreetings.entries()) {
881
+ sourceInputs += 1;
882
+ const targetId = `first-mes-${index + 2}`;
883
+ const order = -40 + index + 1;
884
+ configs.push({
885
+ id: targetId,
886
+ name: `开场白 ${index + 2}`,
887
+ strategy: "static",
888
+ enabled: false,
889
+ order,
890
+ text: greeting,
891
+ layer: "pre-step",
892
+ mergeMode: "merged",
893
+ role: ST_PRE_STEP_ROLE,
894
+ position: "before-all",
895
+ dedupe: "session",
896
+ params: { stSource: {
897
+ field: `alternate_greetings[${index}]`,
898
+ role: "assistant"
899
+ } }
900
+ });
901
+ recordEntry({
902
+ sourceId: `alternate_greetings[${index}]`,
903
+ sourceIndex: index,
904
+ targetId,
905
+ layer: "pre-step",
906
+ order,
907
+ role: ST_PRE_STEP_ROLE,
908
+ position: "before-all",
909
+ classification: "degraded",
910
+ codes: [ST_ROLE_DOWNGRADE_CODE]
911
+ });
912
+ }
913
+ for (const [index, prompt] of prompts.entries()) {
914
+ const rawId = typeof prompt.identifier === "string" ? prompt.identifier : "";
915
+ const sourceId = rawId.length > 0 ? rawId : `prompt-${index + 1}`;
916
+ sourceInputs += 1;
917
+ if (prompt.marker === true || rawId === "SPresetSettings") {
918
+ if (rawId.length > 0) droppedMarkers.push(rawId);
919
+ recordEntry({
920
+ sourceId,
921
+ sourceIndex: index,
922
+ classification: "excluded",
923
+ codes: ["marker-dropped"]
924
+ });
925
+ continue;
926
+ }
927
+ const content = clean(typeof prompt.content === "string" ? prompt.content : "");
928
+ if (content.length === 0) {
929
+ recordEntry({
930
+ sourceId,
931
+ sourceIndex: index,
932
+ classification: "excluded",
933
+ codes: ["empty-content"]
934
+ });
935
+ continue;
936
+ }
937
+ const id = rawId.length > 0 && !/^[0-9a-f-]{36}$/i.test(rawId) ? rawId : `st-prompt-${index + 1}`;
938
+ const stRole = typeof prompt.role === "string" ? prompt.role : "system";
939
+ const preStep = stRole !== "system";
940
+ const role = preStep ? ST_PRE_STEP_ROLE : "system";
941
+ const roleDowngraded = preStep && stRole !== "user";
942
+ const systemPromptFlag = prompt.system_prompt === true;
943
+ const base = {
944
+ id,
945
+ name: typeof prompt.name === "string" && prompt.name.length > 0 ? prompt.name : id,
946
+ enabled: ordered ? orderEnabled.get(rawId) === true : prompt.enabled !== false,
947
+ strategy: "static",
948
+ order: ((rawId.length > 0 ? orderIndex.get(rawId) : void 0) ?? (ordered ? orderIndex.size + index : index)) * 10,
949
+ text: content,
950
+ ...prompt.injection_position === 1 || Array.isArray(prompt.injection_trigger) || roleDowngraded || systemPromptFlag ? { params: { stSource: {
951
+ position: prompt.injection_position ?? 0,
952
+ depth: prompt.injection_depth ?? 4,
953
+ order: prompt.injection_order ?? 100,
954
+ role: stRole,
955
+ ...Array.isArray(prompt.injection_trigger) ? { triggers: prompt.injection_trigger } : {},
956
+ ...systemPromptFlag ? { systemPrompt: true } : {}
957
+ } } } : {}
958
+ };
959
+ const codes = [];
960
+ if (systemPromptFlag) noteInfo("st-prompt-system-flag", "ST 的 prompts[].system_prompt 只标记内置/全局 prompt,不改变发送角色:本条仍按 role 分层", {
961
+ entryId: sourceId,
962
+ field: "system_prompt"
963
+ });
964
+ if (roleDowngraded) {
965
+ codes.push(ST_ROLE_DOWNGRADE_CODE);
966
+ noteInfo("st-prompt-role", "DSH pre-step 只接受 user 角色:ST prompt 的 assistant/model 角色降级为 user,原角色保留在 stSource.role", {
967
+ entryId: sourceId,
968
+ field: "role"
969
+ });
970
+ }
971
+ if (prompt.injection_position === 1) {
972
+ codes.push("depth-collapsed");
973
+ note("st-prompt-depth", "ST prompt 深度注入暂按 DSH 插入点降级,原 position/depth/order/role 保留在 stSource", {
974
+ entryId: sourceId,
975
+ field: "injection_position"
976
+ });
977
+ }
978
+ if (Array.isArray(prompt.injection_trigger) && prompt.injection_trigger.length > 0) {
979
+ codes.push("generation-trigger");
980
+ note("st-prompt-triggers", "ST prompt 的生成类型触发条件在 DSH 不等价,保留在 stSource.triggers", {
981
+ entryId: sourceId,
982
+ field: "injection_trigger"
983
+ });
984
+ }
985
+ if (base.enabled === false) codes.push(ordered && orderIndex.get(rawId) === void 0 ? "not-in-order-group" : "disabled");
986
+ if (!preStep) {
987
+ configs.push({
988
+ ...base,
989
+ layer: "system-section",
990
+ mergeMode: "merged"
991
+ });
992
+ systemSectionCount += 1;
993
+ } else configs.push({
994
+ ...base,
995
+ layer: "pre-step",
996
+ mergeMode: "merged",
997
+ role,
998
+ position: prompt.injection_position === 0 ? "before-all" : "after-user",
999
+ dedupe: "none"
1000
+ });
1001
+ recordEntry({
1002
+ sourceId,
1003
+ sourceIndex: index,
1004
+ targetId: id,
1005
+ layer: preStep ? "pre-step" : "system-section",
1006
+ order: base.order,
1007
+ role: preStep ? role : void 0,
1008
+ position: preStep ? prompt.injection_position === 0 ? "before-all" : "after-user" : void 0,
1009
+ classification: codes.includes("depth-collapsed") || codes.includes("generation-trigger") || roleDowngraded ? "degraded" : "equivalent",
1010
+ codes
1011
+ });
1012
+ }
1013
+ const modules = ["prompt-config-engine", "character-tools"];
1014
+ if (configs.some((config) => config.strategy === "world-book")) modules.push("world-book-tools");
1015
+ modules.push("session-var-tools", "tool-config-engine");
1016
+ const moduleConfigs = {};
1017
+ const persona = systemSectionCount > 0 ? {
1018
+ prefix: "",
1019
+ complete: false
1020
+ } : void 0;
1021
+ if (record.enable_web_search === true) {
1022
+ modules.push("tool-web");
1023
+ moduleConfigs["tool-web"] = { fetch: true };
1024
+ } else if (record.enable_web_search === false) triggers.push({
1025
+ id: "st-web-assembly",
1026
+ channel: "system-prompt/assemble",
1027
+ do: {
1028
+ kind: "assembly",
1029
+ target: { tools: { deny: [...ST_WEB_TOOLS] } }
1030
+ }
1031
+ }, {
1032
+ id: "st-web-sdk-strip",
1033
+ channel: "system-prompt/assemble",
1034
+ do: {
1035
+ kind: "sdk-strip",
1036
+ mask: { deny: [...ST_WEB_TOOLS] }
1037
+ }
1038
+ }, {
1039
+ id: "st-web-guard",
1040
+ channel: "system-prompt/assemble",
1041
+ do: {
1042
+ kind: "guard",
1043
+ mask: { deny: [...ST_WEB_TOOLS] }
1044
+ }
1045
+ });
1046
+ const moduleId = stModuleId(baseName);
1047
+ for (const config of configs) {
1048
+ if (config.layer !== "pre-step" || config.role === ST_PRE_STEP_ROLE) continue;
1049
+ const requested = config.role;
1050
+ config.role = ST_PRE_STEP_ROLE;
1051
+ noteInfo("st-pre-step-role-fallback", `DSH pre-step 只接受 user 角色:${String(config.id)} 声明的 ${JSON.stringify(requested)} 已降级为 user`, {
1052
+ entryId: String(config.id),
1053
+ field: "role"
1054
+ });
1055
+ }
1056
+ const RUNTIME_MACROS = /* @__PURE__ */ new Set([
1057
+ "lastusermessage",
1058
+ "lastcharmessage",
1059
+ "charifnotgroup",
1060
+ "time",
1061
+ "date",
1062
+ "weekday",
1063
+ "isotime",
1064
+ "isodate",
1065
+ "random",
1066
+ "pick",
1067
+ "roll",
1068
+ "chance",
1069
+ "newline",
1070
+ "pipe"
1071
+ ]);
1072
+ const BUILTIN_KEYS = /* @__PURE__ */ new Set([
1073
+ "DSH_HOME",
1074
+ "WORKSPACE",
1075
+ "CWD"
1076
+ ]);
1077
+ const MACRO_RE = /\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g;
1078
+ const declaredKeys = new Set(Object.keys(variables).map((key) => key.toLowerCase()));
1079
+ const knownKeys = new Set(declaredKeys);
1080
+ const hasMacroSource = (key) => declaredKeys.has(key.toLowerCase()) || RUNTIME_MACROS.has(key.toLowerCase()) || BUILTIN_KEYS.has(key);
1081
+ /** 登记未定义宏为空占位;返回识别到的未解析宏名(空数组表示全部已有来源)。 */
1082
+ const registerMacros = (raw) => {
1083
+ const missing = [];
1084
+ MACRO_RE.lastIndex = 0;
1085
+ for (const match of String(raw).matchAll(MACRO_RE)) {
1086
+ const key = match[1];
1087
+ if (hasMacroSource(key)) continue;
1088
+ missing.push(key);
1089
+ const lower = key.toLowerCase();
1090
+ if (!knownKeys.has(lower)) {
1091
+ knownKeys.add(lower);
1092
+ variables[key] = "";
1093
+ }
1094
+ }
1095
+ return missing;
1096
+ };
1097
+ /** 键宏诊断文案:不记录键正文与卡片内容,只说明失效原因与恢复路径。 */
1098
+ const KEY_MACRO_MESSAGE = "世界书触发键含未解析的 ST 宏:已登记为空占位,在「模板变量」中赋值后该键才会命中";
1099
+ for (const config of configs) {
1100
+ config.params = {
1101
+ ...config.params,
1102
+ stMacros: true
1103
+ };
1104
+ const configRecord = config;
1105
+ const texts = [
1106
+ ...typeof config.text === "string" && config.text.length > 0 ? [config.text] : [],
1107
+ ...Array.isArray(config.texts) ? config.texts : [],
1108
+ ...typeof configRecord.params?.text === "string" ? [configRecord.params.text] : []
1109
+ ];
1110
+ for (const raw of texts) registerMacros(raw);
1111
+ if (config.strategy === "world-book") {
1112
+ const entryId = worldBookSources.get(String(config.id));
1113
+ for (const [field, value] of [["keys", configRecord.params?.keys], ["secondaryKeys", configRecord.params?.secondaryKeys]]) if ((Array.isArray(value) ? value : []).flatMap((key) => registerMacros(key)).length > 0 && entryId !== void 0) note("st-key-macro", KEY_MACRO_MESSAGE, {
1114
+ entryId,
1115
+ field
1116
+ });
1117
+ }
1118
+ }
1119
+ const stWarnings = [...warnings];
1120
+ const report = {
1121
+ converter: ST_CONVERTER_VERSION,
1122
+ sourceName: baseName,
1123
+ orderGroups,
1124
+ entries: reportEntries,
1125
+ diagnostics,
1126
+ summary: {
1127
+ inputs: sourceInputs,
1128
+ converted: configs.length,
1129
+ disabled: configs.filter((config) => config.enabled === false).length,
1130
+ excluded: classifications.excluded,
1131
+ unsupported: classifications.unsupported,
1132
+ degraded: classifications.degraded,
1133
+ needsReview
1134
+ },
1135
+ ...reportTruncated ? { truncated: true } : {}
1136
+ };
1137
+ return {
1138
+ spec: {
1139
+ id: moduleId,
1140
+ name: `${cardName || baseName}(SillyTavern 转换)`,
1141
+ version: "1.0.0",
1142
+ engineCompat: ">=0.4.2",
1143
+ meta: {
1144
+ source: "sillytavern",
1145
+ ...stWarnings.length > 0 ? { stWarnings } : {},
1146
+ ...droppedMarkers.length > 0 ? { stDroppedMarkers: droppedMarkers } : {}
1147
+ },
1148
+ ...Object.keys(variables).length > 0 ? { variables } : {},
1149
+ ...persona === void 0 ? {} : { persona },
1150
+ modules: [...new Set(modules.map((name) => name === "prompt-config-engine" || name === "declared-triggers" ? "rule-engine" : name))],
1151
+ moduleConfigs,
1152
+ rules: convertLegacyModuleRules({
1153
+ promptConfigs: configs,
1154
+ triggers
1155
+ }).rules
1156
+ },
1157
+ report
1158
+ };
1159
+ }
1160
+ /**
1161
+ * 多文件导入合并各自报告:条目/诊断有界截断、计数求和。
1162
+ *
1163
+ * 传入 `sources` 时,条目的 `targetId` 被重写为**合并后的最终 id**(消费
1164
+ * `mergeStModulesWithReport` 的同一份映射),并补上来源显示名与来源序号;
1165
+ * 诊断的 `targetId` 同样按映射定位。不传时保持旧行为(不重写)。
1166
+ */
1167
+ function mergeStConversionReports(reports, sources = []) {
1168
+ const finalIdOf = (sourceIndex, entry) => {
1169
+ if (entry.targetIndex === void 0) return void 0;
1170
+ return sources[sourceIndex]?.idMap?.get(entry.targetIndex);
1171
+ };
1172
+ const entries = reports.flatMap((report, sourceIndex) => report.entries.map((entry) => {
1173
+ const source = sources[sourceIndex];
1174
+ const finalId = finalIdOf(sourceIndex, entry);
1175
+ return {
1176
+ ...entry,
1177
+ ...source === void 0 ? {} : {
1178
+ sourceName: source.sourceName,
1179
+ sourceFileIndex: sourceIndex
1180
+ },
1181
+ ...finalId === void 0 ? {} : { targetId: finalId }
1182
+ };
1183
+ })).slice(0, REPORT_ENTRY_LIMIT);
1184
+ const diagnostics = reports.flatMap((report, sourceIndex) => report.diagnostics.map((item) => {
1185
+ if (item.entryId === void 0) return item;
1186
+ const entry = report.entries.find((candidate) => candidate.sourceId === item.entryId);
1187
+ const finalId = entry === void 0 ? void 0 : finalIdOf(sourceIndex, entry);
1188
+ return finalId === void 0 ? item : {
1189
+ ...item,
1190
+ targetId: finalId
1191
+ };
1192
+ })).slice(0, REPORT_DIAGNOSTIC_LIMIT);
1193
+ const total = (pick) => reports.reduce((sum, report) => sum + pick(report.summary), 0);
1194
+ return {
1195
+ converter: ST_CONVERTER_VERSION,
1196
+ sourceName: reports.map((report) => report.sourceName).join(" + "),
1197
+ orderGroups: reports.flatMap((report) => report.orderGroups),
1198
+ entries,
1199
+ diagnostics,
1200
+ summary: {
1201
+ inputs: total((summary) => summary.inputs),
1202
+ converted: total((summary) => summary.converted),
1203
+ disabled: total((summary) => summary.disabled),
1204
+ excluded: total((summary) => summary.excluded),
1205
+ unsupported: total((summary) => summary.unsupported),
1206
+ degraded: total((summary) => summary.degraded),
1207
+ needsReview: total((summary) => summary.needsReview)
1208
+ },
1209
+ ...reports.some((report) => report.truncated === true) || reports.flatMap((report) => report.entries).length > entries.length || reports.flatMap((report) => report.diagnostics).length > diagnostics.length ? { truncated: true } : {}
1210
+ };
1211
+ }
1212
+ /** ST 导入预设 id:必须是官方 agent-presets 可发现的目录名(PRESET_ID =
1213
+ * /^[a-z0-9][a-z0-9-]*$/——含中文的目录会被宿主 discovery 静默跳过,会话
1214
+ * resume 报 preset not found)。文件名 slug 化(去中文);纯中文名退化为
1215
+ * st-<文件名短哈希>(唯一且合法);显示名 name 仍保留中文原名。 */
1216
+ function stModuleId(baseName) {
1217
+ return baseName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || `st-${createHash("sha1").update(baseName).digest("hex").slice(0, 6)}`;
1218
+ }
1219
+ //#endregion
1220
+ //#region src/host/import-source.ts
1221
+ const MAX_ASSET_FILE_BYTES = MAX_ASSET_BYTES;
1222
+ const MAX_ASSET_TOTAL_BYTES = MAX_ASSET_BYTES;
1223
+ function isRecord$2(value) {
1224
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1225
+ }
1226
+ function decodeAssetFile(file) {
1227
+ if (typeof file.content !== "string") throw new Error(`${file.path}: 文件内容必须是字符串`);
1228
+ if (file.encoding === void 0 && /\.(png|jpe?g|gif|webp|zip|ico|pdf|woff2?|bin)$/i.test(file.path)) throw new Error(`${file.path}: 二进制文件缺少编码标记,请刷新页面后重新选择文件`);
1229
+ if (file.encoding !== void 0 && file.encoding !== "utf8" && file.encoding !== "base64") throw new Error(`${file.path}: 未知文件编码`);
1230
+ if (file.content.length > 89478489.33333333) throw new Error(`${file.path}: 文件大小超限`);
1231
+ if (file.encoding === "base64" && (file.content.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(file.content))) throw new Error(`${file.path}: 非法 base64`);
1232
+ const bytes = Buffer.from(file.content, file.encoding === "base64" ? "base64" : "utf8");
1233
+ if (bytes.length > MAX_ASSET_FILE_BYTES) throw new Error(`${file.path}: 文件大小超限`);
1234
+ return bytes;
1235
+ }
1236
+ /** 校验传输路径和字节;外层目录由载体入口剥离一次,显式来源顺序保持不变。 */
1237
+ function normalizeAssetFiles(files) {
1238
+ if (!Array.isArray(files) || files.length === 0 || files.length > 2048) throw new Error("导入文件数量为空或超限");
1239
+ const seen = /* @__PURE__ */ new Set();
1240
+ let total = 0;
1241
+ const normalized = files.map((file) => {
1242
+ if (!isRecord$2(file) || typeof file.path !== "string") throw new Error("文件路径必须是字符串");
1243
+ const path = file.path;
1244
+ const segments = path.split("/");
1245
+ if (path.length > 1024 || path.includes("\\") || [...path].some((char) => char.charCodeAt(0) < 32) || segments.some((segment) => segment === "" || segment === "." || segment === ".." || /[<>:"|?*]/.test(segment) || /[. ]$/.test(segment) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment))) throw new Error(`非法文件路径:${file.path}`);
1246
+ const key = path.normalize("NFC").toLowerCase();
1247
+ if (seen.has(key)) throw new Error(`文件路径重复或大小写冲突:${file.path}`);
1248
+ seen.add(key);
1249
+ const bytes = decodeAssetFile(file);
1250
+ total += bytes.length;
1251
+ if (total > MAX_ASSET_TOTAL_BYTES) throw new Error("导入总大小超限");
1252
+ return {
1253
+ path,
1254
+ content: file.content,
1255
+ ...file.encoding === void 0 ? {} : { encoding: file.encoding }
1256
+ };
1257
+ });
1258
+ for (const path of seen) {
1259
+ const segments = path.split("/");
1260
+ while (segments.length > 1) {
1261
+ segments.pop();
1262
+ if (seen.has(segments.join("/"))) throw new Error(`文件与目录路径冲突:${path}`);
1263
+ }
1264
+ }
1265
+ return normalized;
1266
+ }
1267
+ function assetSourceDigest(files) {
1268
+ const hash = createHash("sha256");
1269
+ for (const file of files) {
1270
+ const bytes = decodeAssetFile(file);
1271
+ hash.update(JSON.stringify([file.path, bytes.length])).update(bytes);
1272
+ }
1273
+ return hash.digest("hex");
1274
+ }
1275
+ /** 角色片段只接受自包含配置,验证时不得读取上传来源以外的磁盘文件。 */
1276
+ function validateCharacterSpec(spec) {
1277
+ assertCanonicalRuleSource(spec);
1278
+ readModuleLayerSettings(spec);
1279
+ for (const field of [
1280
+ "composition",
1281
+ "customTools",
1282
+ "content",
1283
+ "subagentToolPolicy",
1284
+ "moduleConfigs"
1285
+ ]) {
1286
+ const value = spec[field];
1287
+ if (value !== void 0 && !(Array.isArray(value) && value.length === 0) && !(isRecord$2(value) && Object.keys(value).length === 0)) throw new Error(`角色片段不支持 ${field};请从预设页导入完整预设`);
1288
+ }
1289
+ if (spec.persona !== void 0 && (spec.persona.prefix || spec.persona.suffix || spec.persona.complete === true)) throw new Error("角色片段的 persona 正文请改为 rules 中的注入动作,或从模块页导入完整模块");
1290
+ for (const rule of spec.rules ?? []) assertSafeConfigId(rule.id);
1291
+ for (const { config } of ruleInjections(spec.rules)) {
1292
+ if (config.templateFile !== void 0) throw new Error(`角色配置 ${String(config.id)} 的 templateFile 外部文件不受支持`);
1293
+ if (isRecord$2(config.params) && config.params.file !== void 0) throw new Error(`角色配置 ${String(config.id)} 的 params.file 外部文件不受支持`);
1294
+ if (config.text !== void 0 && typeof config.text !== "string") throw new Error(`角色配置 ${String(config.id)} 的 text 必须是字符串`);
1295
+ }
1296
+ compileRules(spec.rules ?? []);
1297
+ }
1298
+ function classify(raw, target) {
1299
+ if (!isRecord$2(raw)) throw new Error("内容必须是已知格式的对象,不能是 null 或数组");
1300
+ const native = [
1301
+ "modules",
1302
+ "rules",
1303
+ "promptConfigs",
1304
+ "layerSettings",
1305
+ "params",
1306
+ "engineCompat",
1307
+ "composition",
1308
+ "content"
1309
+ ].some((key) => key in raw) || typeof raw.id === "string" && typeof raw.name === "string";
1310
+ const prompts = "prompts" in raw;
1311
+ const data = isRecord$2(raw.data) ? raw.data : raw;
1312
+ const character = /^chara_card_v[23]$/.test(String(raw.spec)) || isRecord$2(raw.data) && typeof data.name === "string" || [
1313
+ "personality",
1314
+ "first_mes",
1315
+ "mes_example",
1316
+ "character_book",
1317
+ "alternate_greetings"
1318
+ ].some((key) => key in data) || typeof data.name === "string" && typeof data.description === "string" && !native;
1319
+ const worldBook = "entries" in raw;
1320
+ if ([
1321
+ native,
1322
+ prompts,
1323
+ character,
1324
+ worldBook
1325
+ ].filter(Boolean).length !== 1) throw new Error("无法识别内容,或包含互相矛盾的原生/SillyTavern 结构");
1326
+ if (/^chara_card_v[23]$/.test(String(raw.spec)) && !isRecord$2(raw.data)) throw new Error("chara_card_v2/v3 的 data 必须是对象");
1327
+ if (native) {
1328
+ assertCanonicalRuleSource(raw);
1329
+ for (const field of [
1330
+ "rules",
1331
+ "modules",
1332
+ "customTools"
1333
+ ]) if (raw[field] !== void 0 && !Array.isArray(raw[field])) throw new Error(`${field} 必须是数组`);
1334
+ for (const field of [
1335
+ "layerSettings",
1336
+ "params",
1337
+ "meta",
1338
+ "variables",
1339
+ "moduleConfigs",
1340
+ "content"
1341
+ ]) if (raw[field] !== void 0 && !isRecord$2(raw[field])) throw new Error(`${field} 必须是对象`);
1342
+ readModuleLayerSettings(raw);
1343
+ if (raw.modules !== void 0 && raw.modules.some((value) => typeof value !== "string")) throw new Error("modules 必须是字符串数组");
1344
+ return target === "character" ? "native-character" : "native-preset";
1345
+ }
1346
+ if (prompts) {
1347
+ if (!Array.isArray(raw.prompts) || raw.prompts.some((prompt) => !isRecord$2(prompt))) throw new Error("prompts 必须是对象数组");
1348
+ return "st-preset";
1349
+ }
1350
+ if (worldBook) {
1351
+ if (!isRecord$2(raw.entries) && !Array.isArray(raw.entries)) throw new Error("entries 必须是对象或数组");
1352
+ return "world-book";
1353
+ }
1354
+ return "st-character";
1355
+ }
1356
+ /** 字节集合 → 同一规范化候选;没有目标写入和静默的未知 JSON 回退。 */
1357
+ function prepareImport(input, target, choices = {}) {
1358
+ const files = normalizeAssetFiles(input);
1359
+ const sourceDigest = assetSourceDigest(files);
1360
+ const named = files.filter((file) => /^(?:module|preset|converted)\.ya?ml$/i.test(file.path));
1361
+ if (named.length > 1) throw new Error(`存在多个定义候选:${named.map((file) => file.path).join("、")}`);
1362
+ const topFiles = files.filter((file) => !file.path.includes("/"));
1363
+ const structured = topFiles.filter((file) => /\.(json|ya?ml)$/i.test(file.path));
1364
+ const native = named.length === 0 ? structured.filter((file) => {
1365
+ try {
1366
+ const raw = decodeAssetFile(file).toString("utf8");
1367
+ return classify(/\.json$/i.test(file.path) ? JSON.parse(raw) : parseDocument(raw).toJS({ maxAliasCount: 100 }), target).startsWith("native-");
1368
+ } catch {
1369
+ return false;
1370
+ }
1371
+ }) : [];
1372
+ if (native.length > 1) throw new Error(`存在多个原生定义候选:${native.map((file) => file.path).join("、")}`);
1373
+ const candidates = named.length === 1 ? named : native.length === 1 ? native : topFiles.filter((file) => structured.includes(file) || isPngBuffer(decodeAssetFile(file)) && !(structured.length === 1 && /^avatar\.png$/i.test(file.path)));
1374
+ if (candidates.length === 0) throw new Error(`${files[0].path}: 未发现可识别的预设或角色内容`);
1375
+ if (target === "character" && candidates.length !== 1) throw new Error("角色库每次只能导入一张角色卡,请逐张预览");
1376
+ const parts = [];
1377
+ for (const file of candidates) try {
1378
+ const bytes = decodeAssetFile(file);
1379
+ const png = isPngBuffer(bytes) ? decodePngCharacterCard(bytes) : void 0;
1380
+ const sourceText = png?.jsonText ?? new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1381
+ if (png !== void 0 || /\.json$/i.test(file.path)) JSON.parse(sourceText);
1382
+ const doc = parseDocument(sourceText, { logLevel: "silent" });
1383
+ if (doc.errors.length > 0 || !(doc.contents instanceof YAMLMap)) throw new Error(doc.errors[0]?.message ?? "内容必须是已知格式的对象");
1384
+ const raw = doc.toJS({ maxAliasCount: 100 });
1385
+ let kind = classify(raw, target);
1386
+ if (kind === "native-preset" && isRecord$2(raw)) {
1387
+ const fragment = Array.isArray(raw.rules) && ![
1388
+ "version",
1389
+ "engineCompat",
1390
+ "modules",
1391
+ "composition",
1392
+ "customTools",
1393
+ "persona",
1394
+ "content"
1395
+ ].some((key) => key in raw);
1396
+ let selfContained = false;
1397
+ try {
1398
+ validateCharacterSpec(raw);
1399
+ selfContained = true;
1400
+ } catch {}
1401
+ if (selfContained && fragment && !/^(?:module|preset)\.ya?ml$/i.test(file.path) && !/^converted\.ya?ml$/i.test(file.path) && choices.sourceKind === void 0) return {
1402
+ state: "needs-kind-selection",
1403
+ kinds: ["native-preset", "native-character"],
1404
+ sourceName: file.path
1405
+ };
1406
+ if (choices.sourceKind === "native-character" || /^converted\.ya?ml$/i.test(file.path)) {
1407
+ validateCharacterSpec(raw);
1408
+ kind = "native-character";
1409
+ }
1410
+ }
1411
+ if (target === "character" && kind !== "st-character" && kind !== "native-character") throw new Error("角色库只接受角色卡;预设和独立世界书请从预设页导入");
1412
+ if (choices.sourceKind !== void 0 && choices.sourceKind !== kind) throw new Error(`所选类型 ${choices.sourceKind} 与实际 ${kind} 不一致`);
1413
+ const options = { characterId: choices.promptOrderCharacterId };
1414
+ if (kind === "st-preset") {
1415
+ const state = stOrderSelectionState(raw, options);
1416
+ if (state.needsSelection) return {
1417
+ state: "needs-order-selection",
1418
+ candidates: state.candidates,
1419
+ sourceName: file.path
1420
+ };
1421
+ if (state.error !== void 0) throw new Error(state.error);
1422
+ }
1423
+ let spec;
1424
+ let report;
1425
+ if (kind.startsWith("native-")) {
1426
+ spec = raw;
1427
+ if (spec.id === void 0) doc.set("id", stModuleId(posix.basename(file.path).replace(/\.[^.]+$/, "")));
1428
+ if (spec.name === void 0) doc.set("name", String(doc.get("id")));
1429
+ if (spec.modules === void 0 && spec.composition === void 0) {
1430
+ const legacyComposition = files.find((item) => item.path === "agent.cordis.yml");
1431
+ if (legacyComposition) doc.set("composition", decodeAssetFile(legacyComposition).toString("utf8"));
1432
+ }
1433
+ if (kind === "native-character" && spec.modules === void 0 && spec.composition === void 0) {
1434
+ const hasWorldBook = (spec.rules ?? []).some((rule) => rule.then.some((action) => action.kind === "inject-text" && action.config?.strategy === "world-book"));
1435
+ doc.set("modules", hasWorldBook ? ["world-book-tools"] : []);
1436
+ }
1437
+ if (doc.hasIn(["meta", "characterMemories"])) doc.deleteIn(["meta", "characterMemories"]);
1438
+ spec = doc.toJS();
1439
+ } else {
1440
+ const converted = convertStToModuleWithReport(raw, posix.basename(file.path).replace(/\.[^.]+$/, ""), options);
1441
+ spec = converted.spec;
1442
+ report = converted.report;
1443
+ }
1444
+ if (target === "character") validateCharacterSpec(spec);
1445
+ parts.push({
1446
+ spec,
1447
+ report,
1448
+ kind,
1449
+ yaml: kind.startsWith("native-") ? doc.toString() : stringify(spec, { lineWidth: 0 }),
1450
+ file,
1451
+ sourceText,
1452
+ ...png === void 0 ? {} : { avatar: png.avatar }
1453
+ });
1454
+ } catch (error) {
1455
+ throw new Error(`${file.path}: ${error instanceof Error ? error.message : String(error)}`);
1456
+ }
1457
+ if (parts.length > 1 && parts.some((part) => part.kind.startsWith("native-"))) throw new Error(`多个定义候选不能自动合并:${candidates.map((file) => file.path).join("、")}`);
1458
+ const first = parts[0];
1459
+ let yaml = first.yaml;
1460
+ let report = first.report;
1461
+ if (parts.length > 1) {
1462
+ const merged = mergeStModulesWithReport(parts.map((part) => part.spec));
1463
+ yaml = stringify(merged.spec, { lineWidth: 0 });
1464
+ report = mergeStConversionReports(parts.map((part) => part.report), parts.map((part, index) => ({
1465
+ sourceName: part.file.path,
1466
+ idMap: merged.idMap.get(index)
1467
+ })));
1468
+ }
1469
+ const doc = parseDocument(yaml);
1470
+ if (choices.targetId !== void 0) doc.set("id", choices.targetId);
1471
+ if (choices.targetName !== void 0) doc.set("name", choices.targetName);
1472
+ const spec = doc.toJS();
1473
+ try {
1474
+ assertModuleId(spec.id);
1475
+ } catch (error) {
1476
+ throw new Error(`${first.file.path}: ${String(error)}`);
1477
+ }
1478
+ if (typeof spec.name !== "string" || spec.name.trim().length === 0) throw new Error(`${first.file.path}: 名称必须为非空字符串`);
1479
+ yaml = doc.toString();
1480
+ const resources = files.filter((file) => !candidates.includes(file));
1481
+ if (target === "character" && resources.some((file) => !/^avatar\.png$/i.test(file.path))) throw new Error("角色片段只支持内嵌内容和 avatar.png;其他附件请从预设页导入");
1482
+ const avatarFile = resources.find((file) => /^avatar\.png$/i.test(file.path));
1483
+ const avatar = first.avatar ?? (avatarFile === void 0 ? void 0 : decodeAssetFile(avatarFile));
1484
+ if (avatar !== void 0 && !isPngBuffer(avatar)) throw new Error("avatar.png 必须提供显式编码的 PNG 原始字节");
1485
+ const preparedFiles = [{
1486
+ path: MODULE_DEFINITION_FILE,
1487
+ content: yaml,
1488
+ encoding: "utf8"
1489
+ }, ...resources];
1490
+ if ((first.kind === "st-character" || first.kind === "native-character") && !resources.some((file) => file.path === "card.json")) preparedFiles.push({
1491
+ path: "card.json",
1492
+ content: first.sourceText,
1493
+ encoding: "utf8"
1494
+ });
1495
+ if (first.avatar !== void 0 && !resources.some((file) => file.path.toLowerCase() === "avatar.png")) preparedFiles.push({
1496
+ path: "avatar.png",
1497
+ content: first.avatar.toString("base64"),
1498
+ encoding: "base64"
1499
+ });
1500
+ return {
1501
+ state: "ready",
1502
+ kind: first.kind,
1503
+ spec,
1504
+ yaml,
1505
+ files: preparedFiles,
1506
+ sourceName: candidates.map((file) => file.path).join("、"),
1507
+ sourceDigest,
1508
+ ...report === void 0 ? {} : { report },
1509
+ ...avatar === void 0 ? {} : { avatar },
1510
+ sourceText: first.sourceText
1511
+ };
1512
+ }
1513
+ //#endregion
1514
+ //#region src/host/characters.ts
1515
+ /** 角色卡只是普通模块的导入来源;历史角色库仅用于分享过滤的只读核验。 */
1516
+ function isRecord$1(value) {
1517
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1518
+ }
1519
+ /** 检查祖先与最终文件,避免 junction 或单文件链接越界。 */
1520
+ function assertNoLinks(path) {
1521
+ const absolute = resolve(path);
1522
+ const parent = dirname(absolute);
1523
+ if (parent !== absolute) assertNoLinks(parent);
1524
+ if (modulePathExists(absolute) && lstatSync(absolute).isSymbolicLink()) throw new Error(`模块路径包含链接:${absolute}`);
1525
+ }
1526
+ function memoryFile(moduleDir) {
1527
+ assertNoLinks(moduleDir);
1528
+ return join(assertModuleDirectory(dirname(moduleDir), basename(moduleDir)), "memory.md");
1529
+ }
1530
+ /** 模块记忆与条目同归当前模块;来源和 id 前缀不改变位置。 */
1531
+ function appendModuleMemory(moduleDir, note) {
1532
+ const content = note.trim();
1533
+ if (content.length === 0) return;
1534
+ const file = memoryFile(moduleDir);
1535
+ assertNoLinks(file);
1536
+ const line = `\n- [${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ")}] ${content}\n`;
1537
+ if (modulePathExists(file)) appendFileSync(file, line, "utf8");
1538
+ else writeFileSync(file, `# 本地记忆\n${line}`, "utf8");
1539
+ }
1540
+ /** 每次显式读取最新字节;缺失为空,链接与读取失败原样报错。 */
1541
+ function readModuleMemory(moduleDir) {
1542
+ const file = memoryFile(moduleDir);
1543
+ assertNoLinks(file);
1544
+ return modulePathExists(file) ? readFileSync(file, "utf8") : "";
1545
+ }
1546
+ function loadSpecFile(file) {
1547
+ assertNoLinks(file);
1548
+ if (!modulePathExists(file)) return void 0;
1549
+ const parsed = parse(readFileSync(file, "utf8"), { logLevel: "silent" });
1550
+ if (!isRecord$1(parsed)) throw new Error(`模块定义不是对象:${file}`);
1551
+ assertCanonicalRuleSource(parsed);
1552
+ return parsed;
1553
+ }
1554
+ function loadModuleSource(root, id) {
1555
+ assertModuleId(id);
1556
+ const dir = join(root, id);
1557
+ assertNoLinks(dir);
1558
+ if (!modulePathExists(dir)) return void 0;
1559
+ return loadSpecFile(join(assertModuleDirectory(root, id), MODULE_DEFINITION_FILE));
1560
+ }
1561
+ const CHARACTER_MEMORIES_KEY = "characterMemories";
1562
+ function contentHash(value) {
1563
+ const canonical = (item) => Array.isArray(item) ? item.map(canonical) : isRecord$1(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, canonical(item[key])])) : item;
1564
+ return createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex");
1565
+ }
1566
+ function memoryRecords(meta) {
1567
+ if (!isRecord$1(meta) || !isRecord$1(meta[CHARACTER_MEMORIES_KEY])) return {};
1568
+ return Object.fromEntries(Object.entries(meta[CHARACTER_MEMORIES_KEY]).filter(([id, record]) => isRecord$1(record) && record.characterId === id && typeof record.configId === "string" && typeof record.contentHash === "string"));
1569
+ }
1570
+ function importPrefixes(id) {
1571
+ return [`module-${id}-`, `chara-${id}-`];
1572
+ }
1573
+ /** 分享副本只排除可核验的历史自动记忆;来源缺失或被编辑时须明确选择。 */
1574
+ function projectCharacterMemories(source, choices = {}, moduleRoot) {
1575
+ const doc = source.clone();
1576
+ const spec = doc.toJS();
1577
+ const records = memoryRecords(spec.meta);
1578
+ const imported = [.../* @__PURE__ */ new Set([...Array.isArray(spec.meta?.importedCharacters) ? spec.meta.importedCharacters.filter((id) => typeof id === "string") : [], ...Object.keys(readMergedModules(spec.meta))])];
1579
+ const conflicts = [];
1580
+ const excluded = /* @__PURE__ */ new Set();
1581
+ assertCanonicalRuleSource(spec);
1582
+ for (const [index, config] of (spec.rules ?? []).entries()) {
1583
+ if (!isRecord$1(config) || typeof config.id !== "string") continue;
1584
+ const id = config.id;
1585
+ const proof = Object.values(records).find((record) => record.configId === id);
1586
+ if (proof !== void 0 && proof.contentHash === contentHash(config)) {
1587
+ excluded.add(index);
1588
+ continue;
1589
+ }
1590
+ const cardId = imported.find((card) => importPrefixes(card).some((base) => id === `${base}memory` || id.startsWith(`${base}memory-`))) ?? proof?.characterId;
1591
+ if (cardId === void 0) continue;
1592
+ if (moduleRoot !== void 0) {
1593
+ assertModuleId(cardId);
1594
+ const legacyDir = join(dirname(moduleRoot), ".characters", cardId);
1595
+ const normal = loadModuleSource(moduleRoot, cardId);
1596
+ const original = normal ?? loadSpecFile(join(legacyDir, "converted.yml"));
1597
+ if (original?.rules?.some((entry) => importPrefixes(cardId).some((base) => `${base}${entry.id}` === id))) continue;
1598
+ const file = join(normal === void 0 ? legacyDir : join(moduleRoot, cardId), "memory.md");
1599
+ assertNoLinks(file);
1600
+ const memory = modulePathExists(file) ? readFileSync(file, "utf8").trim() : "";
1601
+ if (original !== void 0 && memory.length > 0) {
1602
+ const entry = buildWorldBookEntry({
1603
+ name: "角色记忆",
1604
+ enabled: true,
1605
+ order: 3e3,
1606
+ text: `【${original.name} 的关系记忆】\n${memory}`,
1607
+ constant: true
1608
+ });
1609
+ if (contentHash(promptConfigToRule({
1610
+ ...entry,
1611
+ id
1612
+ })) === contentHash(config)) {
1613
+ excluded.add(index);
1614
+ continue;
1615
+ }
1616
+ }
1617
+ }
1618
+ if (choices[id] === "exclude") excluded.add(index);
1619
+ else if (choices[id] !== "include") conflicts.push({
1620
+ id,
1621
+ name: String(config.name ?? id)
1622
+ });
1623
+ }
1624
+ const configs = doc.get("rules", true);
1625
+ if (configs instanceof YAMLSeq) for (const index of [...excluded].sort((a, b) => b - a)) configs.delete(index);
1626
+ else if (excluded.size > 0) doc.set("rules", (spec.rules ?? []).filter((_, index) => !excluded.has(index)));
1627
+ if (doc.hasIn(["meta", CHARACTER_MEMORIES_KEY])) doc.deleteIn(["meta", CHARACTER_MEMORIES_KEY]);
1628
+ return {
1629
+ doc,
1630
+ excludedMemoryCount: excluded.size,
1631
+ memoryConflicts: conflicts
1632
+ };
1633
+ }
1634
+ function importChoices(options) {
1635
+ return {
1636
+ ...options,
1637
+ promptOrderCharacterId: options.promptOrderCharacterId ?? options.characterId
1638
+ };
1639
+ }
1640
+ /** 复用普通模块候选校验、版本保护和目录交换,不生成角色专属存储。 */
1641
+ async function importCharacterCard(root, files, options = {}) {
1642
+ try {
1643
+ const prepared = prepareImport(files, "character", importChoices(options));
1644
+ if (prepared.state !== "ready") return {
1645
+ ok: false,
1646
+ message: "请先选择角色内容类型或提示顺序组"
1647
+ };
1648
+ const moduleFiles = prepared.files;
1649
+ const { moduleImportPreview, installModulePackage } = await import("./module-package-CHPFweqK.mjs");
1650
+ const request = {
1651
+ ...importChoices(options),
1652
+ sourceKind: "native-character"
1653
+ };
1654
+ const preview = moduleImportPreview(root, moduleFiles, request);
1655
+ if (!("prepared" in preview)) return {
1656
+ ok: false,
1657
+ message: "请先选择角色内容类型或提示顺序组"
1658
+ };
1659
+ if (preview.summary.exists && options.overwrite === true) readModuleMemory(join(root, preview.summary.targetId));
1660
+ const installed = await installModulePackage(root, moduleFiles, {
1661
+ ...request,
1662
+ expectedSourceDigest: preview.sourceDigest,
1663
+ expectedPreviewRevision: preview.previewRevision
1664
+ }, { preserveAssets: true });
1665
+ return {
1666
+ ok: true,
1667
+ id: installed.id,
1668
+ name: preview.summary.targetName,
1669
+ ...installed.backupPath === void 0 ? {} : { warning: `模块已安装;旧备份保留在 ${installed.backupPath}` }
1670
+ };
1671
+ } catch (error) {
1672
+ return {
1673
+ ok: false,
1674
+ message: `角色卡导入失败:${error instanceof Error ? error.message : String(error)}`
1675
+ };
1676
+ }
1677
+ }
1678
+ async function importCharacterCardFile(root, filePath, fileName = basename(filePath), options = {}) {
1679
+ try {
1680
+ const buffer = readFileSync(filePath);
1681
+ return await importCharacterCard(root, [{
1682
+ path: basename(fileName),
1683
+ content: buffer.toString("base64"),
1684
+ encoding: "base64"
1685
+ }], options);
1686
+ } catch (error) {
1687
+ return {
1688
+ ok: false,
1689
+ message: `角色卡转换失败:${error instanceof Error ? error.message : String(error)}`
1690
+ };
1691
+ }
1692
+ }
1693
+ /** 旧登记仅在此读边界转换,新写入只落 mergedModules。 */
1694
+ function readMergedModules(meta) {
1695
+ if (!isRecord$1(meta)) return {};
1696
+ if (isRecord$1(meta.mergedModules)) return Object.fromEntries(Object.entries(meta.mergedModules).filter(([, value]) => isRecord$1(value) && typeof value.active === "boolean" && Array.isArray(value.capabilities)).map(([id, value]) => {
1697
+ const row = value;
1698
+ return [id, {
1699
+ active: row.active === true,
1700
+ capabilities: row.capabilities.filter((item) => typeof item === "string")
1701
+ }];
1702
+ }));
1703
+ const active = Array.isArray(meta.importedCharacters) ? meta.importedCharacters.filter((id) => typeof id === "string") : [];
1704
+ const old = isRecord$1(meta.characterModules) ? meta.characterModules : {};
1705
+ return Object.fromEntries([.../* @__PURE__ */ new Set([...active, ...Object.keys(old)])].map((id) => [id, {
1706
+ active: active.includes(id),
1707
+ capabilities: Array.isArray(old[id]) ? old[id].filter((item) => typeof item === "string") : []
1708
+ }]));
1709
+ }
1710
+ function writeMergedModules(doc, records) {
1711
+ doc.setIn(["meta", "mergedModules"], records);
1712
+ for (const key of ["importedCharacters", "characterModules"]) if (doc.hasIn(["meta", key])) doc.deleteIn(["meta", key]);
1713
+ }
1714
+ function declaredCapabilities(spec) {
1715
+ return Array.isArray(spec.modules) ? [...new Set(spec.modules.filter((item) => typeof item === "string" && item.length > 0))] : [];
1716
+ }
1717
+ function readCapabilities(doc) {
1718
+ return declaredCapabilities(doc.toJS());
1719
+ }
1720
+ function capabilityStillNeeded(capability, spec, active) {
1721
+ const configs = ruleInjections(spec.rules ?? []).map(({ config }) => config);
1722
+ switch (capability) {
1723
+ case "rule-engine": return (spec.rules?.length ?? 0) > 0;
1724
+ case "prompt-config-engine": return configs.length > 0;
1725
+ case "world-book-tools": return configs.some((config) => config.strategy === "world-book");
1726
+ case "session-var-tools": return configs.some((config) => isRecord$1(config.params) && config.params.stMacros === true);
1727
+ case "tool-config-engine": return (spec.customTools?.length ?? 0) > 0;
1728
+ case "character-tools": return active.length > 0 || configs.some((config) => /^(module|chara)-/.test(String(config.id ?? "")));
1729
+ default: return true;
1730
+ }
1731
+ }
1732
+ /** 通用并入保留既有带前缀往返行为,不复制本地记忆。 */
1733
+ function mergeModuleIntoModule(root, targetId, sourceId) {
1734
+ try {
1735
+ if (sourceId === targetId) throw new Error("不能把模块并入自身");
1736
+ const dir = assertModuleDirectory(root, targetId);
1737
+ const spec = loadModuleSource(root, sourceId);
1738
+ if (spec === void 0) throw new Error(`模块 ${sourceId} 不存在`);
1739
+ validateCharacterSpec(spec);
1740
+ const prefix = `module-${sourceId}-`;
1741
+ let count = 0;
1742
+ let personaOpened = false;
1743
+ withModuleDefinition(dir, (doc) => {
1744
+ const current = doc.toJS();
1745
+ assertCanonicalRuleSource(current);
1746
+ if (ruleInjections(spec.rules).some(({ config }) => config.layer === "system-section") && current.persona?.complete === true) {
1747
+ doc.setIn(["persona", "complete"], false);
1748
+ personaOpened = true;
1749
+ }
1750
+ const existing = (current.rules ?? []).filter((rule) => !importPrefixes(sourceId).some((base) => rule.id.startsWith(base)));
1751
+ const added = (spec.rules ?? []).map((rule) => mapRuleInjections({
1752
+ ...rule,
1753
+ id: `${prefix}${rule.id}`
1754
+ }, (config) => ({
1755
+ ...config,
1756
+ id: `${prefix}${config.id}`,
1757
+ variables: {
1758
+ ...spec.variablesEnabled === false ? {} : spec.variables,
1759
+ ...config.variables
1760
+ }
1761
+ })));
1762
+ for (const [key, value] of Object.entries(readModuleLayerSettings(spec))) doc.setIn(engineParamPath(key), value);
1763
+ const merged = [...existing, ...added];
1764
+ compileRules(merged, { promptConfigOptions: rulePromptConfigOptions(dir) });
1765
+ const before = readCapabilities(doc);
1766
+ const required = ["rule-engine", ...ruleInjections(merged).some(({ config }) => config.strategy === "world-book") ? ["world-book-tools"] : []];
1767
+ appendModuleCapabilities(doc, [.../* @__PURE__ */ new Set([...declaredCapabilities(spec), ...required])]);
1768
+ const records = readMergedModules(current.meta);
1769
+ records[sourceId] = {
1770
+ active: true,
1771
+ capabilities: [.../* @__PURE__ */ new Set([...records[sourceId]?.capabilities ?? [], ...readCapabilities(doc).filter((capability) => !before.includes(capability))])]
1772
+ };
1773
+ writeMergedModules(doc, records);
1774
+ doc.set("rules", merged);
1775
+ if (Array.isArray(spec.meta?.stWarnings) && spec.meta.stWarnings.length > 0) {
1776
+ const warnings = Array.isArray(current.meta?.stWarnings) ? current.meta.stWarnings : [];
1777
+ doc.setIn(["meta", "stWarnings"], [...new Set([...warnings, ...spec.meta.stWarnings].filter((value) => typeof value === "string"))]);
1778
+ }
1779
+ count = added.length;
1780
+ });
1781
+ return {
1782
+ ok: true,
1783
+ count,
1784
+ ...personaOpened ? { personaOpened: true } : {}
1785
+ };
1786
+ } catch (error) {
1787
+ return {
1788
+ ok: false,
1789
+ message: `并入失败:${error instanceof Error ? error.message : String(error)}`
1790
+ };
1791
+ }
1792
+ }
1793
+ /** 撤销并入内容;旧 chara 前缀可撤销,能力归属只写一份登记。 */
1794
+ function removeMergedModule(root, targetId, sourceId) {
1795
+ try {
1796
+ assertModuleId(sourceId);
1797
+ const dir = assertModuleDirectory(root, targetId);
1798
+ const spec = loadModuleSource(root, sourceId);
1799
+ let removed = 0;
1800
+ withModuleDefinition(dir, (doc) => {
1801
+ const current = doc.toJS();
1802
+ assertCanonicalRuleSource(current);
1803
+ const kept = (current.rules ?? []).filter((rule) => !importPrefixes(sourceId).some((base) => rule.id.startsWith(base)));
1804
+ removed = (current.rules?.length ?? 0) - kept.length;
1805
+ compileRules(kept, { promptConfigOptions: rulePromptConfigOptions(dir) });
1806
+ doc.set("rules", kept);
1807
+ for (const rule of current.rules ?? []) if (!kept.some((item) => item.id === rule.id) && doc.hasIn(["configOrder", rule.id])) doc.deleteIn(["configOrder", rule.id]);
1808
+ if (doc.hasIn([
1809
+ "meta",
1810
+ CHARACTER_MEMORIES_KEY,
1811
+ sourceId
1812
+ ])) doc.deleteIn([
1813
+ "meta",
1814
+ CHARACTER_MEMORIES_KEY,
1815
+ sourceId
1816
+ ]);
1817
+ for (const [key, value] of Object.entries(spec === void 0 ? {} : readModuleLayerSettings(spec))) {
1818
+ const path = engineParamPath(key);
1819
+ if (doc.getIn(path) === value) doc.deleteIn(path);
1820
+ }
1821
+ const records = readMergedModules(current.meta);
1822
+ if (records[sourceId] !== void 0) records[sourceId].active = false;
1823
+ const active = Object.keys(records).filter((id) => records[id].active);
1824
+ const claimed = /* @__PURE__ */ new Set();
1825
+ for (const id of active) {
1826
+ for (const capability of records[id].capabilities) claimed.add(capability);
1827
+ const other = loadModuleSource(root, id);
1828
+ if (other !== void 0) for (const capability of declaredCapabilities(other)) claimed.add(capability);
1829
+ }
1830
+ const introduced = new Set(Object.values(records).flatMap((record) => record.capabilities));
1831
+ const next = readCapabilities(doc).filter((capability) => !introduced.has(capability) || claimed.has(capability) || capabilityStillNeeded(capability, doc.toJS(), active));
1832
+ doc.set("modules", next);
1833
+ for (const [id, record] of Object.entries(records)) {
1834
+ record.capabilities = record.capabilities.filter((capability) => next.includes(capability));
1835
+ if (!record.active && record.capabilities.length === 0) delete records[id];
1836
+ }
1837
+ writeMergedModules(doc, records);
1838
+ });
1839
+ return {
1840
+ ok: true,
1841
+ count: removed
1842
+ };
1843
+ } catch (error) {
1844
+ return {
1845
+ ok: false,
1846
+ message: `移除失败:${error instanceof Error ? error.message : String(error)}`
1847
+ };
1848
+ }
1849
+ }
1850
+ //#endregion
1851
+ //#region src/host/asset-archive.ts
1852
+ /** ZIP 仅负责载体,不决定资产语义或写入目标。 */
1853
+ function safeAssetPath(path) {
1854
+ if (typeof path !== "string" || path.length === 0 || path.length > 1024 || path.includes("\\") || path.startsWith("/") || /[\p{Cc}<>:"|?*]/u.test(path)) throw new Error(`非法资源路径:${path}`);
1855
+ if (path.split("/").some((part) => part === "" || part === "." || part === ".." || /[. ]$/.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw new Error(`非法资源路径:${path}`);
1856
+ return path;
1857
+ }
1858
+ async function unpackZip(bytes) {
1859
+ if (bytes.length > 67108864) throw new Error("ZIP 超过 64 MiB 上限");
1860
+ const reader = new ZipReader(new Uint8ArrayReader(bytes), { useWebWorkers: false });
1861
+ const files = [];
1862
+ const seen = /* @__PURE__ */ new Set();
1863
+ let total = 0;
1864
+ let entries = 0;
1865
+ try {
1866
+ for await (const entry of reader.getEntriesGenerator()) {
1867
+ if (++entries > 2048) throw new Error("ZIP 文件数超过上限");
1868
+ const path = safeAssetPath(entry.filename.replace(/\/$/, ""));
1869
+ const key = path.normalize("NFC").toLowerCase();
1870
+ if (seen.has(key)) throw new Error(`ZIP 路径重复:${path}`);
1871
+ seen.add(key);
1872
+ const fileType = entry.externalFileAttributes >>> 16 & 61440;
1873
+ if (entry.encrypted || entry.symlink || ![
1874
+ 0,
1875
+ 32768,
1876
+ 16384
1877
+ ].includes(fileType)) throw new Error(`ZIP 不允许加密项、链接或特殊文件:${path}`);
1878
+ if (entry.directory) continue;
1879
+ if (entry.uncompressedSize > 67108864 || total + entry.uncompressedSize > 67108864) throw new Error("ZIP 解压大小超过 64 MiB");
1880
+ const chunks = [];
1881
+ let size = 0;
1882
+ await entry.getData(new WritableStream({ write(chunk) {
1883
+ size += chunk.length;
1884
+ total += chunk.length;
1885
+ if (total > 67108864) throw new Error("ZIP 实际解压大小超过 64 MiB");
1886
+ chunks.push(Buffer.from(chunk));
1887
+ } }), { checkSignature: true });
1888
+ if (size !== entry.uncompressedSize) throw new Error(`ZIP 长度不符:${path}`);
1889
+ files.push({
1890
+ path,
1891
+ encoding: "base64",
1892
+ content: Buffer.concat(chunks).toString("base64")
1893
+ });
1894
+ }
1895
+ } finally {
1896
+ await reader.close();
1897
+ }
1898
+ if (files.length === 0) throw new Error("ZIP 没有可导入文件");
1899
+ return files;
1900
+ }
1901
+ async function packZip(files) {
1902
+ const writer = new ZipWriter(new Uint8ArrayWriter(), { useWebWorkers: false });
1903
+ for (const file of files) await writer.add(safeAssetPath(file.path), new Uint8ArrayReader(file.bytes));
1904
+ return writer.close();
1905
+ }
1906
+ //#endregion
1907
+ //#region src/host/preview-revision.ts
1908
+ /**
1909
+ * 导入预览版本(previewRevision):把「这次预览代表哪一次输入」压成稳定摘要。
1910
+ *
1911
+ * 预览身份与写入凭证是两件事:`sourceDigest` 只证明「提交的文件内容与预览时相同」,
1912
+ * 而 `previewRevision` 还绑定**实际选组**、**转换器版本**与**目标身份(含目标当前版本)**,
1913
+ * 因此换组、换文件、换目标或预览期间目标被改动都会失配。版本永远由服务端计算,
1914
+ * 客户端只回传;凭据不构成写入授权,服务端每次提交都重算。
1915
+ */
1916
+ /** 预览协议版本:字段语义变化时递增,纳入 revision 计算。 */
1917
+ const PREVIEW_PROTOCOL_VERSION = "st-preview/1";
1918
+ /** 文件顺序参与身份(多文件合并结果依赖顺序),因此不做排序。 */
1919
+ function fileFingerprint(entry) {
1920
+ return [entry.path, createHash("sha256").update(entry.content).digest("hex")];
1921
+ }
1922
+ /** 结构化、确定序列化后的 SHA-256:同输入必得同值,任一绑定项变化即改变。 */
1923
+ function computePreviewRevision(input) {
1924
+ const canonical = JSON.stringify([
1925
+ PREVIEW_PROTOCOL_VERSION,
1926
+ input.converter,
1927
+ input.files.map(fileFingerprint),
1928
+ input.orderCharacterId ?? null,
1929
+ [
1930
+ input.target.kind,
1931
+ input.target.targetId,
1932
+ input.target.targetVersion,
1933
+ input.target.ownerPreset
1934
+ ]
1935
+ ]);
1936
+ return createHash("sha256").update(canonical).digest("hex");
1937
+ }
1938
+ /**
1939
+ * 目录内容版本:按相对路径排序后逐个摘要文件内容,目录不存在返回 `null`。
1940
+ * 只用于目标身份的"是否被改过"判断,不持久化、不缓存。
1941
+ */
1942
+ function directoryVersionOf(dir) {
1943
+ try {
1944
+ if (!lstatSync(dir).isDirectory() || lstatSync(dir).isSymbolicLink()) throw new Error("目标不是普通目录");
1945
+ } catch (error) {
1946
+ if (error.code === "ENOENT") return null;
1947
+ throw error;
1948
+ }
1949
+ const hash = createHash("sha256");
1950
+ const walk = (current, prefix) => {
1951
+ for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1952
+ if (entry.isSymbolicLink()) throw new Error(`目标包含链接:${entry.name}`);
1953
+ const rel = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
1954
+ if (entry.isDirectory()) {
1955
+ hash.update(`D\u0000${rel}\u0000`);
1956
+ walk(join(current, entry.name), rel);
1957
+ continue;
1958
+ }
1959
+ if (!entry.isFile()) continue;
1960
+ hash.update(`F\u0000${rel}\u0000`);
1961
+ hash.update(readFileSync(join(current, entry.name)));
1962
+ }
1963
+ };
1964
+ walk(dir, "");
1965
+ return hash.digest("hex");
1966
+ }
1967
+ //#endregion
1968
+ //#region engine/tool-definition.mjs
1969
+ /** 自定义工具物化定义的纯校验:host 保存/物化与运行时共用,不加载宿主服务。 */
1970
+ const TOOL_NAME_RE = /^[a-z][a-z0-9_]*$/;
1971
+ const JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set([
1972
+ "string",
1973
+ "number",
1974
+ "integer",
1975
+ "boolean",
1976
+ "null",
1977
+ "array",
1978
+ "object"
1979
+ ]);
1980
+ const JSON_SCHEMA_ANNOTATIONS = /* @__PURE__ */ new Set([
1981
+ "description",
1982
+ "title",
1983
+ "default",
1984
+ "examples"
1985
+ ]);
1986
+ /** JSON Schema 结构校验;DSL 转换与官方 registry 仍负责 enforced subset 校验。 */
1987
+ function validateJsonSchemaNode(node, path, seen = /* @__PURE__ */ new Set()) {
1988
+ if (node === null || typeof node !== "object" || Array.isArray(node)) throw new TypeError(`${path} must be an object`);
1989
+ if (seen.has(node)) throw new TypeError(`${path} is circular`);
1990
+ seen.add(node);
1991
+ try {
1992
+ if (node.type !== void 0) {
1993
+ if (!JSON_SCHEMA_TYPES.has(node.type)) throw new TypeError(`${path}.type must be one of ${[...JSON_SCHEMA_TYPES].join("/")} (dsh-tools DSL 形态 'json'/'oneOf' 未在物化时转换)`);
1994
+ if (node.type === "object") {
1995
+ if (node.properties !== void 0) {
1996
+ if (node.properties === null || typeof node.properties !== "object" || Array.isArray(node.properties)) throw new TypeError(`${path}.properties must be an object of schemas`);
1997
+ for (const [key, child] of Object.entries(node.properties)) validateJsonSchemaNode(child, `${path}.properties.${key}`, seen);
1998
+ }
1999
+ if (node.required !== void 0) {
2000
+ if (!Array.isArray(node.required) || node.required.some((item) => typeof item !== "string")) throw new TypeError(`${path}.required must be an array of property names`);
2001
+ }
2002
+ if (node.additionalProperties !== void 0 && typeof node.additionalProperties !== "boolean") throw new TypeError(`${path}.additionalProperties must be a boolean`);
2003
+ }
2004
+ if (node.type === "array" && node.items !== void 0) validateJsonSchemaNode(node.items, `${path}.items`, seen);
2005
+ } else if (Array.isArray(node.oneOf)) {
2006
+ if (node.oneOf.length < 2) throw new TypeError(`${path}.oneOf needs at least two branches`);
2007
+ for (const [index, branch] of node.oneOf.entries()) validateJsonSchemaNode(branch, `${path}.oneOf[${index}]`, seen);
2008
+ } else if (!Object.keys(node).every((key) => JSON_SCHEMA_ANNOTATIONS.has(key))) throw new TypeError(`${path} must declare type or oneOf (or contain annotations only)`);
2009
+ } finally {
2010
+ seen.delete(node);
2011
+ }
2012
+ }
2013
+ /** 完整定义校验;保存方拒绝写盘,writer / runtime 按单条告警跳过。 */
2014
+ function validateDefinition(def) {
2015
+ if (def === null || typeof def !== "object" || Array.isArray(def)) throw new TypeError("tool definition must be an object");
2016
+ if (typeof def.id !== "string" || def.id.trim().length === 0) throw new TypeError("tool definition needs a non-empty string id");
2017
+ if (typeof def.name !== "string" || !TOOL_NAME_RE.test(def.name)) throw new TypeError(`tool name ${JSON.stringify(def.name)} must match ${TOOL_NAME_RE}`);
2018
+ if (typeof def.description !== "string" || def.description.trim().length === 0) throw new TypeError(`tool ${def.id}: description is required`);
2019
+ if (def.scope !== void 0) throw new TypeError(`tool ${def.id}: customTools.scope is not supported (subagent tool policy is configured via subagentToolPolicy, not per-tool scope)`);
2020
+ if (def.parameters !== void 0) validateJsonSchemaNode(def.parameters, `tool ${def.id}: parameters`);
2021
+ const output = def.output;
2022
+ if (output === null || typeof output !== "object" || Array.isArray(output)) throw new TypeError(`tool ${def.id}: output is required`);
2023
+ validateJsonSchemaNode(output.schema, `tool ${def.id}: output.schema`);
2024
+ const exec = def.execute;
2025
+ if (exec === null || typeof exec !== "object" || Array.isArray(exec)) throw new TypeError(`tool ${def.id}: execute is required`);
2026
+ if (typeof exec.kind !== "string" || ![
2027
+ "shell",
2028
+ "http",
2029
+ "delegate",
2030
+ "fs",
2031
+ "ask-user"
2032
+ ].includes(exec.kind)) throw new TypeError(`tool ${def.id}: execute.kind must be shell/http/delegate/fs/ask-user`);
2033
+ if (exec.kind === "shell" && (typeof exec.command !== "string" || exec.command.trim().length === 0)) throw new TypeError(`tool ${def.id}: execute.command is required for shell`);
2034
+ if (exec.kind === "http" && (typeof exec.url !== "string" || exec.url.trim().length === 0)) throw new TypeError(`tool ${def.id}: execute.url is required for http`);
2035
+ if (exec.kind === "delegate" && (typeof exec.tool !== "string" || exec.tool.length === 0)) throw new TypeError(`tool ${def.id}: execute.tool is required for delegate`);
2036
+ if (exec.kind === "fs" && (typeof exec.action !== "string" || ![
2037
+ "read",
2038
+ "write",
2039
+ "append",
2040
+ "list",
2041
+ "delete"
2042
+ ].includes(exec.action) && !exec.action.includes("{{args."))) throw new TypeError(`tool ${def.id}: execute.action must be read/write/append/list/delete (or {{args.*}} template) for fs`);
2043
+ if (def.enabled !== void 0 && typeof def.enabled !== "boolean") throw new TypeError(`tool ${def.id}: enabled must be a boolean`);
2044
+ if (def.timeoutMs !== void 0 && (!Number.isSafeInteger(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > 2147483647)) throw new TypeError(`tool ${def.id}: timeoutMs must be an integer from 1 to 2147483647`);
2045
+ for (const field of [
2046
+ "command",
2047
+ "shell",
2048
+ "url",
2049
+ "method",
2050
+ "tool",
2051
+ "action",
2052
+ "path",
2053
+ "content",
2054
+ "question"
2055
+ ]) if (exec[field] !== void 0 && typeof exec[field] !== "string") throw new TypeError(`tool ${def.id}: execute.${field} must be a string`);
2056
+ if (exec.shell !== void 0 && (exec.shell.trim().length === 0 || exec.shell.includes("\0"))) throw new TypeError(`tool ${def.id}: execute.shell must be a non-empty executable name or path without NUL`);
2057
+ if (exec.method !== void 0 && !/^\{\{args\.[A-Za-z0-9_.]+\}\}$/.test(exec.method) && (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(exec.method) || [
2058
+ "CONNECT",
2059
+ "TRACE",
2060
+ "TRACK"
2061
+ ].includes(exec.method.toUpperCase()))) throw new TypeError(`tool ${def.id}: execute.method must be a supported HTTP method token`);
2062
+ if (exec.kind === "fs" && (typeof exec.path !== "string" || exec.path.includes("\0"))) throw new TypeError(`tool ${def.id}: execute.path must be a string without NUL for fs`);
2063
+ for (const field of [
2064
+ "env",
2065
+ "headers",
2066
+ "args"
2067
+ ]) {
2068
+ const value = exec[field];
2069
+ if (value === void 0) continue;
2070
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`tool ${def.id}: execute.${field} must be an object`);
2071
+ if (field !== "args" && Object.values(value).some((entry) => typeof entry !== "string")) throw new TypeError(`tool ${def.id}: execute.${field} values must be strings`);
2072
+ if (field === "env" && Object.entries(value).some(([key, entry]) => key.length === 0 || /[=\0]/.test(key) || entry.includes("\0"))) throw new TypeError(`tool ${def.id}: execute.env must contain valid environment names and values`);
2073
+ if (field === "headers") try {
2074
+ new Headers(value);
2075
+ } catch {
2076
+ throw new TypeError(`tool ${def.id}: execute.headers must contain valid HTTP names and values`);
2077
+ }
2078
+ }
2079
+ let serialized;
2080
+ try {
2081
+ serialized = JSON.stringify(def);
2082
+ } catch {
2083
+ throw new TypeError(`tool ${def.id}: definition must be JSON serializable`);
2084
+ }
2085
+ if (Buffer.byteLength(serialized, "utf8") > 1048576) throw new TypeError(`tool ${def.id}: definition must not exceed 1048576 UTF-8 bytes`);
2086
+ }
2087
+ //#endregion
2088
+ //#region src/host/custom-tools.ts
2089
+ /** 自定义工具的保存校验与物化:只编译定义,不注册或执行工具,不修改输入。 */
2090
+ /** 官方 DSL → 运行时 JSON Schema;非法字段抛错,id 必填,空 name 回落 id。 */
2091
+ function compileCustomTool(tool) {
2092
+ if (tool === null || typeof tool !== "object" || Array.isArray(tool)) throw new TypeError("custom tool must be an object");
2093
+ const id = typeof tool.id === "string" ? tool.id.trim() : "";
2094
+ assertSafeConfigId(id);
2095
+ const name = typeof tool.name === "string" ? tool.name.trim() || id : tool.name === void 0 ? id : tool.name;
2096
+ const next = {
2097
+ ...tool,
2098
+ id,
2099
+ name
2100
+ };
2101
+ if (tool.parameters !== void 0) try {
2102
+ next.parameters = parameterSchemaSpecToJsonSchema(tool.parameters);
2103
+ } catch (error) {
2104
+ throw new TypeError(`parameters: ${error instanceof Error ? error.message : String(error)}`);
2105
+ }
2106
+ const output = tool.output;
2107
+ if (output === null || typeof output !== "object" || Array.isArray(output)) throw new TypeError("output.schema is required");
2108
+ const outputRecord = output;
2109
+ try {
2110
+ next.output = {
2111
+ ...outputRecord,
2112
+ schema: valueSchemaSpecToJsonSchema(outputRecord.schema)
2113
+ };
2114
+ } catch (error) {
2115
+ throw new TypeError(`output.schema: ${error instanceof Error ? error.message : String(error)}`);
2116
+ }
2117
+ validateDefinition(next);
2118
+ return next;
2119
+ }
2120
+ /** 先拒绝非法/重复身份,再完整编译每条定义(包括停用条目),供 bridge 写盘前调用。 */
2121
+ function validateCustomTools(tools) {
2122
+ const errors = validateCustomToolIdentities(tools);
2123
+ if (errors.length > 0) return errors;
2124
+ for (const [index, tool] of tools.entries()) try {
2125
+ compileCustomTool(tool);
2126
+ } catch (error) {
2127
+ errors.push(`customTools[${index}]: ${error instanceof Error ? error.message : String(error)}`);
2128
+ }
2129
+ return errors;
2130
+ }
2131
+ //#endregion
2132
+ //#region engine/subagent-tool-policy-core.mjs
2133
+ /** 解析字符串列表(数组或逗号/空格分隔字符串),非字符串项报错。 */
2134
+ function stringList(value, path, errors, { allowEmpty = true } = {}) {
2135
+ if (value === void 0 || value === null) return [];
2136
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(/[,\s]+/) : void 0;
2137
+ if (raw === void 0) {
2138
+ errors.push(`${path} must be an array of strings`);
2139
+ return [];
2140
+ }
2141
+ const out = [];
2142
+ for (const item of raw) {
2143
+ if (typeof item !== "string" || item.trim().length === 0) {
2144
+ errors.push(`${path} must contain only non-empty strings`);
2145
+ continue;
2146
+ }
2147
+ if (!out.includes(item.trim())) out.push(item.trim());
2148
+ }
2149
+ return out;
2150
+ }
2151
+ /** 校验策略;返回错误列表(空 = 合法)。 */
2152
+ function validateSubagentToolPolicy(raw) {
2153
+ const errors = [];
2154
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return ["subagentToolPolicy must be an object"];
2155
+ const defaultProfile = raw.defaultProfile;
2156
+ if (typeof defaultProfile !== "string" || defaultProfile.length === 0) errors.push("subagentToolPolicy.defaultProfile is required");
2157
+ const ceiling = raw.ceiling;
2158
+ if (ceiling === null || typeof ceiling !== "object" || Array.isArray(ceiling)) errors.push("subagentToolPolicy.ceiling is required");
2159
+ const ceilingAllow = ceiling !== null && typeof ceiling === "object" && !Array.isArray(ceiling) ? stringList(ceiling.allow, "ceiling.allow", errors) : [];
2160
+ if (ceilingAllow.length === 0) errors.push("subagentToolPolicy.ceiling.allow must be a non-empty list");
2161
+ ceiling !== null && typeof ceiling === "object" && !Array.isArray(ceiling) && stringList(ceiling.deny, "ceiling.deny", errors);
2162
+ const profileIds = /* @__PURE__ */ new Set();
2163
+ const profiles = Array.isArray(raw.profiles) ? raw.profiles : [];
2164
+ if (profiles.length === 0) errors.push("subagentToolPolicy.profiles must declare at least one profile");
2165
+ for (const [index, profile] of profiles.entries()) {
2166
+ const path = `profiles[${index}]`;
2167
+ if (profile === null || typeof profile !== "object" || Array.isArray(profile)) {
2168
+ errors.push(`${path} must be an object`);
2169
+ continue;
2170
+ }
2171
+ if (typeof profile.id !== "string" || profile.id.length === 0) errors.push(`${path}.id is required`);
2172
+ else {
2173
+ if (profileIds.has(profile.id)) errors.push(`${path}.id ${JSON.stringify(profile.id)} is duplicated`);
2174
+ profileIds.add(profile.id);
2175
+ }
2176
+ if (typeof profile.name !== "string" || profile.name.length === 0) errors.push(`${path}.name is required`);
2177
+ const allow = stringList(profile.allow, `${path}.allow`, errors);
2178
+ const deny = stringList(profile.deny, `${path}.deny`, errors);
2179
+ for (const tool of allow) if (!ceilingAllow.includes(tool)) errors.push(`${path}.allow tool ${JSON.stringify(tool)} exceeds ceiling.allow`);
2180
+ for (const tool of deny) if (!ceilingAllow.includes(tool)) errors.push(`${path}.deny tool ${JSON.stringify(tool)} is not in ceiling.allow`);
2181
+ if (typeof profile.modelSelectable !== "undefined" && typeof profile.modelSelectable !== "boolean") errors.push(`${path}.modelSelectable must be a boolean`);
2182
+ }
2183
+ const characterIds = /* @__PURE__ */ new Set();
2184
+ const characterBindings = Array.isArray(raw.characterBindings) ? raw.characterBindings : [];
2185
+ for (const [index, binding] of characterBindings.entries()) {
2186
+ const path = `characterBindings[${index}]`;
2187
+ if (binding === null || typeof binding !== "object" || Array.isArray(binding)) {
2188
+ errors.push(`${path} must be an object`);
2189
+ continue;
2190
+ }
2191
+ if (typeof binding.characterId !== "string" || binding.characterId.length === 0) errors.push(`${path}.characterId is required`);
2192
+ else {
2193
+ if (characterIds.has(binding.characterId)) errors.push(`${path}.characterId ${JSON.stringify(binding.characterId)} is duplicated`);
2194
+ characterIds.add(binding.characterId);
2195
+ }
2196
+ if (typeof binding.profile !== "string" || !profileIds.has(binding.profile)) errors.push(`${path}.profile ${JSON.stringify(binding.profile)} must reference an existing profile`);
2197
+ }
2198
+ const taskIds = /* @__PURE__ */ new Set();
2199
+ const taskRules = Array.isArray(raw.taskRules) ? raw.taskRules : [];
2200
+ for (const [index, rule] of taskRules.entries()) {
2201
+ const path = `taskRules[${index}]`;
2202
+ if (rule === null || typeof rule !== "object" || Array.isArray(rule)) {
2203
+ errors.push(`${path} must be an object`);
2204
+ continue;
2205
+ }
2206
+ if (typeof rule.id !== "string" || rule.id.length === 0) errors.push(`${path}.id is required`);
2207
+ else {
2208
+ if (taskIds.has(rule.id)) errors.push(`${path}.id ${JSON.stringify(rule.id)} is duplicated`);
2209
+ taskIds.add(rule.id);
2210
+ }
2211
+ if (typeof rule.pattern !== "string" || rule.pattern.length === 0) errors.push(`${path}.pattern is required`);
2212
+ else try {
2213
+ new RegExp(rule.pattern);
2214
+ } catch (error) {
2215
+ errors.push(`${path}.pattern is not a valid regular expression: ${String(error.message ?? error)}`);
2216
+ }
2217
+ if (typeof rule.order !== "number" || !Number.isSafeInteger(rule.order)) errors.push(`${path}.order must be an integer`);
2218
+ if (typeof rule.profile !== "string" || !profileIds.has(rule.profile)) errors.push(`${path}.profile ${JSON.stringify(rule.profile)} must reference an existing profile`);
2219
+ }
2220
+ const expansion = raw.modelExpansion;
2221
+ if (expansion !== void 0 && expansion !== null) {
2222
+ const expansionPath = "modelExpansion";
2223
+ if (typeof expansion !== "object" || Array.isArray(expansion)) errors.push(`${expansionPath} must be an object`);
2224
+ else {
2225
+ const expansionAllow = stringList(expansion.allow, `${expansionPath}.allow`, errors);
2226
+ for (const tool of expansionAllow) if (!ceilingAllow.includes(tool)) errors.push(`${expansionPath}.allow tool ${JSON.stringify(tool)} exceeds ceiling.allow`);
2227
+ if (expansion.maxAdditionalTools !== void 0 && (!Number.isSafeInteger(expansion.maxAdditionalTools) || expansion.maxAdditionalTools < 0)) errors.push(`${expansionPath}.maxAdditionalTools must be a non-negative safe integer`);
2228
+ }
2229
+ }
2230
+ if (defaultProfile !== void 0 && typeof defaultProfile === "string" && defaultProfile.length > 0 && !profileIds.has(defaultProfile)) errors.push(`subagentToolPolicy.defaultProfile ${JSON.stringify(defaultProfile)} must reference an existing profile`);
2231
+ return errors;
2232
+ }
2233
+ //#endregion
2234
+ //#region src/host/write-preset.ts
2235
+ /** 模块完整定义校验、运行切片恢复与隔离导入候选。普通重建不交换用户目录。 */
2236
+ const LEGACY_ARTIFACTS = [
2237
+ "configs",
2238
+ "rules.yml",
2239
+ "agent.cordis.yml",
2240
+ "custom-tools",
2241
+ "subagent-tools",
2242
+ "triggers.yml"
2243
+ ];
2244
+ function validateModule(directory, warn) {
2245
+ assertModuleTree(directory);
2246
+ const snapshot = loadModuleDefinition(directory);
2247
+ const spec = {
2248
+ ...snapshot.source,
2249
+ id: snapshot.source.id ?? basename(snapshot.dir),
2250
+ rules: snapshot.rules,
2251
+ configOrder: snapshot.configOrder,
2252
+ variables: snapshot.variables,
2253
+ variablesEnabled: snapshot.variablesEnabled
2254
+ };
2255
+ assertCompositionArray(renderComposition(spec, {}, directory), spec);
2256
+ if (spec.customTools !== void 0 && !Array.isArray(spec.customTools)) throw new TypeError("customTools must be an array");
2257
+ const toolErrors = validateCustomTools(spec.customTools ?? []);
2258
+ if (toolErrors.length > 0) throw new Error(`invalid customTools: ${toolErrors.join("; ")}`);
2259
+ if (spec.subagentToolPolicy !== void 0 && spec.subagentToolPolicy !== null) {
2260
+ const policyErrors = validateSubagentToolPolicy(spec.subagentToolPolicy);
2261
+ if (policyErrors.length > 0) throw new Error(`invalid subagentToolPolicy: ${policyErrors.join("; ")}`);
2262
+ }
2263
+ if (Array.isArray(spec.meta?.stWarnings)) {
2264
+ for (const warning of spec.meta.stWarnings) if (typeof warning === "string") warn?.(`prompt-tool: ST 导入兼容提示:${warning}`);
2265
+ }
2266
+ }
2267
+ function removeLegacyArtifacts(directory) {
2268
+ for (const name of LEGACY_ARTIFACTS) rmSync(join(directory, name), {
2269
+ recursive: true,
2270
+ force: true
2271
+ });
2272
+ }
2273
+ /** 只有明确传入的非空内容才更新旧内容资产;空值保留原文件。 */
2274
+ function writeContentAssets(directory, prompt, agentsText) {
2275
+ for (const [file, content] of [["preset.md", prompt], ["agents.md", agentsText]]) {
2276
+ if (typeof content !== "string" || content.trim().length === 0) continue;
2277
+ const path = join(directory, file);
2278
+ if (!existsSync(path) || readFileSync(path, "utf8") !== content) atomicWriteTextFile(path, content);
2279
+ }
2280
+ }
2281
+ /** 按模块身份原地恢复切片;用户资产、正文与完整定义均不重写。 */
2282
+ function ensureModuleReady(id, options) {
2283
+ const root = options.modulesRoot ?? options.moduleDir ?? MODULES_DIR;
2284
+ const directory = assertModuleDirectory(root, id);
2285
+ validateModule(directory, options.warn);
2286
+ if (enabledModuleIds(root).includes(id)) appendModuleConfigOrder(root, id);
2287
+ ensureModuleSlices(directory);
2288
+ withModuleLock(root, id, () => {
2289
+ assertModuleDirectory(root, id);
2290
+ removeLegacyArtifacts(directory);
2291
+ });
2292
+ return directory;
2293
+ }
2294
+ /** 已有同一模块只恢复切片;导入/复制先完整生成合法身份候选,再交换目标。 */
2295
+ function writeModule(prompt, options) {
2296
+ const root = options.modulesRoot.trim().length > 0 ? options.modulesRoot : MODULES_DIR;
2297
+ assertModuleId(options.moduleId);
2298
+ const targetId = options.targetModuleId ?? options.moduleId;
2299
+ assertModuleId(targetId);
2300
+ const source = options.sourceDir ?? resolveModuleDir(options.moduleId, root);
2301
+ validateModule(source, options.warn);
2302
+ const target = assertModuleDirectory(root, targetId, true);
2303
+ const targetVersion = options.stageOnly ? void 0 : directoryVersionOf(target);
2304
+ if (!options.stageOnly && resolve(source) === resolve(target)) {
2305
+ ensureModuleSlices(target);
2306
+ withModuleLock(root, targetId, () => {
2307
+ assertModuleDirectory(root, targetId);
2308
+ writeContentAssets(target, prompt, options.agentsInstructionText);
2309
+ removeLegacyArtifacts(target);
2310
+ });
2311
+ return target;
2312
+ }
2313
+ mkdirSync(root, { recursive: true });
2314
+ const stageRoot = mkdtempSync(join(root, `.${targetId}.stage-`));
2315
+ const candidate = join(stageRoot, targetId);
2316
+ try {
2317
+ mkdirSync(candidate);
2318
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
2319
+ if (LEGACY_ARTIFACTS.includes(entry.name) || entry.name === "rules") continue;
2320
+ cpSync(join(source, entry.name), join(candidate, entry.name), {
2321
+ recursive: entry.isDirectory(),
2322
+ force: true
2323
+ });
2324
+ }
2325
+ const file = join(candidate, MODULE_DEFINITION_FILE);
2326
+ const text = readFileSync(file, "utf8");
2327
+ const doc = parseDocument(text, { logLevel: "silent" });
2328
+ if (doc.get("id") !== targetId) {
2329
+ doc.set("id", targetId);
2330
+ validateModuleDefinitionText(candidate, doc.toString());
2331
+ atomicWriteTextFile(file, doc.toString());
2332
+ }
2333
+ writeContentAssets(candidate, prompt, options.agentsInstructionText);
2334
+ validateModule(candidate);
2335
+ ensureModuleSlices(candidate);
2336
+ if (options.stageOnly) return candidate;
2337
+ withModuleLock(root, targetId, () => {
2338
+ assertModuleDirectory(root, targetId, true);
2339
+ if (directoryVersionOf(target) !== targetVersion) throw new ModuleRulesError("模块安装期间目标已变化,未覆盖", 409, "rules-conflict");
2340
+ const backup = join(stageRoot, "previous");
2341
+ const hadTarget = existsSync(target);
2342
+ if (hadTarget) renameSync(target, backup);
2343
+ try {
2344
+ renameSync(candidate, target);
2345
+ } catch (error) {
2346
+ if (hadTarget) try {
2347
+ renameSync(backup, target);
2348
+ } catch (restoreError) {
2349
+ throw new Error(`模块安装失败且恢复失败;原目录保留在 ${backup}: ${String(restoreError)}`, { cause: error });
2350
+ }
2351
+ throw error;
2352
+ }
2353
+ });
2354
+ rmSync(stageRoot, {
2355
+ recursive: true,
2356
+ force: true
2357
+ });
2358
+ return target;
2359
+ } catch (error) {
2360
+ if (!existsSync(join(stageRoot, "previous"))) rmSync(stageRoot, {
2361
+ recursive: true,
2362
+ force: true
2363
+ });
2364
+ throw error;
2365
+ }
2366
+ }
2367
+ /** @deprecated 使用 writeModule;旧字段仅在此边界适配。 */
2368
+ function writePreset(prompt, options) {
2369
+ if ((options.promptConfigs?.length ?? 0) > 0) throw new Error("旧 promptConfigs 物化覆盖已退出,请先离线迁移为 rules");
2370
+ const moduleId = options.presetTemplate?.trim() || "ponytail";
2371
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(moduleId)) throw new Error(`invalid presetTemplate ${JSON.stringify(moduleId)}`);
2372
+ return writeModule(prompt, {
2373
+ modulesRoot: options.moduleDir,
2374
+ moduleId,
2375
+ targetModuleId: options.outputId,
2376
+ sourceDir: options.sourceDir,
2377
+ stageOnly: options.materializeOnly,
2378
+ agentsInstructionText: options.agentsInstructionText,
2379
+ warn: options.warn
2380
+ });
2381
+ }
2382
+ //#endregion
2383
+ //#region src/host/module-package.ts
2384
+ /** 模块交换:有界资源、完整候选与可恢复提交,Web 和 CLI 共用。 */
2385
+ const MANIFEST = "prompt-tool-package.json";
2386
+ const PACKAGE_VERSION = 1;
2387
+ const STORAGE_VERSION = 6;
2388
+ /**
2389
+ * 包内定义文件名:写入用新名,**读取接受历史名**。
2390
+ *
2391
+ * 包是可分享的外部产物,用户手里可能有旧版导出的包(清单 `definition: preset.yml`);
2392
+ * 导入是信任边界,拒绝它等于把用户已有的包判为损坏。
2393
+ */
2394
+ const DEFINITION_NAMES = [MODULE_DEFINITION_FILE, "preset.yml"];
2395
+ const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
2396
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2397
+ var AssetTransferError = class extends Error {
2398
+ code;
2399
+ constructor(code, message) {
2400
+ super(message);
2401
+ this.code = code;
2402
+ }
2403
+ };
2404
+ function stale() {
2405
+ throw new AssetTransferError("module-preview-stale", "预览已过期,请重新预览后再导入");
2406
+ }
2407
+ function assertTransferId(id) {
2408
+ assertModuleId(id);
2409
+ if (id.length > 128) throw new Error("预设 id 过长");
2410
+ }
2411
+ function ownedFile(path) {
2412
+ const root = path.split("/")[0];
2413
+ if (root.startsWith(".") || [
2414
+ "configs",
2415
+ "custom-tools",
2416
+ "subagent-tools",
2417
+ "skills"
2418
+ ].includes(root)) return false;
2419
+ if (path === "variables.yml" || path === MANIFEST) return false;
2420
+ return path === "agents.md" || !/^(?:AGENTS|CLAUDE)(?:\.local)?\.md$/i.test(basename(path));
2421
+ }
2422
+ /** 先筛选再读取:派生切片和不分享的私有资产不参与读取、配额或导出版本。 */
2423
+ function shareableFile(path) {
2424
+ const root = path.split("/")[0].toLowerCase();
2425
+ return ![
2426
+ "rules",
2427
+ "rules.yml",
2428
+ "agent.cordis.yml",
2429
+ "triggers.yml",
2430
+ "memory.md"
2431
+ ].includes(root) && ownedFile(path);
2432
+ }
2433
+ function packageTemplate(files, missing) {
2434
+ const entries = new Map(files.map((file) => [file.path, file.bytes]));
2435
+ return (file) => {
2436
+ if (file === void 0 || file === "") return void 0;
2437
+ if (typeof file !== "string") throw new Error("templateFile 必须是字符串");
2438
+ const path = posix.normalize(file);
2439
+ if (file.includes("\\") || file.includes(":") || posix.isAbsolute(path) || path === ".." || path.startsWith("../") || path === "rules" || path.startsWith("rules/")) throw new Error("非法或外部模板资源:" + file);
2440
+ safeAssetPath(path);
2441
+ const bytes = entries.get(path);
2442
+ if (bytes === void 0) {
2443
+ if (missing) {
2444
+ missing.add("缺失资源:" + path);
2445
+ return;
2446
+ }
2447
+ throw new Error("缺失模板资源:" + path);
2448
+ }
2449
+ const raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2450
+ if (/\.json$/i.test(path)) {
2451
+ const value = JSON.parse(raw);
2452
+ return typeof value === "string" ? { text: value } : value;
2453
+ }
2454
+ if (/\.ya?ml$/i.test(path)) {
2455
+ const doc = parseDocument(raw, { logLevel: "silent" });
2456
+ if (doc.errors.length > 0) throw new Error("模板 YAML 无效:" + path);
2457
+ const value = doc.toJS();
2458
+ return typeof value === "string" ? { text: value } : value;
2459
+ }
2460
+ return { text: raw };
2461
+ };
2462
+ }
2463
+ function validatePackageDefinition(directory, text, files, missing) {
2464
+ const snapshot = validateModuleDefinitionText(directory, text, { loadTemplate: packageTemplate(files, missing) });
2465
+ const spec = snapshot.source;
2466
+ readModuleLayerSettings(spec);
2467
+ if (spec.customTools !== void 0) {
2468
+ if (!Array.isArray(spec.customTools)) throw new Error("customTools 必须是数组");
2469
+ const errors = validateCustomTools(spec.customTools);
2470
+ if (errors.length > 0) throw new Error(errors.join("\n"));
2471
+ }
2472
+ if (spec.subagentToolPolicy !== void 0 && spec.subagentToolPolicy !== null) {
2473
+ const errors = validateSubagentToolPolicy(spec.subagentToolPolicy);
2474
+ if (errors.length > 0) throw new Error(errors.join("\n"));
2475
+ }
2476
+ return snapshot;
2477
+ }
2478
+ async function expandModuleSource(input) {
2479
+ let files = input;
2480
+ if (files.length === 1 && /\.zip$/i.test(files[0].path)) files = await unpackZip(decodeAssetFile(files[0]));
2481
+ files = normalizeAssetFiles(files);
2482
+ const first = files[0]?.path.split("/")[0];
2483
+ let sourceFolder;
2484
+ if (first !== void 0 && files.every((file) => file.path.startsWith(first + "/"))) {
2485
+ sourceFolder = first;
2486
+ files = files.map((file) => ({
2487
+ ...file,
2488
+ path: file.path.slice(first.length + 1)
2489
+ }));
2490
+ }
2491
+ const seen = /* @__PURE__ */ new Set();
2492
+ let size = 0;
2493
+ for (const file of files) {
2494
+ safeAssetPath(file.path);
2495
+ const key = file.path.normalize("NFC").toLowerCase();
2496
+ if (seen.has(key)) throw new Error(`重复资源路径:${file.path}`);
2497
+ seen.add(key);
2498
+ size += decodeAssetFile(file).length;
2499
+ if (size > 67108864 || files.length > 2048) throw new Error("资源包超过大小或数量上限");
2500
+ if (file.path !== MANIFEST && !ownedFile(file.path)) throw new Error(`资源不属于预设交换范围:${file.path}`);
2501
+ }
2502
+ const manifest = files.find((file) => file.path === MANIFEST);
2503
+ if (manifest !== void 0) {
2504
+ const value = JSON.parse(decodeAssetFile(manifest).toString("utf8"));
2505
+ if (!isRecord(value) || value.version !== PACKAGE_VERSION || typeof value.definition !== "string" || !DEFINITION_NAMES.includes(value.definition) || !isRecord(value.files)) throw new Error("不支持或损坏的预设包清单");
2506
+ if (isRecord(value.requires) && typeof value.requires.renderVersion === "number" && value.requires.renderVersion > STORAGE_VERSION) throw new Error("模块包需要较新的 Prompt Tool");
2507
+ const entries = files.filter((file) => file !== manifest);
2508
+ if (Object.keys(value.files).length !== entries.length) throw new Error("包清单与文件集合不一致");
2509
+ for (const file of entries) if (value.files[file.path] !== sha(decodeAssetFile(file))) throw new Error(`包文件摘要不符:${file.path}`);
2510
+ files = entries;
2511
+ }
2512
+ const yaml = files.filter((file) => !file.path.includes("/") && /\.ya?ml$/i.test(file.path) && file.path !== "agent.cordis.yml");
2513
+ const definition = yaml.find((file) => /^(?:module|preset)\.ya?ml$/i.test(file.path)) ?? (yaml.length === 1 ? yaml[0] : void 0);
2514
+ if (definition !== void 0) {
2515
+ const doc = parseDocument(decodeAssetFile(definition).toString("utf8"), { logLevel: "silent" });
2516
+ const value = doc.errors.length === 0 ? doc.toJS({ maxAliasCount: 100 }) : void 0;
2517
+ if (isRecord(value) && value.id === void 0 && !("prompts" in value) && !("spec" in value) && ([
2518
+ "modules",
2519
+ "composition",
2520
+ "rules",
2521
+ "params",
2522
+ "content"
2523
+ ].some((key) => key in value) || typeof value.name === "string" && files.some((file) => file.path === "agent.cordis.yml"))) {
2524
+ doc.set("id", sourceFolder ?? "imported-preset");
2525
+ definition.content = doc.toString();
2526
+ definition.encoding = "utf8";
2527
+ }
2528
+ }
2529
+ return files;
2530
+ }
2531
+ function targetId(root, wanted, request) {
2532
+ assertTransferId(wanted);
2533
+ if (request.targetId !== void 0 || request.overwrite) return wanted;
2534
+ let id = wanted;
2535
+ for (let n = 1; existsSync(join(root, id)); n++) id = `${wanted}-copy${n === 1 ? "" : "-" + n}`;
2536
+ return id;
2537
+ }
2538
+ function moduleImportPreview(root, files, request) {
2539
+ const prepared = prepareImport(files, "preset", {
2540
+ ...request,
2541
+ targetId: void 0,
2542
+ targetName: void 0
2543
+ });
2544
+ if (prepared.state !== "ready") return prepared;
2545
+ validatePackageDefinition(join(root, prepared.spec.id), prepared.yaml, prepared.files.map((file) => ({
2546
+ path: file.path,
2547
+ bytes: decodeAssetFile(file)
2548
+ })));
2549
+ const id = targetId(root, request.targetId ?? prepared.spec.id, request);
2550
+ const version = directoryVersionOf(assertModuleDirectory(root, id, true));
2551
+ const sourceDigest = assetSourceDigest(files);
2552
+ return {
2553
+ prepared,
2554
+ sourceDigest,
2555
+ previewRevision: computePreviewRevision({
2556
+ files: [{
2557
+ path: "source",
2558
+ content: sourceDigest
2559
+ }, {
2560
+ path: "choices",
2561
+ content: JSON.stringify([
2562
+ id,
2563
+ request.targetName ?? "",
2564
+ request.overwrite === true,
2565
+ request.sourceKind ?? null
2566
+ ])
2567
+ }],
2568
+ ...request.promptOrderCharacterId === void 0 ? {} : { orderCharacterId: request.promptOrderCharacterId },
2569
+ converter: "asset-import/1",
2570
+ target: {
2571
+ kind: "preset-package",
2572
+ targetId: id,
2573
+ targetVersion: version,
2574
+ ownerPreset: root
2575
+ }
2576
+ }),
2577
+ summary: {
2578
+ sourceName: prepared.sourceName,
2579
+ kind: prepared.kind,
2580
+ targetId: id,
2581
+ targetName: request.targetName ?? prepared.spec.name,
2582
+ exists: version !== null,
2583
+ files: files.map((file) => ({
2584
+ path: file.path,
2585
+ bytes: decodeAssetFile(file).length
2586
+ })),
2587
+ configCount: prepared.spec.rules?.length ?? 0,
2588
+ warnings: []
2589
+ }
2590
+ };
2591
+ }
2592
+ /** 同一进程的同目标安装串行;磁盘版本复检保护其他写入者。 */
2593
+ const installs = /* @__PURE__ */ new Map();
2594
+ async function installModulePackage(root, files, request, options = {}) {
2595
+ root = canonicalModulesRoot(root, true);
2596
+ const initial = moduleImportPreview(root, files, request);
2597
+ if (!("prepared" in initial)) throw new Error("请先完成来源和顺序组选择");
2598
+ const target = join(root, initial.summary.targetId);
2599
+ const run = (installs.get(target) ?? Promise.resolve()).catch(() => {}).then(() => {
2600
+ const preview = moduleImportPreview(root, files, request);
2601
+ if (!("prepared" in preview)) throw new Error("来源选择已失效");
2602
+ if (!request.expectedPreviewRevision || request.expectedPreviewRevision !== preview.previewRevision || request.expectedSourceDigest !== preview.sourceDigest) stale();
2603
+ if (preview.summary.exists && request.overwrite !== true) throw new Error("目标已存在,请明确选择更新或另存");
2604
+ mkdirSync(root, { recursive: true });
2605
+ const stage = mkdtempSync(join(root, ".import-"));
2606
+ const source = join(stage, preview.summary.targetId);
2607
+ mkdirSync(source);
2608
+ let generated;
2609
+ let generatedRoot;
2610
+ let moved = false;
2611
+ const backup = join(stage, "previous");
2612
+ try {
2613
+ if ((options.preserveAssets || preview.prepared.kind === "st-character" || preview.prepared.kind === "native-character") && preview.summary.exists) for (const entry of readdirSync(target)) {
2614
+ if ([
2615
+ "module.yml",
2616
+ "rules",
2617
+ "configs",
2618
+ "rules.yml",
2619
+ "agent.cordis.yml",
2620
+ "custom-tools",
2621
+ "subagent-tools"
2622
+ ].includes(entry)) continue;
2623
+ const path = join(target, entry);
2624
+ assertModuleTree(path);
2625
+ cpSync(path, join(source, entry), { recursive: true });
2626
+ }
2627
+ for (const file of preview.prepared.files.filter((file) => file.path !== MANIFEST && !DEFINITION_NAMES.includes(file.path))) {
2628
+ const path = join(source, safeAssetPath(file.path));
2629
+ mkdirSync(dirname(path), { recursive: true });
2630
+ if (file.path === "memory.md" && existsSync(path)) continue;
2631
+ writeFileSync(path, decodeAssetFile(file));
2632
+ }
2633
+ const doc = parseDocument(preview.prepared.yaml);
2634
+ setModuleDefinitionId(doc, preview.summary.targetId);
2635
+ doc.set("name", preview.summary.targetName);
2636
+ writeFileSync(join(source, MODULE_DEFINITION_FILE), doc.toString(), "utf8");
2637
+ generated = writeModule(existsSync(join(source, "preset.md")) ? readFileSync(join(source, "preset.md"), "utf8") : "", {
2638
+ modulesRoot: root,
2639
+ moduleId: preview.summary.targetId,
2640
+ targetModuleId: preview.summary.targetId,
2641
+ sourceDir: source,
2642
+ stageOnly: true
2643
+ });
2644
+ generatedRoot = dirname(generated);
2645
+ withModuleLock(root, preview.summary.targetId, () => {
2646
+ const now = moduleImportPreview(root, files, request);
2647
+ if (!("prepared" in now) || now.previewRevision !== preview.previewRevision) stale();
2648
+ if (existsSync(target)) {
2649
+ renameSync(target, backup);
2650
+ moved = true;
2651
+ }
2652
+ try {
2653
+ renameSync(generated, target);
2654
+ generated = void 0;
2655
+ } catch (error) {
2656
+ if (moved) try {
2657
+ renameSync(backup, target);
2658
+ moved = false;
2659
+ } catch {
2660
+ throw new AssetTransferError("module-recovery-required", `安装失败且恢复未完成,原目录保留在 ${backup}`);
2661
+ }
2662
+ throw error;
2663
+ }
2664
+ });
2665
+ invalidateModuleSpec(target);
2666
+ try {
2667
+ rmSync(stage, {
2668
+ recursive: true,
2669
+ force: true
2670
+ });
2671
+ moved = false;
2672
+ } catch {
2673
+ return {
2674
+ id: preview.summary.targetId,
2675
+ backupPath: stage
2676
+ };
2677
+ }
2678
+ return { id: preview.summary.targetId };
2679
+ } finally {
2680
+ if (generated !== void 0) rmSync(generated, {
2681
+ recursive: true,
2682
+ force: true
2683
+ });
2684
+ if (generatedRoot !== void 0) rmSync(generatedRoot, {
2685
+ recursive: true,
2686
+ force: true
2687
+ });
2688
+ if (!moved) rmSync(stage, {
2689
+ recursive: true,
2690
+ force: true
2691
+ });
2692
+ }
2693
+ });
2694
+ installs.set(target, run);
2695
+ try {
2696
+ return await run;
2697
+ } finally {
2698
+ if (installs.get(target) === run) installs.delete(target);
2699
+ }
2700
+ }
2701
+ function collectFiles(dir) {
2702
+ const files = [];
2703
+ const seen = /* @__PURE__ */ new Set();
2704
+ let total = 0;
2705
+ const walk = (current, prefix) => {
2706
+ for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
2707
+ if (!shareableFile(prefix + entry.name)) continue;
2708
+ const path = safeAssetPath(prefix + entry.name);
2709
+ const key = path.normalize("NFC").toLowerCase();
2710
+ if (seen.has(key)) throw new Error(`导出路径大小写或规范化冲突:${path}`);
2711
+ seen.add(key);
2712
+ if (entry.isSymbolicLink()) throw new Error(`不能导出链接:${path}`);
2713
+ const absolute = join(current, entry.name);
2714
+ if (entry.isDirectory()) {
2715
+ walk(absolute, path + "/");
2716
+ continue;
2717
+ }
2718
+ const stat = lstatSync(absolute);
2719
+ if (!stat.isFile()) throw new Error(`不能导出特殊文件:${path}`);
2720
+ total += stat.size;
2721
+ if (total > 67108864 || files.length >= 2048) throw new Error("导出超过大小或数量上限");
2722
+ const bytes = readFileSync(absolute);
2723
+ if (bytes.length !== stat.size) throw new Error("导出期间文件发生变化");
2724
+ files.push({
2725
+ path,
2726
+ bytes
2727
+ });
2728
+ }
2729
+ };
2730
+ walk(dir, "");
2731
+ return files;
2732
+ }
2733
+ function dependencyErrors(files, spec) {
2734
+ const paths = new Set(files.map((file) => file.path));
2735
+ const errors = /* @__PURE__ */ new Set();
2736
+ const check = (ref, from) => {
2737
+ const rel = posix.normalize(posix.join(posix.dirname(from), ref));
2738
+ if (rel.startsWith("../") || posix.isAbsolute(rel) || ref.includes(":")) errors.add(`外部资源:${ref}`);
2739
+ else if (!paths.has(rel)) errors.add(`缺失资源:${rel}`);
2740
+ };
2741
+ for (const { config } of ruleInjections(spec.rules)) if (typeof config.templateFile === "string") check(config.templateFile, MODULE_DEFINITION_FILE);
2742
+ if (!Array.isArray(spec.modules) && typeof spec.composition === "string") {
2743
+ let raw = spec.composition;
2744
+ if (raw.startsWith("./")) {
2745
+ check(raw, MODULE_DEFINITION_FILE);
2746
+ raw = files.find((file) => file.path === posix.normalize(raw))?.bytes.toString("utf8") ?? "";
2747
+ }
2748
+ if (raw.includes("\n")) {
2749
+ const doc = parseDocument(raw, { logLevel: "silent" });
2750
+ if (doc.errors.length > 0) errors.add("模块组合 YAML 无效");
2751
+ else {
2752
+ const visit = (value) => {
2753
+ if (Array.isArray(value)) {
2754
+ value.forEach(visit);
2755
+ return;
2756
+ }
2757
+ if (!isRecord(value)) return;
2758
+ if (typeof value.name === "string" && value.name.startsWith(".")) check(value.name, MODULE_DEFINITION_FILE);
2759
+ Object.values(value).forEach((child) => {
2760
+ if (typeof child === "object") visit(child);
2761
+ });
2762
+ };
2763
+ visit(doc.toJS());
2764
+ }
2765
+ }
2766
+ }
2767
+ for (const file of files) if (/\.[cm]?js$/i.test(file.path)) {
2768
+ const code = file.bytes.toString("utf8");
2769
+ for (const match of code.matchAll(/(?:from\s*|import\s*\(|require\s*\()\s*['"](\.[^'"]+)['"]/g)) check(match[1], file.path);
2770
+ }
2771
+ return [...errors];
2772
+ }
2773
+ function sharingSnapshot(dir, id, root, choices) {
2774
+ const files = collectFiles(dir);
2775
+ const definition = files.find((file) => file.path === MODULE_DEFINITION_FILE);
2776
+ if (!definition) throw new Error("缺少模块定义");
2777
+ const originalText = new TextDecoder("utf-8", { fatal: true }).decode(definition.bytes);
2778
+ const missing = /* @__PURE__ */ new Set();
2779
+ const original = validatePackageDefinition(dir, originalText, files, missing).doc;
2780
+ if (original.get("id") === void 0) original.set("id", id);
2781
+ const source = files.map((file) => [file.path, sha(file.bytes)]);
2782
+ const projection = projectCharacterMemories(original, choices, root);
2783
+ definition.bytes = Buffer.from(projection.doc.toString());
2784
+ const spec = validatePackageDefinition(dir, definition.bytes.toString("utf8"), files, missing).source;
2785
+ const revision = sha(JSON.stringify([
2786
+ source,
2787
+ definition.bytes.toString("utf8"),
2788
+ projection.memoryConflicts,
2789
+ projection.excludedMemoryCount
2790
+ ]));
2791
+ return {
2792
+ files,
2793
+ definition,
2794
+ spec,
2795
+ projection,
2796
+ missing: [...missing],
2797
+ revision
2798
+ };
2799
+ }
2800
+ async function exportModulePackage(root, request) {
2801
+ assertTransferId(request.id);
2802
+ const resolved = resolveModuleDir(request.id, root);
2803
+ const dir = assertModuleDirectory(dirname(resolved), request.id);
2804
+ if (!modulePathExists(dir)) throw new Error("模块不存在");
2805
+ const choices = request.memoryChoices ?? {};
2806
+ const { files, definition, spec, projection, missing, revision: before } = sharingSnapshot(dir, request.id, root, choices);
2807
+ const blockers = [.../* @__PURE__ */ new Set([...missing, ...dependencyErrors(files, spec)])];
2808
+ if (projection.memoryConflicts.length > 0) blockers.push("请明确选择来源不明的记忆条目是否分享");
2809
+ const mode = request.mode ?? "definition";
2810
+ const revision = sha(JSON.stringify([
2811
+ before,
2812
+ mode,
2813
+ request.memoryChoices ?? {}
2814
+ ]));
2815
+ const result = {
2816
+ id: request.id,
2817
+ name: spec.name || request.id,
2818
+ content: "",
2819
+ revision,
2820
+ filename: `${request.id}.${mode === "zip" ? "zip" : MODULE_DEFINITION_FILE}`,
2821
+ files: files.map((file) => ({
2822
+ path: file.path,
2823
+ bytes: file.bytes.length
2824
+ })),
2825
+ warnings: ["不包含工作区指令、角色记忆文件、技能库;需要兼容的 Prompt Tool 与宿主。"],
2826
+ blockers: mode === "zip" ? blockers : projection.memoryConflicts.length ? ["请先处理记忆条目"] : [],
2827
+ memoryConflicts: projection.memoryConflicts,
2828
+ excludedMemoryCount: projection.excludedMemoryCount
2829
+ };
2830
+ if (mode === "definition") result.warnings.push(...blockers);
2831
+ if (mode === "definition" && files.length > 1) result.warnings.push(`仅导出定义,不包含 ${files.length - 1} 个附属文件。`);
2832
+ const verify = () => {
2833
+ if (sharingSnapshot(dir, request.id, root, choices).revision !== before) stale();
2834
+ };
2835
+ verify();
2836
+ if (request.preview) return result;
2837
+ if (request.expectedRevision !== void 0 && request.expectedRevision !== revision) stale();
2838
+ if (result.blockers.length) throw new Error(result.blockers.join("\n"));
2839
+ if (mode === "definition") return {
2840
+ ...result,
2841
+ encoding: "utf8",
2842
+ content: definition.bytes.toString("utf8")
2843
+ };
2844
+ const plugin = JSON.parse(readFileSync(join(dirname(packageEngineDir()), "package.json"), "utf8"));
2845
+ const manifest = Buffer.from(JSON.stringify({
2846
+ version: PACKAGE_VERSION,
2847
+ definition: MODULE_DEFINITION_FILE,
2848
+ requires: {
2849
+ promptTool: plugin.version,
2850
+ renderVersion: STORAGE_VERSION
2851
+ },
2852
+ files: Object.fromEntries(files.map((file) => [file.path, sha(file.bytes)]))
2853
+ }, null, 2));
2854
+ if (files.length + 1 > 2048) throw new Error("包含清单后的模块包超过文件数量上限");
2855
+ if (files.reduce((sum, file) => sum + file.bytes.length, manifest.length) > 67108864) throw new Error("包含清单后的预设包超过 64 MiB");
2856
+ const archive = await packZip([...files, {
2857
+ path: MANIFEST,
2858
+ bytes: manifest
2859
+ }].map((file) => ({
2860
+ ...file,
2861
+ path: `${request.id}/${file.path}`
2862
+ })));
2863
+ if (archive.length > 67108864) throw new Error("ZIP 超过 64 MiB,无法通过导入上限");
2864
+ verify();
2865
+ return {
2866
+ ...result,
2867
+ encoding: "base64",
2868
+ content: Buffer.from(archive).toString("base64")
2869
+ };
2870
+ }
2871
+ //#endregion
2872
+ export { MAX_ASSET_BYTES as A, mergeStModulesWithReport as C, deleteWorldBookEntry as D, buildWorldBookEntry as E, DEFAULT_MODULE_ID as M, listWorldBookEntries as O, mergeStModules as S, stModuleId as T, readModuleMemory as _, installModulePackage as a, convertStToModule as b, writeModule as c, validateCustomTools as d, safeAssetPath as f, mergeModuleIntoModule as g, importCharacterCardFile as h, exportModulePackage as i, ruleInjections as j, upsertWorldBookEntry as k, writePreset as l, importCharacterCard as m, assertTransferId as n, moduleImportPreview as o, appendModuleMemory as p, expandModuleSource as r, ensureModuleReady as s, AssetTransferError as t, compileCustomTool as u, removeMergedModule as v, processStText as w, convertStToModuleWithReport as x, prepareImport as y };