narraeon 0.7.9 → 0.8.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 (45) hide show
  1. package/dist/node/protocol/modelConnections.js +15 -0
  2. package/dist/node/protocol/modelUsage.js +4 -0
  3. package/dist/node/protocol/sillyTavernContent.js +1 -0
  4. package/dist/node/protocol/v1.js +75 -0
  5. package/dist/node/runtime/V1Runtime.js +71 -1
  6. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
  7. package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
  8. package/dist/node/runtime/authoring/ModelToolConversation.js +47 -22
  9. package/dist/node/runtime/content/ContentWorkspace.js +3 -0
  10. package/dist/node/runtime/content/SillyTavernContent.js +796 -0
  11. package/dist/node/runtime/model/AiFailureLog.js +8 -1
  12. package/dist/node/runtime/model/FileNativeModelAdapters.js +610 -18
  13. package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
  14. package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
  15. package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
  16. package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
  17. package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
  18. package/dist/node/runtime/model/ProviderStream.js +4 -2
  19. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
  20. package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
  21. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +150 -10
  22. package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
  23. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +33 -9
  24. package/dist/node/runtime/play/PlayCallChain.js +76 -4
  25. package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
  26. package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
  27. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +112 -8
  28. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +100 -41
  29. package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
  30. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
  31. package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
  32. package/dist/node/shared/application-preset-files.js +29 -0
  33. package/dist/node/shared/default-preset-host.js +50 -38
  34. package/dist/node/shared/first-party-action-choices.js +7 -11
  35. package/dist/node/shared/first-party-generic-panels.js +7 -11
  36. package/dist/node/shared/first-party-player-view.js +7 -11
  37. package/dist/node/shared/ordered-author-prompts.js +6 -4
  38. package/dist/node/shared/ordered-play-prompts.js +4 -2
  39. package/dist/node/shared/prompt-arrangement.js +205 -0
  40. package/dist/node/shared/sillytavern-preset.js +454 -0
  41. package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
  42. package/dist/web/assets/index-C_Qta8UL.js +1432 -0
  43. package/dist/web/index.html +2 -2
  44. package/package.json +1 -1
  45. package/dist/web/assets/index-B2kbaOGe.js +0 -1408
@@ -0,0 +1,796 @@
1
+ import { inflateSync } from "node:zlib";
2
+ import { parseDocument, stringify } from "yaml";
3
+ const limit = 8 * 1024 * 1024;
4
+ const decoder = new TextDecoder("utf-8", { fatal: true });
5
+ function fail(code, path) {
6
+ throw new Error(`${path}: ${code}`);
7
+ }
8
+ function record(value, path) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value))
10
+ fail("expected_object", path);
11
+ return value;
12
+ }
13
+ function pngJson(bytes) {
14
+ if (!bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
15
+ fail("invalid_png", "$ ");
16
+ let offset = 8, ended = false, sawHeader = false, sawImage = false, imageEnded = false;
17
+ let width = 0, height = 0, depth = 0, color = 0, interlace = 0;
18
+ const imageChunks = [];
19
+ let sawPalette = false;
20
+ const chunks = new Map();
21
+ while (offset < bytes.length) {
22
+ if (offset + 12 > bytes.length)
23
+ fail("invalid_png_chunk", "$ ");
24
+ const length = bytes.readUInt32BE(offset), end = offset + 12 + length;
25
+ if (end > bytes.length || length > limit)
26
+ fail("invalid_png_chunk", "$ ");
27
+ const body = bytes.subarray(offset + 4, offset + 8 + length);
28
+ if (crc32(body) !== bytes.readUInt32BE(offset + 8 + length))
29
+ fail("invalid_png_crc", "$ ");
30
+ const type = bytes.toString("latin1", offset + 4, offset + 8);
31
+ if (!/^[A-Za-z]{4}$/u.test(type) ||
32
+ (/^[A-Z]/u.test(type) && !["IHDR", "PLTE", "IDAT", "IEND"].includes(type)))
33
+ fail("invalid_png_chunk", "$");
34
+ if (!sawHeader) {
35
+ if (type !== "IHDR" || length !== 13)
36
+ fail("invalid_png_header", "$");
37
+ width = bytes.readUInt32BE(offset + 8);
38
+ height = bytes.readUInt32BE(offset + 12);
39
+ depth = bytes[offset + 16];
40
+ color = bytes[offset + 17];
41
+ interlace = bytes[offset + 20];
42
+ const allowedDepths = {
43
+ 0: [1, 2, 4, 8, 16],
44
+ 2: [8, 16],
45
+ 3: [1, 2, 4, 8],
46
+ 4: [8, 16],
47
+ 6: [8, 16],
48
+ };
49
+ if (!allowedDepths[color]?.includes(depth) ||
50
+ bytes[offset + 18] !== 0 ||
51
+ bytes[offset + 19] !== 0 ||
52
+ ![0, 1].includes(interlace))
53
+ fail("invalid_png_header", "$");
54
+ if (!width ||
55
+ !height ||
56
+ width > 16384 ||
57
+ height > 16384 ||
58
+ width * height > 64000000)
59
+ fail("invalid_png_dimensions", "$");
60
+ sawHeader = true;
61
+ }
62
+ else if (type === "IHDR")
63
+ fail("invalid_png_header", "$");
64
+ if (type === "PLTE") {
65
+ if (sawPalette ||
66
+ sawImage ||
67
+ length === 0 ||
68
+ length > 768 ||
69
+ length % 3 !== 0 ||
70
+ color === 0 ||
71
+ color === 4)
72
+ fail("invalid_png_chunk", "$");
73
+ sawPalette = true;
74
+ }
75
+ if (type === "IDAT") {
76
+ if (color === 3 && !sawPalette)
77
+ fail("invalid_png_chunk", "$");
78
+ if (imageEnded)
79
+ fail("invalid_png_chunk", "$");
80
+ sawImage = true;
81
+ imageChunks.push(bytes.subarray(offset + 8, offset + 8 + length));
82
+ }
83
+ else if (sawImage)
84
+ imageEnded = true;
85
+ if (type === "IEND" && (length !== 0 || !sawImage))
86
+ fail("invalid_png", "$");
87
+ if (type === "tEXt") {
88
+ const data = bytes.subarray(offset + 8, offset + 8 + length), sep = data.indexOf(0);
89
+ const key = data.subarray(0, sep).toString("latin1").toLowerCase();
90
+ if ((key === "chara" || key === "ccv3") && sep >= 0) {
91
+ if (chunks.has(key))
92
+ fail("duplicate_metadata", key);
93
+ chunks.set(key, data.subarray(sep + 1));
94
+ }
95
+ }
96
+ offset = end;
97
+ if (type === "IEND") {
98
+ ended = true;
99
+ break;
100
+ }
101
+ }
102
+ if (!ended || offset !== bytes.length)
103
+ fail("invalid_png", "$ ");
104
+ const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
105
+ const passes = interlace === 0
106
+ ? [[0, 0, 1, 1]]
107
+ : [
108
+ [0, 0, 8, 8],
109
+ [4, 0, 8, 8],
110
+ [0, 4, 4, 8],
111
+ [2, 0, 4, 4],
112
+ [0, 2, 2, 4],
113
+ [1, 0, 2, 2],
114
+ [0, 1, 1, 2],
115
+ ];
116
+ const rows = [];
117
+ for (const [x, y, dx, dy] of passes) {
118
+ const w = Math.max(0, Math.ceil((width - x) / dx)), h = Math.max(0, Math.ceil((height - y) / dy));
119
+ if (w && h)
120
+ for (let row = 0; row < h; row++)
121
+ rows.push(1 + Math.ceil((w * depth * channels[color]) / 8));
122
+ }
123
+ const expected = rows.reduce((sum, size) => sum + size, 0);
124
+ if (expected > 64 * 1024 * 1024)
125
+ fail("invalid_png_dimensions", "$");
126
+ let pixels;
127
+ try {
128
+ const compressed = Buffer.concat(imageChunks);
129
+ const result = inflateSync(compressed, {
130
+ maxOutputLength: expected + 1,
131
+ info: true,
132
+ });
133
+ if (result.engine.bytesWritten !== compressed.length)
134
+ fail("invalid_png_image", "$");
135
+ pixels = result.buffer;
136
+ }
137
+ catch {
138
+ return fail("invalid_png_image", "$");
139
+ }
140
+ if (pixels.length !== expected)
141
+ fail("invalid_png_image", "$");
142
+ let cursor = 0;
143
+ for (const size of rows) {
144
+ if (pixels[cursor] > 4)
145
+ fail("invalid_png_image", "$");
146
+ cursor += size;
147
+ }
148
+ const encoded = chunks.get("ccv3") ?? chunks.get("chara");
149
+ if (!encoded)
150
+ fail("missing_card_metadata", "$ ");
151
+ const text = encoded.toString("latin1");
152
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(text))
153
+ fail("invalid_base64", "$ ");
154
+ const decoded = Buffer.from(text, "base64");
155
+ if (decoded.toString("base64") !== text)
156
+ fail("invalid_base64", "$ ");
157
+ return { bytes: decoded, key: chunks.has("ccv3") ? "ccv3" : "chara" };
158
+ }
159
+ function crc32(bytes) {
160
+ let crc = 0xffffffff;
161
+ for (const byte of bytes) {
162
+ crc ^= byte;
163
+ for (let i = 0; i < 8; i++)
164
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
165
+ }
166
+ return (crc ^ 0xffffffff) >>> 0;
167
+ }
168
+ const ruleKeys = new Set([
169
+ "id",
170
+ "uid",
171
+ "key",
172
+ "keys",
173
+ "keysecondary",
174
+ "secondary_keys",
175
+ "enabled",
176
+ "disable",
177
+ "constant",
178
+ "selective",
179
+ "selectiveLogic",
180
+ "insertion_order",
181
+ "order",
182
+ "displayIndex",
183
+ "position",
184
+ "depth",
185
+ "role",
186
+ "sticky",
187
+ "cooldown",
188
+ "delay",
189
+ "probability",
190
+ "useProbability",
191
+ "group",
192
+ "group_weight",
193
+ "groupWeight",
194
+ "groupOverride",
195
+ "exclude_recursion",
196
+ "prevent_recursion",
197
+ "delay_until_recursion",
198
+ "excludeRecursion",
199
+ "preventRecursion",
200
+ "delayUntilRecursion",
201
+ "scanDepth",
202
+ "caseSensitive",
203
+ "matchWholeWords",
204
+ "useGroupScoring",
205
+ "automationId",
206
+ "vectorized",
207
+ "triggers",
208
+ "ignoreBudget",
209
+ "outletName",
210
+ "addMemo",
211
+ "characterFilterNames",
212
+ "characterFilterTags",
213
+ "characterFilterExclude",
214
+ "use_regex",
215
+ "display_index",
216
+ "outlet_name",
217
+ "group_override",
218
+ "scan_depth",
219
+ "case_sensitive",
220
+ "match_whole_words",
221
+ "use_group_scoring",
222
+ "automation_id",
223
+ "match_persona_description",
224
+ "match_character_description",
225
+ "match_character_personality",
226
+ "match_character_depth_prompt",
227
+ "match_scenario",
228
+ "match_creator_notes",
229
+ "ignore_budget",
230
+ "matchPersonaDescription",
231
+ "matchCharacterDescription",
232
+ "matchCharacterPersonality",
233
+ "matchCharacterDepthPrompt",
234
+ "matchScenario",
235
+ "matchCreatorNotes",
236
+ ]);
237
+ const numericRules = new Set([
238
+ "order",
239
+ "insertion_order",
240
+ "displayIndex",
241
+ "display_index",
242
+ "depth",
243
+ "probability",
244
+ "groupWeight",
245
+ "group_weight",
246
+ "scanDepth",
247
+ "scan_depth",
248
+ "sticky",
249
+ "cooldown",
250
+ "delay",
251
+ ]);
252
+ const booleanRules = new Set([
253
+ "enabled",
254
+ "disable",
255
+ "constant",
256
+ "selective",
257
+ "useProbability",
258
+ "groupOverride",
259
+ "group_override",
260
+ "excludeRecursion",
261
+ "exclude_recursion",
262
+ "preventRecursion",
263
+ "prevent_recursion",
264
+ "caseSensitive",
265
+ "case_sensitive",
266
+ "matchWholeWords",
267
+ "match_whole_words",
268
+ "useGroupScoring",
269
+ "use_group_scoring",
270
+ "vectorized",
271
+ "ignoreBudget",
272
+ "ignore_budget",
273
+ "addMemo",
274
+ "use_regex",
275
+ "characterFilterExclude",
276
+ "matchPersonaDescription",
277
+ "matchCharacterDescription",
278
+ "matchCharacterPersonality",
279
+ "matchCharacterDepthPrompt",
280
+ "matchScenario",
281
+ "matchCreatorNotes",
282
+ "match_persona_description",
283
+ "match_character_description",
284
+ "match_character_personality",
285
+ "match_character_depth_prompt",
286
+ "match_scenario",
287
+ "match_creator_notes",
288
+ ]);
289
+ const arrayRules = new Set([
290
+ "key",
291
+ "keys",
292
+ "keysecondary",
293
+ "secondary_keys",
294
+ "triggers",
295
+ "characterFilterNames",
296
+ "characterFilterTags",
297
+ ]);
298
+ function safeRules(entry, path, diagnostics) {
299
+ const rules = {};
300
+ const visit = (source, target, prefix) => {
301
+ for (const [key, value] of Object.entries(source)) {
302
+ if (["content", "comment", "name"].includes(key))
303
+ continue;
304
+ if (key === "extensions" && prefix === path) {
305
+ const nested = record(value, `${prefix}.extensions`), safe = {};
306
+ visit(nested, safe, `${prefix}.extensions`);
307
+ target.extensions = safe;
308
+ continue;
309
+ }
310
+ if (key === "characterFilter" && prefix === path) {
311
+ if (value === null) {
312
+ target.characterFilter = null;
313
+ continue;
314
+ }
315
+ const filter = record(value, `${prefix}.characterFilter`), safe = {};
316
+ for (const [field, item] of Object.entries(filter)) {
317
+ if (["names", "tags"].includes(field) &&
318
+ Array.isArray(item) &&
319
+ item.every((x) => typeof x === "string"))
320
+ safe[field] = item;
321
+ else if (field === "isExclude" && typeof item === "boolean")
322
+ safe[field] = item;
323
+ else
324
+ fail("invalid_rule_type", `${prefix}.characterFilter.${field}`);
325
+ }
326
+ target.characterFilter = safe;
327
+ continue;
328
+ }
329
+ if (!ruleKeys.has(key)) {
330
+ diagnostics.push({
331
+ code: "unsupported_field",
332
+ path: `${prefix}.${key}`,
333
+ });
334
+ continue;
335
+ }
336
+ const nullable = new Set([
337
+ "scanDepth",
338
+ "scan_depth",
339
+ "caseSensitive",
340
+ "case_sensitive",
341
+ "matchWholeWords",
342
+ "match_whole_words",
343
+ "useGroupScoring",
344
+ "use_group_scoring",
345
+ "sticky",
346
+ "cooldown",
347
+ "delay",
348
+ "group_weight",
349
+ "probability",
350
+ ]);
351
+ if (value === null &&
352
+ (numericRules.has(key) || booleanRules.has(key)) &&
353
+ !nullable.has(key))
354
+ fail("invalid_rule_type", `${prefix}.${key}`);
355
+ if (value !== null &&
356
+ numericRules.has(key) &&
357
+ (typeof value !== "number" || !Number.isFinite(value)))
358
+ fail("invalid_rule_type", `${prefix}.${key}`);
359
+ if (value !== null && booleanRules.has(key) && typeof value !== "boolean")
360
+ fail("invalid_rule_type", `${prefix}.${key}`);
361
+ if (arrayRules.has(key) &&
362
+ (!Array.isArray(value) ||
363
+ !value.every((item) => typeof item === "string")))
364
+ fail("invalid_rule_type", `${prefix}.${key}`);
365
+ if ([
366
+ "group",
367
+ "automationId",
368
+ "automation_id",
369
+ "outletName",
370
+ "outlet_name",
371
+ ].includes(key) &&
372
+ typeof value !== "string")
373
+ fail("invalid_rule_type", `${prefix}.${key}`);
374
+ if (key === "selectiveLogic" &&
375
+ (typeof value !== "number" ||
376
+ !Number.isInteger(value) ||
377
+ value < 0 ||
378
+ value > 3))
379
+ fail("invalid_rule_type", `${prefix}.${key}`);
380
+ if (["position", "role"].includes(key) &&
381
+ typeof value !== "number" &&
382
+ typeof value !== "string")
383
+ fail("invalid_rule_type", `${prefix}.${key}`);
384
+ if (["delayUntilRecursion", "delay_until_recursion"].includes(key) &&
385
+ typeof value !== "number" &&
386
+ typeof value !== "boolean")
387
+ fail("invalid_rule_type", `${prefix}.${key}`);
388
+ if (value === null ||
389
+ typeof value === "string" ||
390
+ typeof value === "boolean" ||
391
+ (typeof value === "number" && Number.isFinite(value)) ||
392
+ (Array.isArray(value) &&
393
+ value.every((item) => typeof item === "string")))
394
+ target[key] = value;
395
+ else
396
+ fail("invalid_rule_type", `${prefix}.${key}`);
397
+ }
398
+ };
399
+ visit(entry, rules, path);
400
+ return rules;
401
+ }
402
+ export function inspectSillyTavernContent(source) {
403
+ if (source.length > limit)
404
+ fail("source_too_large", "$");
405
+ const png = source[0] === 137 ? pngJson(source) : undefined;
406
+ const bytes = png?.bytes ?? source;
407
+ let root;
408
+ try {
409
+ const text = decoder.decode(bytes);
410
+ root = record(JSON.parse(text), "$");
411
+ const document = parseDocument(text, { uniqueKeys: true, strict: true });
412
+ if (document.errors.length > 0)
413
+ fail("ambiguous_json", "$");
414
+ }
415
+ catch (error) {
416
+ if (error instanceof Error &&
417
+ (error.message.includes("expected_object") ||
418
+ error.message.includes("ambiguous_json")))
419
+ throw error;
420
+ return fail("invalid_json_or_utf8", "$");
421
+ }
422
+ if (png?.key === "ccv3" && root.spec !== "chara_card_v3")
423
+ fail("invalid_png_metadata", "$.spec");
424
+ if (png && root.spec === undefined && root.entries !== undefined)
425
+ fail("invalid_png_metadata", "$");
426
+ let kind = "character", version = "v1", data = root;
427
+ if (root.spec !== undefined) {
428
+ if (root.spec !== "chara_card_v2" && root.spec !== "chara_card_v3")
429
+ fail("unknown_version", "$.spec");
430
+ const expected = root.spec === "chara_card_v2" ? "2" : "3";
431
+ if (typeof root.spec_version !== "string" ||
432
+ !(expected === "2"
433
+ ? root.spec_version === "2.0"
434
+ : /^3\.\d+(?:\.\d+)?$/u.test(root.spec_version)))
435
+ fail("unknown_version", "$.spec_version");
436
+ version = String(root.spec);
437
+ data = record(root.data, "$.data");
438
+ }
439
+ else if (root.entries !== undefined) {
440
+ kind = "worldbook";
441
+ version = "world_info";
442
+ }
443
+ else if (![
444
+ "name",
445
+ "description",
446
+ "personality",
447
+ "scenario",
448
+ "first_mes",
449
+ "mes_example",
450
+ ].every((key) => typeof root[key] === "string"))
451
+ fail("unsupported_format", "$");
452
+ const diagnostics = [];
453
+ if (data !== root)
454
+ for (const key of [
455
+ "name",
456
+ "description",
457
+ "personality",
458
+ "scenario",
459
+ "first_mes",
460
+ "mes_example",
461
+ ])
462
+ if (root[key] !== undefined && root[key] !== data[key])
463
+ diagnostics.push({ code: "legacy_field_conflict", path: `$.${key}` });
464
+ if (kind === "character" &&
465
+ (typeof data.name !== "string" || !data.name.trim()))
466
+ fail("missing_character_name", "$.data.name");
467
+ for (const key of [
468
+ "name",
469
+ "description",
470
+ "personality",
471
+ "scenario",
472
+ "first_mes",
473
+ "mes_example",
474
+ "system_prompt",
475
+ "post_history_instructions",
476
+ "creator_notes",
477
+ "creator",
478
+ "character_version",
479
+ ])
480
+ if (data[key] !== undefined && typeof data[key] !== "string")
481
+ fail("expected_string", `$.data.${key}`);
482
+ if (data.alternate_greetings !== undefined &&
483
+ (!Array.isArray(data.alternate_greetings) ||
484
+ !data.alternate_greetings.every((x) => typeof x === "string")))
485
+ fail("expected_string_array", "$.data.alternate_greetings");
486
+ if (data.tags !== undefined &&
487
+ (!Array.isArray(data.tags) ||
488
+ !data.tags.every((item) => typeof item === "string")))
489
+ fail("expected_string_array", "$.data.tags");
490
+ if (data.extensions !== undefined) {
491
+ const extensions = record(data.extensions, "$.data.extensions");
492
+ if (extensions.world !== undefined && typeof extensions.world !== "string")
493
+ fail("expected_string", "$.data.extensions.world");
494
+ if (extensions.depth_prompt !== undefined) {
495
+ const depth = record(extensions.depth_prompt, "$.data.extensions.depth_prompt");
496
+ if ((depth.prompt !== undefined && typeof depth.prompt !== "string") ||
497
+ (depth.depth !== undefined && typeof depth.depth !== "number") ||
498
+ (depth.role !== undefined &&
499
+ typeof depth.role !== "number" &&
500
+ typeof depth.role !== "string"))
501
+ fail("invalid_depth_prompt", "$.data.extensions.depth_prompt");
502
+ }
503
+ }
504
+ const book = kind === "worldbook"
505
+ ? root
506
+ : data.character_book === undefined
507
+ ? undefined
508
+ : record(data.character_book, "$.data.character_book");
509
+ if (kind === "character") {
510
+ const knownFields = new Set([
511
+ "name",
512
+ "description",
513
+ "personality",
514
+ "scenario",
515
+ "first_mes",
516
+ "alternate_greetings",
517
+ "mes_example",
518
+ "system_prompt",
519
+ "post_history_instructions",
520
+ "creator_notes",
521
+ "creator",
522
+ "character_version",
523
+ "tags",
524
+ "extensions",
525
+ "character_book",
526
+ ]);
527
+ for (const key of Object.keys(data))
528
+ if (!knownFields.has(key))
529
+ diagnostics.push({ code: "unsupported_field", path: `$.data.${key}` });
530
+ }
531
+ const entries = [];
532
+ if (book) {
533
+ const embedded = kind === "character", collection = book.entries;
534
+ if (embedded
535
+ ? !Array.isArray(collection)
536
+ : !collection ||
537
+ typeof collection !== "object" ||
538
+ Array.isArray(collection))
539
+ fail("invalid_entries", "$.entries");
540
+ const items = embedded
541
+ ? collection.map((v, i) => [String(i), v])
542
+ : Object.entries(collection);
543
+ const identities = new Set();
544
+ for (const [key, value] of items) {
545
+ const path = `${embedded ? "$.data.character_book" : "$"}.entries.${key}`, entry = record(value, path);
546
+ const providedIdentity = embedded ? entry.id : entry.uid;
547
+ const rawIdentity = providedIdentity === undefined ? key : providedIdentity;
548
+ if ((typeof rawIdentity !== "string" && typeof rawIdentity !== "number") ||
549
+ (typeof rawIdentity === "number" && !Number.isFinite(rawIdentity)))
550
+ fail("invalid_identity", path);
551
+ const identity = String(rawIdentity);
552
+ if (entry.comment !== undefined && typeof entry.comment !== "string")
553
+ fail("expected_string", `${path}.comment`);
554
+ if (entry.name !== undefined && typeof entry.name !== "string")
555
+ fail("expected_string", `${path}.name`);
556
+ if (identities.has(identity))
557
+ fail("duplicate_identity", path);
558
+ identities.add(identity);
559
+ if (typeof entry.content !== "string")
560
+ fail("expected_string", `${path}.content`);
561
+ const flag = embedded ? entry.enabled : entry.disable;
562
+ if (flag !== undefined && typeof flag !== "boolean")
563
+ fail("expected_boolean", path);
564
+ for (const k of embedded
565
+ ? ["keys", "secondary_keys"]
566
+ : ["key", "keysecondary"])
567
+ if (entry[k] !== undefined &&
568
+ (!Array.isArray(entry[k]) ||
569
+ !entry[k].every((x) => typeof x === "string")))
570
+ fail("expected_string_array", `${path}.${k}`);
571
+ entries.push({
572
+ id: `entry-${entries.length}`,
573
+ path,
574
+ name: String(entry.comment ?? entry.name ?? identity),
575
+ content: entry.content,
576
+ enabled: embedded ? flag === true : flag !== true,
577
+ rules: safeRules(entry, path, diagnostics),
578
+ });
579
+ }
580
+ }
581
+ if (recordOrEmpty(data.extensions).world)
582
+ diagnostics.push({
583
+ code: book
584
+ ? "external_worldbook_reference"
585
+ : "external_worldbook_unavailable",
586
+ path: "$.data.extensions.world",
587
+ });
588
+ if (source[0] === 137)
589
+ diagnostics.push({ code: "image_not_migrated", path: "$" });
590
+ const legacyFields = data === root
591
+ ? {}
592
+ : Object.fromEntries([
593
+ "name",
594
+ "description",
595
+ "personality",
596
+ "scenario",
597
+ "first_mes",
598
+ "mes_example",
599
+ ]
600
+ .filter((key) => root[key] !== undefined)
601
+ .map((key) => [key, root[key]]));
602
+ return { kind, version, fields: data, legacyFields, entries, diagnostics };
603
+ }
604
+ function recordOrEmpty(value) {
605
+ return value && typeof value === "object" && !Array.isArray(value)
606
+ ? value
607
+ : {};
608
+ }
609
+ function hasMacro(text) {
610
+ return /\{\{[^{}]+\}\}|<(?:USER|BOT|CHAR|GROUP|CHARIFNOTGROUP)>/iu.test(text);
611
+ }
612
+ export function convertSillyTavernContent(source, selection, locale = "en") {
613
+ const view = inspectSillyTavernContent(source), files = [], diagnostics = [...view.diagnostics];
614
+ if (!selection?.entries ||
615
+ typeof selection.entries !== "object" ||
616
+ Array.isArray(selection.entries))
617
+ fail("invalid_selection", "$.selection");
618
+ const allowedSelection = [
619
+ "character",
620
+ "scenario",
621
+ "opening",
622
+ "example",
623
+ "systemPrompt",
624
+ "attachments",
625
+ "entries",
626
+ ];
627
+ if (Object.keys(selection).some((key) => !allowedSelection.includes(key)) ||
628
+ [
629
+ selection.character,
630
+ selection.scenario,
631
+ selection.example,
632
+ selection.systemPrompt,
633
+ ].some((value) => value !== undefined && typeof value !== "boolean") ||
634
+ (selection.opening !== undefined && typeof selection.opening !== "string"))
635
+ fail("invalid_selection", "$.selection");
636
+ const attachmentFields = [
637
+ "creator_notes",
638
+ "creator",
639
+ "character_version",
640
+ "tags",
641
+ "post_history_instructions",
642
+ "depth_prompt",
643
+ ];
644
+ if (selection.attachments !== undefined &&
645
+ (!Array.isArray(selection.attachments) ||
646
+ selection.attachments.some((key) => !attachmentFields.includes(key))))
647
+ fail("invalid_selection", "$.selection.attachments");
648
+ const preservedFields = {};
649
+ for (const key of selection.attachments ?? []) {
650
+ const value = key === "depth_prompt"
651
+ ? recordOrEmpty(view.fields.extensions).depth_prompt
652
+ : view.fields[key];
653
+ if (typeof value === "string" ||
654
+ (key === "tags" &&
655
+ Array.isArray(value) &&
656
+ value.every((item) => typeof item === "string")))
657
+ preservedFields[key] = value;
658
+ else if (key === "depth_prompt" && value !== undefined) {
659
+ const depth = record(value, "$.data.extensions.depth_prompt");
660
+ preservedFields[key] = Object.fromEntries(Object.entries(depth).filter(([field, item]) => ["prompt", "depth", "role"].includes(field) &&
661
+ (typeof item === "string" || typeof item === "number")));
662
+ }
663
+ }
664
+ const known = new Set(view.entries.map((e) => e.id));
665
+ for (const [id, target] of Object.entries(selection.entries))
666
+ if (!known.has(id) || !["world", "attachment"].includes(target))
667
+ fail("invalid_selection", `$.selection.entries.${id}`);
668
+ const field = (key) => typeof view.fields[key] === "string" ? view.fields[key] : "";
669
+ const addDocument = (path, id, title, content) => files.push({
670
+ path,
671
+ contents: stringify({
672
+ $document: {
673
+ id,
674
+ ref: id.replaceAll(".", "-"),
675
+ title: title || id,
676
+ summary: title || id,
677
+ aliases: [],
678
+ },
679
+ content,
680
+ }),
681
+ });
682
+ if (selection.character && view.kind === "character") {
683
+ const content = {
684
+ description: field("description"),
685
+ personality: field("personality"),
686
+ };
687
+ if (hasMacro(Object.values(content).join("\n")) ||
688
+ hasMacro(field("name"))) {
689
+ diagnostics.push({ code: "unbound_macro", path: "$.data.description" });
690
+ preservedFields.character = { name: field("name"), ...content };
691
+ }
692
+ else if (content.description || content.personality)
693
+ addDocument("world/characters/character.yaml", "character.imported", field("name"), content);
694
+ }
695
+ if (selection.scenario && field("scenario") && !hasMacro(field("scenario")))
696
+ addDocument("world/current-situation.yaml", "current.situation", locale === "zh-CN" ? "当前情境" : "Current situation", field("scenario"));
697
+ else if (selection.scenario && hasMacro(field("scenario"))) {
698
+ diagnostics.push({ code: "unbound_macro", path: "$.data.scenario" });
699
+ preservedFields.scenario = field("scenario");
700
+ }
701
+ const openings = [
702
+ field("first_mes"),
703
+ ...(Array.isArray(view.fields.alternate_greetings)
704
+ ? view.fields.alternate_greetings
705
+ : []),
706
+ ];
707
+ if (selection.opening !== undefined) {
708
+ if (!/^\d+$/u.test(selection.opening) ||
709
+ !openings[Number(selection.opening)])
710
+ fail("invalid_opening_selection", "$.selection.opening");
711
+ const remaining = openings
712
+ .map((text, index) => ({ index, text }))
713
+ .filter((item) => item.index !== Number(selection.opening) && item.text);
714
+ if (remaining.length)
715
+ preservedFields.other_openings = remaining;
716
+ const opening = openings[Number(selection.opening)];
717
+ if (hasMacro(opening)) {
718
+ diagnostics.push({ code: "unbound_macro", path: "$.data.first_mes" });
719
+ preservedFields.opening = opening;
720
+ }
721
+ else
722
+ files.push({ path: "opening.md", contents: opening });
723
+ }
724
+ const blocks = [];
725
+ if (selection.example && hasMacro(field("mes_example"))) {
726
+ diagnostics.push({ code: "unbound_macro", path: "$.data.mes_example" });
727
+ preservedFields.mes_example = field("mes_example");
728
+ }
729
+ if (selection.systemPrompt && hasMacro(field("system_prompt"))) {
730
+ diagnostics.push({ code: "unbound_macro", path: "$.data.system_prompt" });
731
+ preservedFields.system_prompt = field("system_prompt");
732
+ }
733
+ if (selection.example &&
734
+ field("mes_example") &&
735
+ !hasMacro(field("mes_example")))
736
+ blocks.push(`# ${locale === "zh-CN" ? "虚构对话示例" : "Fictional dialogue example"}\n\n${field("mes_example")}`);
737
+ if (selection.systemPrompt &&
738
+ field("system_prompt") &&
739
+ !hasMacro(field("system_prompt")))
740
+ blocks.push(`# ${locale === "zh-CN" ? "角色专属提示" : "Character system prompt"}\n\n${field("system_prompt")}`);
741
+ files.push({
742
+ path: "control/blocks/world.md",
743
+ contents: blocks.join("\n\n") || "<!-- No imported control instructions. -->\n",
744
+ });
745
+ files.push({
746
+ path: "control/frame.yaml",
747
+ contents: 'format: narraeon.world-frame/v1\nbindings:\n currentSituation: "@current-situation"\ninstructions:\n - markdown: blocks/world.md\ncontext:\n - slot: { kind: catalog, directory: characters, maxEntries: 24, required: false }\n - slot: { kind: catalog, directory: lore, maxEntries: 100, required: false }\n - slot: { kind: current_situation }\n - slot: { kind: history, recent: 2 }\n - slot: { kind: additional_materials }\n',
748
+ });
749
+ files.push({
750
+ path: "control/player-views.yaml",
751
+ contents: "format: narraeon.player-views/v1\nviews: []\n",
752
+ });
753
+ const attachments = [];
754
+ for (const entry of view.entries) {
755
+ const target = selection.entries[entry.id];
756
+ if (!target)
757
+ continue;
758
+ if (target === "world" &&
759
+ entry.enabled &&
760
+ !hasMacro(entry.content) &&
761
+ !hasMacro(entry.name))
762
+ addDocument(`world/lore/${entry.id}.yaml`, `lore.${entry.id}`, entry.name, entry.content);
763
+ {
764
+ attachments.push({
765
+ id: entry.id,
766
+ path: entry.path,
767
+ name: entry.name,
768
+ content: entry.content,
769
+ enabled: entry.enabled,
770
+ rules: entry.rules,
771
+ });
772
+ if (target === "world" &&
773
+ (!entry.enabled || hasMacro(entry.content) || hasMacro(entry.name)))
774
+ diagnostics.push({
775
+ code: entry.enabled ? "unbound_macro" : "disabled_entry",
776
+ path: entry.path,
777
+ });
778
+ }
779
+ }
780
+ if (diagnostics.some((item) => item.code === "unbound_macro")) {
781
+ const openingIndex = files.findIndex((file) => file.path === "opening.md");
782
+ if (openingIndex >= 0) {
783
+ preservedFields.opening = files[openingIndex].contents;
784
+ files.splice(openingIndex, 1);
785
+ }
786
+ }
787
+ files.push({
788
+ path: "assets/sillytavern-migration.yaml",
789
+ contents: stringify({
790
+ selectedFields: preservedFields,
791
+ selectedAttachments: attachments,
792
+ diagnostics,
793
+ }),
794
+ });
795
+ return { files, diagnostics };
796
+ }