@riemannre3/dsh-roleplay 0.1.3

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.
@@ -0,0 +1,361 @@
1
+ export function withoutWorldbookRuntimeEffects(state, entryIds) {
2
+ const effects = Object.fromEntries(Object.entries(state.effects).map(([id, effect]) => [id, { ...effect }]));
3
+ for (const id of entryIds)
4
+ delete effects[id];
5
+ return { messageCount: state.messageCount, effects };
6
+ }
7
+ function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
8
+ function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : []; }
9
+ function numberValue(value, fallback) { return typeof value === "number" && Number.isFinite(value) ? value : fallback; }
10
+ function booleanValue(value, fallback) { return typeof value === "boolean" ? value : fallback; }
11
+ function stringValue(value, fallback = "") { return typeof value === "string" ? value : fallback; }
12
+ function normalizePosition(value, extensionValue) {
13
+ const candidate = extensionValue ?? value;
14
+ if (candidate === "before_char" || candidate === 0)
15
+ return "before_char";
16
+ if (candidate === "an_top" || candidate === 2)
17
+ return "an_top";
18
+ if (candidate === "an_bottom" || candidate === 3)
19
+ return "an_bottom";
20
+ if (candidate === "at_depth" || candidate === 4)
21
+ return "at_depth";
22
+ if (candidate === "before_examples" || candidate === 5)
23
+ return "before_examples";
24
+ if (candidate === "after_examples" || candidate === 6)
25
+ return "after_examples";
26
+ if (candidate === "outlet" || candidate === 7)
27
+ return "outlet";
28
+ return "after_char";
29
+ }
30
+ function normalizeRole(value) {
31
+ if (value === "user" || value === 1)
32
+ return "user";
33
+ if (value === "assistant" || value === 2)
34
+ return "assistant";
35
+ return "system";
36
+ }
37
+ export function normalizeWorldbookEntry(value, index) {
38
+ if (!isObject(value))
39
+ throw new Error(`世界书第 ${index + 1} 项不是对象`);
40
+ const extensions = isObject(value.extensions) ? value.extensions : {};
41
+ const rawLogic = numberValue(value.selectiveLogic ?? extensions.selectiveLogic, 0);
42
+ const selectiveLogic = [0, 1, 2, 3].includes(rawLogic) ? rawLogic : 0;
43
+ const probabilityEnabled = booleanValue(extensions.use_probability ?? extensions.useProbability, true);
44
+ const probability = probabilityEnabled ? Math.max(0, Math.min(100, numberValue(extensions.probability ?? value.probability, 100))) : 100;
45
+ const scanDepthExplicit = booleanValue(value.scanDepthExplicit, Object.prototype.hasOwnProperty.call(value, "scan_depth") || Object.prototype.hasOwnProperty.call(extensions, "scan_depth"));
46
+ return {
47
+ id: String(value.id ?? value.uid ?? index),
48
+ comment: stringValue(value.comment ?? value.memo),
49
+ content: stringValue(value.content),
50
+ enabled: booleanValue(value.enabled, true),
51
+ constant: booleanValue(value.constant, false),
52
+ keys: strings(value.keys ?? value.key),
53
+ secondaryKeys: strings(value.secondaryKeys ?? value.secondary_keys ?? value.keysecondary),
54
+ selective: booleanValue(value.selective, false),
55
+ selectiveLogic,
56
+ order: numberValue(value.insertion_order ?? value.order, 0),
57
+ position: normalizePosition(value.position, extensions.position),
58
+ depth: Math.max(0, Math.trunc(numberValue(extensions.depth ?? value.depth, 4))),
59
+ role: normalizeRole(extensions.role ?? value.role),
60
+ outletName: stringValue(extensions.outlet_name ?? value.outletName).trim(),
61
+ scanDepth: Math.max(0, Math.trunc(numberValue(value.scanDepth ?? extensions.scan_depth ?? value.scan_depth, 0))),
62
+ scanDepthExplicit,
63
+ useRegex: booleanValue(value.useRegex ?? value.use_regex ?? extensions.use_regex, false),
64
+ caseSensitive: booleanValue(value.caseSensitive ?? extensions.case_sensitive, false),
65
+ matchWholeWords: booleanValue(value.matchWholeWords ?? extensions.match_whole_words, false),
66
+ probability,
67
+ group: stringValue(extensions.group ?? value.group).trim(),
68
+ groupOverride: booleanValue(extensions.group_override ?? value.groupOverride, false),
69
+ groupWeight: Math.max(0, numberValue(extensions.group_weight ?? value.groupWeight, 100)),
70
+ sticky: Math.max(0, Math.trunc(numberValue(extensions.sticky ?? value.sticky, 0))),
71
+ cooldown: Math.max(0, Math.trunc(numberValue(extensions.cooldown ?? value.cooldown, 0))),
72
+ delay: Math.max(0, Math.trunc(numberValue(extensions.delay ?? value.delay, 0))),
73
+ preventRecursion: booleanValue(value.preventRecursion ?? extensions.prevent_recursion, false),
74
+ excludeRecursion: booleanValue(value.excludeRecursion ?? extensions.exclude_recursion, false),
75
+ delayUntilRecursion: booleanValue(value.delayUntilRecursion ?? extensions.delay_until_recursion, false),
76
+ };
77
+ }
78
+ function parseRegexKey(key, entry) {
79
+ const slash = key.match(/^\/(.*)\/([dgimsuvy]*)$/u);
80
+ try {
81
+ if (slash !== null)
82
+ return new RegExp(slash[1] ?? "", slash[2] ?? "");
83
+ if (entry.useRegex)
84
+ return new RegExp(key, entry.caseSensitive ? "u" : "iu");
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ return undefined;
90
+ }
91
+ function literalMatch(haystack, needle, entry) {
92
+ const source = entry.caseSensitive ? haystack : haystack.toLocaleLowerCase();
93
+ const target = entry.caseSensitive ? needle : needle.toLocaleLowerCase();
94
+ if (!entry.matchWholeWords)
95
+ return source.includes(target);
96
+ const escaped = target.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
97
+ try {
98
+ return new RegExp(`(?:^|[^\\p{L}\\p{N}_])${escaped}(?:$|[^\\p{L}\\p{N}_])`, "u").test(source);
99
+ }
100
+ catch {
101
+ return source.includes(target);
102
+ }
103
+ }
104
+ function keyMatches(haystack, key, entry) {
105
+ const regex = parseRegexKey(key, entry);
106
+ if (regex !== undefined)
107
+ return regex.test(haystack);
108
+ if (entry.useRegex || /^\/.+\/[dgimsuvy]*$/u.test(key))
109
+ return false;
110
+ return literalMatch(haystack, key, entry);
111
+ }
112
+ function matchedKeys(haystack, keys, entry) { return keys.filter((key) => keyMatches(haystack, key, entry)); }
113
+ function secondaryPass(matches, logic) {
114
+ if (matches.length === 0)
115
+ return true;
116
+ if (logic === 1)
117
+ return !matches.every(Boolean);
118
+ if (logic === 2)
119
+ return !matches.some(Boolean);
120
+ if (logic === 3)
121
+ return matches.every(Boolean);
122
+ return matches.some(Boolean);
123
+ }
124
+ function deterministicPercent(seed) {
125
+ let hash = 2166136261;
126
+ for (const character of seed) {
127
+ hash ^= character.codePointAt(0) ?? 0;
128
+ hash = Math.imul(hash, 16777619);
129
+ }
130
+ return (hash >>> 0) % 100;
131
+ }
132
+ function traceRow(entry, activated, pass, reason, primary = [], secondary = [], parents = []) {
133
+ return { id: entry.id, label: entry.comment.trim() || `条目 ${entry.id}`, activated, pass, reason, matchedPrimaryKeys: primary, matchedSecondaryKeys: secondary, recursiveParents: parents, position: entry.position, depth: entry.depth, role: entry.role, order: entry.order };
134
+ }
135
+ function chooseGroupWinner(group, seed) {
136
+ if (group.some((entry) => entry.groupOverride))
137
+ return [...group].sort((left, right) => right.order - left.order || left.id.localeCompare(right.id))[0];
138
+ const total = group.reduce((sum, entry) => sum + entry.groupWeight, 0);
139
+ if (total <= 0)
140
+ return [...group].sort((left, right) => right.order - left.order || left.id.localeCompare(right.id))[0];
141
+ let target = deterministicPercent(seed) / 100 * total;
142
+ for (const entry of group) {
143
+ target -= entry.groupWeight;
144
+ if (target < 0)
145
+ return entry;
146
+ }
147
+ return group[group.length - 1];
148
+ }
149
+ export function activateWorldbook(entries, scanTexts, seed = "default", options = {}) {
150
+ const ordered = [...entries].sort((left, right) => right.order - left.order || left.id.localeCompare(right.id));
151
+ const messageCount = options.messageCount ?? scanTexts.length;
152
+ const runtimeState = options.runtimeState ?? { messageCount, effects: {} };
153
+ runtimeState.messageCount = messageCount;
154
+ const activeIds = new Set();
155
+ const trace = new Map();
156
+ const recursiveTexts = [];
157
+ let pass = 0;
158
+ let passesRun = 0;
159
+ const maximumPasses = Math.max(0, Math.trunc(options.maxRecursionSteps ?? 0));
160
+ while (pass <= entries.length && (maximumPasses === 0 || pass < maximumPasses)) {
161
+ passesRun += 1;
162
+ const recursive = pass > 0;
163
+ const newlyActive = [];
164
+ for (const entry of ordered) {
165
+ if (activeIds.has(entry.id))
166
+ continue;
167
+ if (!entry.enabled || entry.content.length === 0) {
168
+ if (pass === 0)
169
+ trace.set(entry.id, traceRow(entry, false, pass, !entry.enabled ? "disabled" : "empty-content"));
170
+ continue;
171
+ }
172
+ if (entry.delay > messageCount) {
173
+ if (pass === 0)
174
+ trace.set(entry.id, traceRow(entry, false, pass, "delay"));
175
+ continue;
176
+ }
177
+ const effect = runtimeState.effects[entry.id] ?? {};
178
+ const sticky = (effect.stickyUntil ?? -1) >= messageCount;
179
+ if (!sticky && (effect.cooldownUntil ?? -1) >= messageCount) {
180
+ if (pass === 0)
181
+ trace.set(entry.id, traceRow(entry, false, pass, "cooldown"));
182
+ continue;
183
+ }
184
+ if (!recursive && entry.delayUntilRecursion && !sticky)
185
+ continue;
186
+ if (recursive && entry.excludeRecursion && !sticky)
187
+ continue;
188
+ const scanDepth = entry.scanDepthExplicit ? entry.scanDepth : Math.max(0, Math.trunc(options.globalScanDepth ?? 2));
189
+ const baseTexts = scanDepth > 0 ? scanTexts.slice(-scanDepth) : [];
190
+ const baseHaystack = baseTexts.join("\n");
191
+ const recursiveHaystack = recursiveTexts.map((item) => item.text).join("\n");
192
+ const haystack = [baseHaystack, recursiveHaystack].filter(Boolean).join("\n");
193
+ const primaryMatches = matchedKeys(haystack, entry.keys, entry);
194
+ const secondaryMatches = matchedKeys(haystack, entry.secondaryKeys, entry);
195
+ const primary = sticky || entry.constant || primaryMatches.length > 0;
196
+ const secondary = !entry.selective || secondaryPass(entry.secondaryKeys.map((key) => secondaryMatches.includes(key)), entry.selectiveLogic);
197
+ const probability = sticky || entry.probability >= 100 || deterministicPercent(`${seed}:${messageCount}:${entry.id}`) < entry.probability;
198
+ if (primary && secondary && probability) {
199
+ activeIds.add(entry.id);
200
+ newlyActive.push(entry);
201
+ const parents = recursiveTexts.filter((item) => entry.keys.some((key) => keyMatches(item.text, key, entry))).map((item) => item.id);
202
+ const reason = sticky ? "sticky" : entry.constant ? "constant" : recursive && parents.length > 0 ? "recursive-keyword" : "keyword";
203
+ trace.set(entry.id, traceRow(entry, true, pass, reason, primaryMatches, secondaryMatches, parents));
204
+ }
205
+ else if (pass === 0 && !entry.delayUntilRecursion)
206
+ trace.set(entry.id, traceRow(entry, false, pass, !primary ? "primary-miss" : !secondary ? "secondary-miss" : "probability-miss", primaryMatches, secondaryMatches));
207
+ }
208
+ if (newlyActive.length === 0)
209
+ break;
210
+ const recursiveContent = newlyActive.filter((entry) => !entry.preventRecursion).map((entry) => ({ id: entry.id, text: entry.content }));
211
+ if (recursiveContent.length === 0)
212
+ break;
213
+ recursiveTexts.push(...recursiveContent);
214
+ pass += 1;
215
+ }
216
+ const active = ordered.filter((entry) => activeIds.has(entry.id));
217
+ const grouped = new Map();
218
+ for (const entry of active)
219
+ if (entry.group.length > 0)
220
+ grouped.set(entry.group, [...(grouped.get(entry.group) ?? []), entry]);
221
+ for (const [groupName, groupEntries] of grouped) {
222
+ if (groupEntries.length < 2)
223
+ continue;
224
+ const winner = chooseGroupWinner(groupEntries, `${seed}:${messageCount}:group:${groupName}`);
225
+ for (const entry of groupEntries) {
226
+ if (entry.id === winner.id)
227
+ continue;
228
+ activeIds.delete(entry.id);
229
+ const previous = trace.get(entry.id);
230
+ trace.set(entry.id, traceRow(entry, false, previous?.pass ?? 0, `group-loser:${winner.id}`, previous?.matchedPrimaryKeys, previous?.matchedSecondaryKeys, previous?.recursiveParents));
231
+ }
232
+ }
233
+ const finalActive = ordered.filter((entry) => activeIds.has(entry.id));
234
+ for (const entry of finalActive) {
235
+ if (entry.sticky <= 0 && entry.cooldown <= 0)
236
+ continue;
237
+ if (runtimeState.effects[entry.id] !== undefined)
238
+ continue;
239
+ runtimeState.effects[entry.id] = { ...(entry.sticky > 0 ? { stickyUntil: messageCount + entry.sticky } : {}), ...(entry.cooldown > 0 ? { cooldownUntil: messageCount + entry.sticky + entry.cooldown } : {}) };
240
+ }
241
+ for (const [id, effect] of Object.entries(runtimeState.effects))
242
+ if ((effect.stickyUntil ?? -1) < messageCount && (effect.cooldownUntil ?? -1) < messageCount)
243
+ delete runtimeState.effects[id];
244
+ return { active: finalActive, trace: ordered.map((entry) => trace.get(entry.id) ?? traceRow(entry, false, 0, "not-eligible")), passes: passesRun, runtimeState };
245
+ }
246
+ // Template-backed World Info cannot be activated safely from its raw source:
247
+ // raw EJS may create false recursion matches, win an exclusion group and write
248
+ // sticky/cooldown effects before rendering later fails. Resolve only the
249
+ // currently selected candidates, then restart activation from the same clean
250
+ // state until the rendered graph reaches a fixed point.
251
+ export async function activateWorldbookWithRenderer(entries, scanTexts, seed, options, needsRender, render) {
252
+ const sourceById = new Map(entries.map((entry) => [entry.id, entry]));
253
+ const templateIds = new Set(entries.filter(needsRender).map((entry) => entry.id));
254
+ const rendered = new Map();
255
+ const failed = new Set();
256
+ const effectExclusions = () => new Set([
257
+ ...failed,
258
+ ...[...rendered].filter(([, content]) => content.length === 0).map(([id]) => id),
259
+ ]);
260
+ for (let iteration = 0; iteration <= templateIds.size; iteration += 1) {
261
+ const exclusions = effectExclusions();
262
+ const projectedEntries = entries.map((entry) => {
263
+ if (failed.has(entry.id))
264
+ return { ...entry, enabled: false };
265
+ if (!templateIds.has(entry.id))
266
+ return entry;
267
+ if (rendered.has(entry.id))
268
+ return { ...entry, content: rendered.get(entry.id) };
269
+ // The placeholder keeps direct/constant/sticky candidacy intact, while
270
+ // preventRecursion guarantees raw template source never becomes a scan
271
+ // surface for another entry.
272
+ return { ...entry, content: "\u0000dsh-pending-template\u0000", preventRecursion: true };
273
+ });
274
+ const baseRuntimeState = options.runtimeState === undefined
275
+ ? undefined
276
+ : withoutWorldbookRuntimeEffects(options.runtimeState, exclusions);
277
+ const activation = activateWorldbook(projectedEntries, scanTexts, seed, { ...options, runtimeState: baseRuntimeState });
278
+ const pending = activation.active.filter((entry) => templateIds.has(entry.id) && !rendered.has(entry.id) && !failed.has(entry.id));
279
+ if (pending.length === 0) {
280
+ return {
281
+ activation: { ...activation, runtimeState: withoutWorldbookRuntimeEffects(activation.runtimeState, exclusions) },
282
+ renderedEntryIds: [...rendered.keys()],
283
+ failedEntryIds: [...failed],
284
+ };
285
+ }
286
+ for (const candidate of pending) {
287
+ const source = sourceById.get(candidate.id);
288
+ const output = await render(source);
289
+ if (output === undefined)
290
+ failed.add(candidate.id);
291
+ else
292
+ rendered.set(candidate.id, output);
293
+ }
294
+ }
295
+ throw new Error("世界书模板激活未在有限迭代内收敛");
296
+ }
297
+ function stableMacroNumber(seed) {
298
+ let hash = 2166136261;
299
+ for (const character of seed) {
300
+ hash ^= character.codePointAt(0) ?? 0;
301
+ hash = Math.imul(hash, 16777619);
302
+ }
303
+ return hash >>> 0;
304
+ }
305
+ function messageVariable(values, rawPath) {
306
+ const segments = rawPath.trim().split(".").filter(Boolean);
307
+ if (segments[0]?.toLocaleLowerCase() === "stat_data")
308
+ segments.shift();
309
+ let current = values.messageVariables ?? {};
310
+ for (const segment of segments) {
311
+ if (typeof current !== "object" || current === null || Array.isArray(current))
312
+ return undefined;
313
+ current = current[segment];
314
+ }
315
+ return current;
316
+ }
317
+ function displayMacroValue(value, pretty = false) {
318
+ if (value === undefined || value === null)
319
+ return value === null ? "null" : "";
320
+ if (typeof value === "string")
321
+ return value;
322
+ if (typeof value === "number" || typeof value === "boolean")
323
+ return String(value);
324
+ try {
325
+ return JSON.stringify(value, null, pretty ? 2 : undefined);
326
+ }
327
+ catch {
328
+ return "";
329
+ }
330
+ }
331
+ export function substituteCardMacros(text, values) {
332
+ const locals = values.localVariables ??= {};
333
+ return text
334
+ .replace(/\{\{setvar::([^{}:]+)::([^{}]*)\}\}/giu, (_match, name, value) => { locals[name.trim()] = value.trim(); return ""; })
335
+ .replace(/\{\{getvar::([^{}:]+)\}\}/giu, (_match, name) => locals[name.trim()] ?? "")
336
+ .replace(/\{\{get_message_variable::([^{}]+)\}\}/giu, (_match, path) => displayMacroValue(messageVariable(values, path)))
337
+ .replace(/\{\{format_message_variable::stat_data\}\}/giu, () => displayMacroValue(values.messageVariables ?? {}, true))
338
+ .replace(/\{\{random::([^{}]+)\}\}/giu, (macro, rawOptions) => {
339
+ const options = rawOptions.split("::");
340
+ return options[stableMacroNumber(`${values.macroSeed ?? "card"}:${macro}`) % options.length] ?? "";
341
+ })
342
+ .replace(/\{\{roll(?::(\d+)d(\d+)(?:\+(\d+))?)?\}\}/giu, (macro, countText, sidesText, bonusText) => {
343
+ const count = countText === undefined ? 1 : Math.max(1, Math.min(100, Number(countText)));
344
+ const sides = sidesText === undefined ? 100 : Math.max(1, Math.min(10000, Number(sidesText)));
345
+ const bonus = bonusText === undefined ? 0 : Number(bonusText);
346
+ let total = bonus;
347
+ for (let index = 0; index < count; index += 1)
348
+ total += 1 + (stableMacroNumber(`${values.macroSeed ?? "card"}:${macro}:${index}`) % sides);
349
+ return String(total);
350
+ })
351
+ .replace(/\{\{user\}\}/giu, values.userName)
352
+ .replace(/\{\{char\}\}/giu, values.characterName);
353
+ }
354
+ export function placeWorldbook(entries, values) {
355
+ const placed = [...entries].sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)).map((entry) => ({ id: entry.id, label: entry.comment.trim() || `条目 ${entry.id}`, content: substituteCardMacros(entry.content, values), order: entry.order, position: entry.position, depth: entry.depth, role: entry.role, outletName: entry.outletName }));
356
+ const join = (position) => placed.filter((entry) => entry.position === position).map((entry) => entry.content).join("\n\n");
357
+ const outlets = {};
358
+ for (const entry of placed.filter((candidate) => candidate.position === "outlet" && candidate.outletName.length > 0))
359
+ outlets[entry.outletName] = [outlets[entry.outletName], entry.content].filter(Boolean).join("\n\n");
360
+ return { beforeCharacter: join("before_char"), afterCharacter: join("after_char"), atDepth: join("at_depth"), beforeExamples: join("before_examples"), afterExamples: join("after_examples"), authorNoteTop: join("an_top"), authorNoteBottom: join("an_bottom"), outlets, entries: placed };
361
+ }
package/package.json ADDED
@@ -0,0 +1,128 @@
1
+ {
2
+ "name": "@riemannre3/dsh-roleplay",
3
+ "version": "0.1.3",
4
+ "license": "MIT",
5
+ "author": "RiemannRe3",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/RiemannRe3/DSH-RolePlay.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/RiemannRe3/DSH-RolePlay/issues"
12
+ },
13
+ "homepage": "https://github.com/RiemannRe3/DSH-RolePlay#readme",
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "provenance": true,
17
+ "registry": "https://registry.npmjs.org/"
18
+ },
19
+ "keywords": [
20
+ "deepseek-harness",
21
+ "dsh",
22
+ "dsh-plugin",
23
+ "roleplay",
24
+ "tavern"
25
+ ],
26
+ "type": "module",
27
+ "main": "./lib/index.js",
28
+ "exports": {
29
+ ".": "./lib/index.js",
30
+ "./client": "./lib/client.js",
31
+ "./card-library": "./lib/card-library.js",
32
+ "./card-runtime": "./lib/card-runtime.js",
33
+ "./worldbook": "./lib/worldbook.js",
34
+ "./prompt-compiler": "./lib/prompt-compiler.js",
35
+ "./persona-runtime": "./lib/persona-runtime.js",
36
+ "./preset-runtime": "./lib/preset-runtime.js",
37
+ "./session-runtime": "./lib/session-runtime.js",
38
+ "./variable-runtime": "./lib/variable-runtime.js",
39
+ "./frontend-runtime": "./lib/frontend-runtime.js",
40
+ "./rich-message": "./lib/rich-message.js",
41
+ "./ejs-runtime": "./lib/ejs-runtime.js",
42
+ "./split-mvu": "./lib/split-mvu.js",
43
+ "./compatibility-call-runtime": "./lib/compatibility-call-runtime.js",
44
+ "./mvu-session-control": "./lib/mvu-session-control.js",
45
+ "./auxiliary-generation": "./lib/auxiliary-generation.js",
46
+ "./package.json": "./package.json"
47
+ },
48
+ "files": [
49
+ "cordis.patch.yml",
50
+ "demo.png",
51
+ "plugin-settings.png",
52
+ "lib/index.js",
53
+ "lib/client.js",
54
+ "lib/card-library.js",
55
+ "lib/card-runtime.js",
56
+ "lib/worldbook.js",
57
+ "lib/prompt-compiler.js",
58
+ "lib/persona-runtime.js",
59
+ "lib/preset-runtime.js",
60
+ "lib/session-runtime.js",
61
+ "lib/variable-runtime.js",
62
+ "lib/frontend-runtime.js",
63
+ "lib/rich-message.js",
64
+ "lib/ejs-runtime.js",
65
+ "lib/ejs-worker.js",
66
+ "lib/split-mvu.js",
67
+ "lib/compatibility-call-runtime.js",
68
+ "lib/mvu-session-control.js",
69
+ "lib/auxiliary-generation.js",
70
+ "lib/lifecycle.js",
71
+ "runtime-assets/standalone/core.js",
72
+ "runtime-assets/standalone/index.html",
73
+ "runtime-assets/standalone/style.css",
74
+ "runtime-assets/required/index.html",
75
+ "runtime-assets/required/weather-flags.json",
76
+ "README.md"
77
+ ],
78
+ "dsh": {
79
+ "bundle": {
80
+ "patch": "./cordis.patch.yml"
81
+ },
82
+ "client": {
83
+ "inject": [
84
+ "@deepseek-ai/dsh-client-runtime",
85
+ "@deepseek-ai/dsh-client-ui-layout",
86
+ "@deepseek-ai/dsh-client-ui-primitives",
87
+ "@deepseek-ai/dsh-client-ui-sidebar",
88
+ "@deepseek-ai/dsh-client-ui-conversation"
89
+ ],
90
+ "platform": "web"
91
+ }
92
+ },
93
+ "description": "DSH-native Tavern character-card, opening and worldbook runtime.",
94
+ "engines": {
95
+ "node": ">=22.19.0 <23 || >=24.13.1 <25"
96
+ },
97
+ "scripts": {
98
+ "build": "tsc -p tsconfig.json && node bundle-client.mjs",
99
+ "prepack": "npm run build"
100
+ },
101
+ "devDependencies": {
102
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.7",
103
+ "@deepseek-ai/schemastery": "^3.18.1",
104
+ "typescript": "5.9.3"
105
+ },
106
+ "peerDependencies": {
107
+ "@deepseek-ai/cordis": "^4.0.1",
108
+ "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
109
+ "@deepseek-ai/dsh-agent-default-model": "0.1.1-rc.2",
110
+ "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
111
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.1-rc.2",
112
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
113
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
114
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
115
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
116
+ "@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
117
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
118
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.2.0",
119
+ "@deepseek-ai/dsh-storage-domain": "0.1.1-rc.2",
120
+ "@deepseek-ai/schemastery": "^3.18.1",
121
+ "react": "^18.2.0",
122
+ "zod": "^4.0.0"
123
+ },
124
+ "dependencies": {
125
+ "@jitl/quickjs-wasmfile-release-sync": "0.32.0",
126
+ "quickjs-emscripten-core": "0.32.0"
127
+ }
128
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Required 资源检查</title><style>:root{color-scheme:dark;font-family:system-ui;background:#101713;color:#eef8f0}body{margin:0;display:grid;place-items:center;min-height:100vh}main{width:min(620px,calc(100vw - 40px));padding:26px;border:1px solid #579769;border-radius:16px;background:#17251b}button{margin:4px;padding:9px 14px;border:0;border-radius:999px}#result[data-status=pass]{color:#8af0a4}#result[data-status=blocked]{color:#ffadad}</style></head>
4
+ <body><main data-ft-marker="FT_REMOTE_READY_V1"><p>FT_REMOTE_READY_V1</p><h1>气象旗语资源检查</h1><p>受控第二 origin:<code id="origin">未配置</code></p><p><button data-scenario="normal">正常</button><button data-scenario="missing">缺失</button><button data-scenario="digest-mismatch">摘要错误</button></p><pre id="result">等待检查</pre></main><script type="module" src="./main.js"></script></body>
5
+ </html>
@@ -0,0 +1,7 @@
1
+ {
2
+ "schema_version": 1,
3
+ "asset_id": "weather-flags",
4
+ "marker": "FT_REMOTE_ASSET_OK_V1",
5
+ "signal": "北风三级",
6
+ "required": true
7
+ }
@@ -0,0 +1,40 @@
1
+ export function mountStandaloneFrontend({ adapter, document }) {
2
+ const bindingNode = document.querySelector("#binding");
3
+ const projectionNode = document.querySelector("#projection");
4
+ const eventNode = document.querySelector("#event");
5
+ const submitButton = document.querySelector("#submit");
6
+
7
+ const render = (projection) => {
8
+ projectionNode.replaceChildren(...projection.messages.map((message) => {
9
+ const item = document.createElement("li");
10
+ item.textContent = `${message.role}: ${message.text}`;
11
+ item.dataset.seq = String(message.seq);
12
+ return item;
13
+ }));
14
+ };
15
+
16
+ const ready = async () => {
17
+ const binding = await adapter.getBinding();
18
+ bindingNode.textContent = `chat=${binding.chatId} · card=${binding.cardId} · adapter=${adapter.version}`;
19
+ render(await adapter.getProjection());
20
+ adapter.subscribe((event) => {
21
+ eventNode.textContent = `${event.type}:${event.operationId ?? event.seq ?? ""}`;
22
+ if (event.projection) render(event.projection);
23
+ });
24
+ };
25
+
26
+ submitButton.addEventListener("click", async () => {
27
+ submitButton.disabled = true;
28
+ delete eventNode.dataset.error;
29
+ try {
30
+ const result = await adapter.submitTurn({ text: "我在调度台登记:启航前检查北舷灯。", operationId: "ft-standalone-u1" });
31
+ eventNode.textContent = `generation_committed:${result.committedSeq}`;
32
+ render(await adapter.getProjection());
33
+ } catch (error) {
34
+ eventNode.dataset.error = error.code ?? "bridge_unavailable";
35
+ eventNode.textContent = `提交失败:${eventNode.dataset.error}`;
36
+ } finally { submitButton.disabled = false; }
37
+ });
38
+
39
+ return ready();
40
+ }
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>远航调度台</title><link rel="stylesheet" href="./style.css"></head>
4
+ <body>
5
+ <main data-ft-marker="FT_STANDALONE_READY_V1">
6
+ <p class="eyebrow">FT_STANDALONE_READY_V1</p>
7
+ <h1>远航调度台</h1>
8
+ <p id="binding">正在绑定原版聊天…</p>
9
+ <ol id="projection"></ol>
10
+ <button id="submit" type="button">登记:检查北舷灯</button>
11
+ <p id="event" aria-live="polite"></p>
12
+ </main>
13
+ <script type="module" src="./main.js"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,7 @@
1
+ :root { color-scheme: dark; font-family: Inter, system-ui, sans-serif; background: #07131f; color: #e7f2ff; }
2
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; }
3
+ main { width: min(620px, calc(100vw - 40px)); padding: 28px; border: 1px solid #376a91; border-radius: 18px; background: linear-gradient(145deg, #10283c, #091923); box-shadow: 0 20px 70px #0008; }
4
+ .eyebrow { color: #79c9ff; font-size: 12px; letter-spacing: .14em; }
5
+ button { border: 0; border-radius: 999px; padding: 11px 18px; background: #79c9ff; color: #06111a; font-weight: 700; cursor: pointer; }
6
+ button:disabled { opacity: .55; cursor: wait; }
7
+ #event[data-error] { color: #ff9f9f; }