@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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/cordis.patch.yml +4 -0
- package/demo.png +0 -0
- package/lib/auxiliary-generation.js +68 -0
- package/lib/card-library.js +37 -0
- package/lib/card-runtime.js +363 -0
- package/lib/client.js +2327 -0
- package/lib/compatibility-call-runtime.js +34 -0
- package/lib/ejs-runtime.js +239 -0
- package/lib/ejs-worker.js +33 -0
- package/lib/frontend-runtime.js +352 -0
- package/lib/index.js +2998 -0
- package/lib/lifecycle.js +93 -0
- package/lib/mvu-session-control.js +70 -0
- package/lib/persona-runtime.js +60 -0
- package/lib/preset-runtime.js +222 -0
- package/lib/prompt-compiler.js +288 -0
- package/lib/rich-message.js +176 -0
- package/lib/session-runtime.js +324 -0
- package/lib/split-mvu.js +85 -0
- package/lib/variable-runtime.js +618 -0
- package/lib/worldbook.js +361 -0
- package/package.json +128 -0
- package/plugin-settings.png +0 -0
- package/runtime-assets/required/index.html +5 -0
- package/runtime-assets/required/weather-flags.json +7 -0
- package/runtime-assets/standalone/core.js +40 -0
- package/runtime-assets/standalone/index.html +15 -0
- package/runtime-assets/standalone/style.css +7 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Keep the context prefix stable: it is persisted in existing DSH Sessions.
|
|
2
|
+
export const TAVERN_CONTEXT_PREFIX = "dsh-re3-rp:";
|
|
3
|
+
export const TAVERN_PLUGIN_ID = "dsh-roleplay";
|
|
4
|
+
export const LEGACY_TAVERN_PLUGIN_ID = "dsh-re3-rp";
|
|
5
|
+
export function isTavernPluginId(value) {
|
|
6
|
+
return value === TAVERN_PLUGIN_ID || value === LEGACY_TAVERN_PLUGIN_ID;
|
|
7
|
+
}
|
|
8
|
+
export const TAVERN_WORLD_CONTEXT_MARKER = "[DSH_RE3_RP_WORLD_CONTEXT]";
|
|
9
|
+
export const TAVERN_ASSEMBLY_MARKER = "[DSH_RE3_RP_ASSEMBLY]";
|
|
10
|
+
function tavernAssemblyMessage(summary, contextText) {
|
|
11
|
+
const changeParts = [
|
|
12
|
+
summary.addedEntryIds.length > 0 ? `+${summary.addedEntryIds.length}` : "",
|
|
13
|
+
summary.removedEntryIds.length > 0 ? `−${summary.removedEntryIds.length}` : "",
|
|
14
|
+
].filter(Boolean);
|
|
15
|
+
const title = summary.stage === "prepared" ? "酒馆预设已准备" : "酒馆上下文已装配";
|
|
16
|
+
const text = contextText ?? `${TAVERN_ASSEMBLY_MARKER}\n${title}\n${summary.activeEntries} 项世界书激活 · ${summary.depthInjections} 项深度插入${changeParts.length > 0 ? ` · ${changeParts.join(" / ")}` : ""}`;
|
|
17
|
+
return {
|
|
18
|
+
id: crypto.randomUUID(),
|
|
19
|
+
role: "user",
|
|
20
|
+
content: [{ type: "text", text }],
|
|
21
|
+
source: {
|
|
22
|
+
kind: "plugin",
|
|
23
|
+
plugin: TAVERN_PLUGIN_ID,
|
|
24
|
+
form: "assembly",
|
|
25
|
+
assemblyId: summary.assemblyId,
|
|
26
|
+
assembly: summary,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function appendTavernAssemblyEvent(session, summary, contextText) {
|
|
31
|
+
const event = session.append("user/message", tavernAssemblyMessage(summary, contextText), { surfaceOp: "append" });
|
|
32
|
+
return event.seq;
|
|
33
|
+
}
|
|
34
|
+
export function currentAssemblySurfaceSeq(session) {
|
|
35
|
+
return session?.surface?.nodes?.find((seq) => {
|
|
36
|
+
const event = session.events?.[seq];
|
|
37
|
+
const message = event?.data?.message ?? event?.data;
|
|
38
|
+
return event?.type === "user/message" && message?.source?.kind === "plugin" && isTavernPluginId(message.source.plugin) && message.source.form === "assembly";
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function upsertTavernAssemblyContext(session, summary, contextText) {
|
|
42
|
+
const previous = currentAssemblySurfaceSeq(session);
|
|
43
|
+
if (previous === undefined)
|
|
44
|
+
return appendTavernAssemblyEvent(session, summary, contextText);
|
|
45
|
+
const event = session.append("user/message", tavernAssemblyMessage(summary, contextText), {
|
|
46
|
+
surfaceOp: { op: "replace", start: previous, end: previous },
|
|
47
|
+
sourceEventSeqs: [previous],
|
|
48
|
+
});
|
|
49
|
+
return event.seq;
|
|
50
|
+
}
|
|
51
|
+
export function createTavernSessionSeed(openingText, sections = [], time = Date.now(), preparedAssembly) {
|
|
52
|
+
const events = [];
|
|
53
|
+
const append = (type, data, surfaceOp) => {
|
|
54
|
+
events.push({
|
|
55
|
+
seq: events.length,
|
|
56
|
+
time,
|
|
57
|
+
type,
|
|
58
|
+
data,
|
|
59
|
+
...(surfaceOp === undefined ? {} : { surfaceOp }),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
if (sections.length > 0) {
|
|
63
|
+
append("user/message", {
|
|
64
|
+
id: crypto.randomUUID(),
|
|
65
|
+
role: "user",
|
|
66
|
+
content: [{ type: "text", text: renderWorldbookSnapshot(sections) }],
|
|
67
|
+
source: {
|
|
68
|
+
kind: "plugin",
|
|
69
|
+
plugin: TAVERN_PLUGIN_ID,
|
|
70
|
+
form: "snapshot",
|
|
71
|
+
sections,
|
|
72
|
+
},
|
|
73
|
+
}, "append");
|
|
74
|
+
}
|
|
75
|
+
if (preparedAssembly !== undefined)
|
|
76
|
+
append("user/message", tavernAssemblyMessage(preparedAssembly), "append");
|
|
77
|
+
const turn = 1;
|
|
78
|
+
const step = 1;
|
|
79
|
+
append("turn/start", { turn });
|
|
80
|
+
append("step/start", { turn, step });
|
|
81
|
+
append("assistant/message", {
|
|
82
|
+
turn,
|
|
83
|
+
step,
|
|
84
|
+
message: {
|
|
85
|
+
id: crypto.randomUUID(),
|
|
86
|
+
role: "assistant",
|
|
87
|
+
content: [{ type: "text", text: openingText }],
|
|
88
|
+
source: { kind: "model", provider: TAVERN_PLUGIN_ID, model: "character-card-opening" },
|
|
89
|
+
},
|
|
90
|
+
}, "append");
|
|
91
|
+
append("step/end", { turn, step });
|
|
92
|
+
append("turn/end", { turn, reason: { kind: "completed" } });
|
|
93
|
+
return events;
|
|
94
|
+
}
|
|
95
|
+
export function isPlayerMessage(event) {
|
|
96
|
+
if (event?.type !== "user/message")
|
|
97
|
+
return false;
|
|
98
|
+
const message = event.data?.message ?? event.data;
|
|
99
|
+
return message?.source?.kind !== "plugin";
|
|
100
|
+
}
|
|
101
|
+
export function hasPlayerMessage(events) {
|
|
102
|
+
return events.some(isPlayerMessage);
|
|
103
|
+
}
|
|
104
|
+
export function currentOpeningSurfaceSeq(session) {
|
|
105
|
+
return session?.surface?.nodes?.find((seq) => session.events?.[seq]?.type === "assistant/message");
|
|
106
|
+
}
|
|
107
|
+
export function currentOpeningText(session) {
|
|
108
|
+
const seq = currentOpeningSurfaceSeq(session);
|
|
109
|
+
if (seq === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
const event = session.events?.[seq];
|
|
112
|
+
return messageText(event?.data?.message ?? event?.data);
|
|
113
|
+
}
|
|
114
|
+
function messageText(message) {
|
|
115
|
+
if (typeof message?.content === "string")
|
|
116
|
+
return message.content;
|
|
117
|
+
if (!Array.isArray(message?.content))
|
|
118
|
+
return "";
|
|
119
|
+
return message.content
|
|
120
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
121
|
+
.map((part) => part.text)
|
|
122
|
+
.join("\n");
|
|
123
|
+
}
|
|
124
|
+
export function currentWorldbookSurfaceSeq(session) {
|
|
125
|
+
return session?.surface?.nodes?.find((seq) => {
|
|
126
|
+
const event = session.events?.[seq];
|
|
127
|
+
if (event?.type !== "user/message")
|
|
128
|
+
return false;
|
|
129
|
+
const message = event.data?.message ?? event.data;
|
|
130
|
+
return message?.source?.kind === "plugin" && isTavernPluginId(message.source.plugin) && messageText(message).startsWith(TAVERN_WORLD_CONTEXT_MARKER);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function worldbookContextRevision(session) {
|
|
134
|
+
return (session?.events ?? []).filter((event) => {
|
|
135
|
+
if (event?.type !== "user/message")
|
|
136
|
+
return false;
|
|
137
|
+
const message = event.data?.message ?? event.data;
|
|
138
|
+
return message?.source?.kind === "plugin" && isTavernPluginId(message.source.plugin) && messageText(message).startsWith(TAVERN_WORLD_CONTEXT_MARKER);
|
|
139
|
+
}).length;
|
|
140
|
+
}
|
|
141
|
+
function surfaceEventKind(event) {
|
|
142
|
+
if (event?.type === "user/message") {
|
|
143
|
+
const message = event.data?.message ?? event.data;
|
|
144
|
+
if (message?.source?.kind === "plugin" && isTavernPluginId(message.source.plugin) && message.source.form === "assembly")
|
|
145
|
+
return "assembly";
|
|
146
|
+
if (message?.source?.kind === "plugin" && isTavernPluginId(message.source.plugin) && messageText(message).startsWith(TAVERN_WORLD_CONTEXT_MARKER))
|
|
147
|
+
return "worldbook";
|
|
148
|
+
if (message?.source?.kind === "plugin")
|
|
149
|
+
return "context";
|
|
150
|
+
return "user";
|
|
151
|
+
}
|
|
152
|
+
if (event?.type === "assistant/message") {
|
|
153
|
+
const message = event.data?.message ?? event.data;
|
|
154
|
+
if (isTavernPluginId(message?.source?.provider) && message.source.model === "character-card-opening")
|
|
155
|
+
return "opening";
|
|
156
|
+
return "assistant";
|
|
157
|
+
}
|
|
158
|
+
if (event?.type === "tool/result")
|
|
159
|
+
return "tool";
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
function surfaceEventText(event) {
|
|
163
|
+
const message = event?.data?.message ?? event?.data;
|
|
164
|
+
const text = messageText(message);
|
|
165
|
+
if (text.length > 0)
|
|
166
|
+
return text;
|
|
167
|
+
if (event?.type !== "tool/result")
|
|
168
|
+
return "";
|
|
169
|
+
try {
|
|
170
|
+
return JSON.stringify(event.data?.result ?? event.data, null, 2);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function surfaceEventLabel(kind, operation) {
|
|
177
|
+
if (kind === "assembly")
|
|
178
|
+
return "酒馆上下文已装配";
|
|
179
|
+
const noun = kind === "worldbook" ? "世界书上下文"
|
|
180
|
+
: kind === "opening" ? "开场"
|
|
181
|
+
: kind === "context" ? "上下文"
|
|
182
|
+
: kind === "user" ? "用户消息"
|
|
183
|
+
: kind === "assistant" ? "助手消息"
|
|
184
|
+
: "工具结果";
|
|
185
|
+
return `${noun}${operation === "append" ? "已追加" : "已替换"}`;
|
|
186
|
+
}
|
|
187
|
+
function surfaceTrajectoryRowKey(event, seq, kind) {
|
|
188
|
+
if (kind === "assembly" || kind === "worldbook" || kind === "context")
|
|
189
|
+
return `context\u0000seq\u0000${seq}`;
|
|
190
|
+
if (kind === "assistant" || kind === "opening") {
|
|
191
|
+
const turn = Number(event?.data?.turn);
|
|
192
|
+
const step = Number(event?.data?.step);
|
|
193
|
+
if (Number.isInteger(turn) && Number.isInteger(step))
|
|
194
|
+
return `assistant\u0000${turn}\u0000${step}`;
|
|
195
|
+
}
|
|
196
|
+
if (kind === "tool") {
|
|
197
|
+
const turn = Number(event?.data?.turn);
|
|
198
|
+
const step = Number(event?.data?.step);
|
|
199
|
+
if (Number.isInteger(turn) && Number.isInteger(step))
|
|
200
|
+
return `tool\u0000${turn}\u0000${step}`;
|
|
201
|
+
}
|
|
202
|
+
return `user\u0000seq\u0000${seq}`;
|
|
203
|
+
}
|
|
204
|
+
function replacedSeqs(event) {
|
|
205
|
+
const operation = event?.surfaceOp;
|
|
206
|
+
if (operation === null || typeof operation !== "object" || operation.op !== "replace")
|
|
207
|
+
return [];
|
|
208
|
+
const start = Number(operation.start);
|
|
209
|
+
const end = Number(operation.end);
|
|
210
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || end < start)
|
|
211
|
+
return [];
|
|
212
|
+
return Array.from({ length: end - start + 1 }, (_, index) => start + index);
|
|
213
|
+
}
|
|
214
|
+
export function tavernSurfaceAudit(session) {
|
|
215
|
+
const active = new Set(Array.isArray(session?.surface?.nodes) ? session.surface.nodes : []);
|
|
216
|
+
const entries = (Array.isArray(session?.events) ? session.events : []).flatMap((event, index) => {
|
|
217
|
+
const kind = surfaceEventKind(event);
|
|
218
|
+
if (kind === undefined)
|
|
219
|
+
return [];
|
|
220
|
+
const message = event.data?.message ?? event.data;
|
|
221
|
+
const operation = event.surfaceOp === "append" ? "append" : event.surfaceOp?.op === "replace" ? "replace" : undefined;
|
|
222
|
+
if (operation === undefined)
|
|
223
|
+
return [];
|
|
224
|
+
const sections = Array.isArray(message?.source?.sections) ? message.source.sections : [];
|
|
225
|
+
const assembly = kind === "assembly" && message?.source?.assembly && typeof message.source.assembly === "object" ? message.source.assembly : undefined;
|
|
226
|
+
const seq = Number.isInteger(event.seq) ? event.seq : index;
|
|
227
|
+
const replacement = replacedSeqs(event);
|
|
228
|
+
return [{
|
|
229
|
+
seq,
|
|
230
|
+
time: Number.isFinite(event.time) ? event.time : null,
|
|
231
|
+
kind,
|
|
232
|
+
label: surfaceEventLabel(kind, operation),
|
|
233
|
+
operation,
|
|
234
|
+
active: active.has(seq),
|
|
235
|
+
replaces: replacement,
|
|
236
|
+
replacedBy: [],
|
|
237
|
+
sourceEventSeqs: Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.filter(Number.isInteger) : [],
|
|
238
|
+
trajectoryRowKey: surfaceTrajectoryRowKey(event, seq, kind),
|
|
239
|
+
sectionNames: sections.flatMap((section) => typeof section?.name === "string" ? [section.name] : []),
|
|
240
|
+
characterCount: surfaceEventText(event).length,
|
|
241
|
+
...(assembly === undefined ? {} : { assembly }),
|
|
242
|
+
}];
|
|
243
|
+
});
|
|
244
|
+
const replacedBy = new Map();
|
|
245
|
+
for (const entry of entries)
|
|
246
|
+
for (const target of entry.replaces)
|
|
247
|
+
replacedBy.set(target, [...(replacedBy.get(target) ?? []), entry.seq]);
|
|
248
|
+
return entries.map((entry) => ({
|
|
249
|
+
...entry,
|
|
250
|
+
replacedBy: replacedBy.get(entry.seq) ?? [],
|
|
251
|
+
trajectoryRowKey: surfaceTrajectoryRowKey(session.events?.[entry.seq], entry.seq, entry.kind),
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
export function tavernSurfaceEventDetail(session, seq) {
|
|
255
|
+
const audit = tavernSurfaceAudit(session).find((entry) => entry.seq === seq);
|
|
256
|
+
const event = session?.events?.[seq];
|
|
257
|
+
if (audit === undefined || event === undefined)
|
|
258
|
+
return undefined;
|
|
259
|
+
const message = event.data?.message ?? event.data;
|
|
260
|
+
const sections = Array.isArray(message?.source?.sections)
|
|
261
|
+
? message.source.sections.flatMap((section) => typeof section?.name === "string" && typeof section?.text === "string" ? [{ name: section.name, text: section.text }] : [])
|
|
262
|
+
: [];
|
|
263
|
+
return { ...audit, content: surfaceEventText(event), sections };
|
|
264
|
+
}
|
|
265
|
+
export function renderWorldbookSnapshot(sections) {
|
|
266
|
+
const body = sections.length === 0
|
|
267
|
+
? "当前没有生效的世界书条目。"
|
|
268
|
+
: sections.map((section) => `[${section.name}]\n${section.text}`).join("\n\n");
|
|
269
|
+
return `${TAVERN_WORLD_CONTEXT_MARKER}\n这是当前对话对应的世界书事实快照。不要向玩家提及世界书、快照或插件。\n\n${body}`;
|
|
270
|
+
}
|
|
271
|
+
export function upsertWorldbookContext(session, sections) {
|
|
272
|
+
const text = renderWorldbookSnapshot(sections);
|
|
273
|
+
const currentSeq = currentWorldbookSurfaceSeq(session);
|
|
274
|
+
const currentEvent = currentSeq === undefined ? undefined : session.events?.[currentSeq];
|
|
275
|
+
const currentMessage = currentEvent?.data?.message ?? currentEvent?.data;
|
|
276
|
+
const currentText = messageText(currentMessage);
|
|
277
|
+
const currentRevision = worldbookContextRevision(session);
|
|
278
|
+
if (currentSeq !== undefined && currentText === text)
|
|
279
|
+
return { changed: false, seq: currentSeq, revision: currentRevision };
|
|
280
|
+
const revision = currentRevision + 1;
|
|
281
|
+
const message = {
|
|
282
|
+
id: crypto.randomUUID(),
|
|
283
|
+
role: "user",
|
|
284
|
+
content: [{ type: "text", text }],
|
|
285
|
+
source: {
|
|
286
|
+
kind: "plugin",
|
|
287
|
+
plugin: TAVERN_PLUGIN_ID,
|
|
288
|
+
form: "snapshot",
|
|
289
|
+
sections,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
const event = currentSeq === undefined
|
|
293
|
+
? session.append("user/message", message, { surfaceOp: "append" })
|
|
294
|
+
: session.append("user/message", message, {
|
|
295
|
+
surfaceOp: { op: "replace", start: currentSeq, end: currentSeq },
|
|
296
|
+
sourceEventSeqs: [currentSeq],
|
|
297
|
+
});
|
|
298
|
+
return { changed: true, seq: event.seq, revision };
|
|
299
|
+
}
|
|
300
|
+
export function isolateTavernAssembly(assembly) {
|
|
301
|
+
return {
|
|
302
|
+
...assembly,
|
|
303
|
+
contexts: (assembly.contexts ?? []).filter((context) => typeof context?.name === "string" && context.name.startsWith(TAVERN_CONTEXT_PREFIX)),
|
|
304
|
+
tools: [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
export function adjacentOpeningId(openings, currentId, offset) {
|
|
308
|
+
if (openings.length === 0)
|
|
309
|
+
return undefined;
|
|
310
|
+
const current = Math.max(0, openings.findIndex((opening) => opening.id === currentId));
|
|
311
|
+
return openings[(current + offset % openings.length + openings.length) % openings.length]?.id;
|
|
312
|
+
}
|
|
313
|
+
export function openingIdFromSetChatMessages(messages, openings) {
|
|
314
|
+
if (!Array.isArray(messages) || messages.length !== 1)
|
|
315
|
+
return undefined;
|
|
316
|
+
const selection = messages[0];
|
|
317
|
+
if (selection === null || typeof selection !== "object" || Array.isArray(selection))
|
|
318
|
+
return undefined;
|
|
319
|
+
const { message_id: messageId, swipe_id: swipeId } = selection;
|
|
320
|
+
if (messageId !== 0 || !Number.isInteger(swipeId) || swipeId < 0)
|
|
321
|
+
return undefined;
|
|
322
|
+
const openingId = `opening-${swipeId}`;
|
|
323
|
+
return openings.some((opening) => opening.id === openingId) ? openingId : undefined;
|
|
324
|
+
}
|
package/lib/split-mvu.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { applyVariableUpdate } from "./variable-runtime.js";
|
|
2
|
+
function entryPhase(entry) {
|
|
3
|
+
if (/\[mvu_plot\]/iu.test(entry.comment))
|
|
4
|
+
return "plot";
|
|
5
|
+
if (/\[mvu_update\]/iu.test(entry.comment))
|
|
6
|
+
return "update";
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
export function hasSplitMvuContract(entries) {
|
|
10
|
+
let hasPlot = false;
|
|
11
|
+
let hasUpdate = false;
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const phase = entryPhase(entry);
|
|
14
|
+
if (phase === "plot")
|
|
15
|
+
hasPlot = true;
|
|
16
|
+
if (phase === "update")
|
|
17
|
+
hasUpdate = true;
|
|
18
|
+
}
|
|
19
|
+
return hasPlot && hasUpdate;
|
|
20
|
+
}
|
|
21
|
+
export function splitMvuActivationForPhase(activation, phase) {
|
|
22
|
+
return {
|
|
23
|
+
...activation,
|
|
24
|
+
active: activation.active.filter((entry) => {
|
|
25
|
+
const taggedPhase = entryPhase(entry);
|
|
26
|
+
return taggedPhase === undefined || taggedPhase === phase;
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Tavern MVU prompts commonly emit JSON Patch `replace` for a leaf that has not
|
|
32
|
+
* appeared in the current snapshot yet. Keep the core variable runtime strict,
|
|
33
|
+
* but make that one ecosystem compatibility concession for the isolated
|
|
34
|
+
* secondary MVU phase. Every rewrite is followed by a complete atomic replay;
|
|
35
|
+
* unrelated path/type errors remain failures.
|
|
36
|
+
*/
|
|
37
|
+
export function applySplitMvuPatchCompatibility(before, body) {
|
|
38
|
+
const blocks = [];
|
|
39
|
+
const matcher = /<JSON_?Patch\b[^>]*>([\s\S]*?)<\/JSON_?Patch>/giu;
|
|
40
|
+
for (const match of body.matchAll(matcher)) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(match[1].trim());
|
|
43
|
+
if (!Array.isArray(parsed))
|
|
44
|
+
return { body, result: applyVariableUpdate(before, body), repairedPaths: [] };
|
|
45
|
+
blocks.push({ source: match[0], items: parsed });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { body, result: applyVariableUpdate(before, body), repairedPaths: [] };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (blocks.length === 0)
|
|
52
|
+
return { body, result: applyVariableUpdate(before, body), repairedPaths: [] };
|
|
53
|
+
const render = () => {
|
|
54
|
+
let rendered = body;
|
|
55
|
+
for (const block of blocks) {
|
|
56
|
+
const tag = /^<JSON_?Patch\b[^>]*>/iu.exec(block.source)?.[0] ?? "<JSONPatch>";
|
|
57
|
+
const close = /<\/JSON_?Patch>$/iu.exec(block.source)?.[0] ?? "</JSONPatch>";
|
|
58
|
+
rendered = rendered.replace(block.source, `${tag}${JSON.stringify(block.items)}${close}`);
|
|
59
|
+
}
|
|
60
|
+
return rendered;
|
|
61
|
+
};
|
|
62
|
+
const repairedPaths = [];
|
|
63
|
+
let candidate = body;
|
|
64
|
+
let result = applyVariableUpdate(before, candidate);
|
|
65
|
+
for (let repairCount = 0; result.status === "failed" && repairCount < 64; repairCount += 1) {
|
|
66
|
+
const diagnostic = result.diagnostics.find((item) => item.code === "PATH_NOT_FOUND" && item.operation === "replace" && typeof item.path === "string");
|
|
67
|
+
if (diagnostic?.path === undefined)
|
|
68
|
+
break;
|
|
69
|
+
let repaired = false;
|
|
70
|
+
for (const block of blocks) {
|
|
71
|
+
const item = block.items.find((entry) => String(entry.op).toLocaleLowerCase() === "replace" && entry.path === diagnostic.path);
|
|
72
|
+
if (item === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
item.op = "insert";
|
|
75
|
+
repairedPaths.push(diagnostic.path);
|
|
76
|
+
repaired = true;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (!repaired)
|
|
80
|
+
break;
|
|
81
|
+
candidate = render();
|
|
82
|
+
result = applyVariableUpdate(before, candidate);
|
|
83
|
+
}
|
|
84
|
+
return { body: candidate, result, repairedPaths };
|
|
85
|
+
}
|