@musnows/scriverse 1.0.8 → 1.0.9
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/dist/ai.js +7 -88
- package/dist/ai.js.map +1 -1
- package/dist/app.js +33 -42
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +176 -133
- package/dist/public/index.html +3 -3
- package/dist/public/styles.css +14 -23
- package/dist/skills/continue-writing/SKILL.md +2 -2
- package/dist/skills/polish-writing/SKILL.md +2 -2
- package/dist/user-auth.js +0 -3
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -5,7 +5,7 @@ import { renderMarkdown } from "/markdown.js?v=20260912-stream-render-v2";
|
|
|
5
5
|
import { createStreamingMarkdownRenderer } from "/stream-markdown.js?v=20260912-stream-render-v2";
|
|
6
6
|
import { createAiRenderScheduler } from "/ai-render-scheduler.js?v=20260912-stream-render-v2";
|
|
7
7
|
import { createImWorkspace } from "/im.js?v=20260904-im-judge-outcomes-v106";
|
|
8
|
-
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope
|
|
8
|
+
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
|
|
9
9
|
import { applyAiSkillCommand, findAiSkillCommand, listAiSkillOptions } from "/ai-skill-menu.js?v=20260830-ai-skill-slash-menu-v1";
|
|
10
10
|
import {
|
|
11
11
|
emptyRoleplayScenePin,
|
|
@@ -2326,6 +2326,148 @@ function aiReferenceKindLabel(reference) {
|
|
|
2326
2326
|
return ({ character: "角色", setting: "设定", chapter: "章节", "context-settings": "能力" })[reference.kind] ?? "引用";
|
|
2327
2327
|
}
|
|
2328
2328
|
|
|
2329
|
+
function escapeAiReferenceXmlText(value) {
|
|
2330
|
+
return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<");
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
function escapeAiReferenceXmlAttribute(value) {
|
|
2334
|
+
return escapeAiReferenceXmlText(value).replaceAll('"', """);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
function unescapeAiReferenceXmlText(value) {
|
|
2338
|
+
return String(value ?? "").replaceAll(""", '"').replaceAll("<", "<").replaceAll("&", "&");
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function serializeAiReference(reference) {
|
|
2342
|
+
return `<ai_reference kind="${escapeAiReferenceXmlAttribute(reference.kind)}" id="${escapeAiReferenceXmlAttribute(reference.id)}">${escapeAiReferenceXmlText(reference.name)}</ai_reference>`;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function parseAiReferenceMarkup(value) {
|
|
2346
|
+
const references = [];
|
|
2347
|
+
const pattern = /<ai_reference kind="(character|setting|chapter|context-settings)" id="([^"]+)">([\s\S]*?)<\/ai_reference>/gu;
|
|
2348
|
+
let text = "";
|
|
2349
|
+
let cursor = 0;
|
|
2350
|
+
for (const match of String(value ?? "").matchAll(pattern)) {
|
|
2351
|
+
const index = match.index ?? 0;
|
|
2352
|
+
const reference = {
|
|
2353
|
+
kind: match[1],
|
|
2354
|
+
id: unescapeAiReferenceXmlText(match[2]),
|
|
2355
|
+
name: unescapeAiReferenceXmlText(match[3])
|
|
2356
|
+
};
|
|
2357
|
+
const marker = `\uE000ai-reference-${references.length}\uE001`;
|
|
2358
|
+
text += `${String(value).slice(cursor, index)}${marker}`;
|
|
2359
|
+
references.push({ ...reference, marker });
|
|
2360
|
+
cursor = index + match[0].length;
|
|
2361
|
+
}
|
|
2362
|
+
return { text: `${text}${String(value ?? "").slice(cursor)}`, references };
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function aiReferenceByKey(kind, referenceId) {
|
|
2366
|
+
return state.aiReferences.find((reference) => reference.kind === kind && String(reference.id) === String(referenceId)) ?? null;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function aiPromptMarkupFromNode(node, root = node) {
|
|
2370
|
+
if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? "";
|
|
2371
|
+
if (!(node instanceof Element)) return "";
|
|
2372
|
+
if (node.matches("[data-ai-reference-key]")) {
|
|
2373
|
+
const [kind, ...idParts] = String(node.dataset.aiReferenceKey ?? "").split(":");
|
|
2374
|
+
const reference = aiReferenceByKey(kind, idParts.join(":"));
|
|
2375
|
+
return reference ? serializeAiReference(reference) : "";
|
|
2376
|
+
}
|
|
2377
|
+
if (node.tagName === "BR") return "\n";
|
|
2378
|
+
const text = [...node.childNodes].map((child) => aiPromptMarkupFromNode(child, root)).join("");
|
|
2379
|
+
return node !== root && ["DIV", "P"].includes(node.tagName) ? `${text}\n` : text;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
function aiPromptMarkup() {
|
|
2383
|
+
return aiPromptMarkupFromNode($("#ai-prompt")).replace(/\n$/u, "");
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
function setAiPromptMarkup(value) {
|
|
2387
|
+
const prompt = $("#ai-prompt");
|
|
2388
|
+
const { text, references } = parseAiReferenceMarkup(value);
|
|
2389
|
+
prompt.replaceChildren();
|
|
2390
|
+
let cursor = 0;
|
|
2391
|
+
for (const reference of references) {
|
|
2392
|
+
const markerIndex = text.indexOf(reference.marker, cursor);
|
|
2393
|
+
if (markerIndex < 0) continue;
|
|
2394
|
+
if (markerIndex > cursor) prompt.append(document.createTextNode(text.slice(cursor, markerIndex)));
|
|
2395
|
+
const currentReference = aiReferenceByKey(reference.kind, reference.id);
|
|
2396
|
+
if (currentReference) prompt.append(createAiReferenceChip(currentReference));
|
|
2397
|
+
else prompt.append(document.createTextNode(reference.name));
|
|
2398
|
+
cursor = markerIndex + reference.marker.length;
|
|
2399
|
+
}
|
|
2400
|
+
if (cursor < text.length) prompt.append(document.createTextNode(text.slice(cursor)));
|
|
2401
|
+
renderAiReferences();
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function aiMessageReferenceReadable(reference) {
|
|
2405
|
+
const module = ({
|
|
2406
|
+
character: "characters",
|
|
2407
|
+
setting: "settings",
|
|
2408
|
+
chapter: "prose",
|
|
2409
|
+
"context-settings": "settings"
|
|
2410
|
+
})[reference.kind];
|
|
2411
|
+
return !module || canReadModule(module);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
function createInlineUserMessageReference(reference) {
|
|
2415
|
+
const bubble = document.createElement("span");
|
|
2416
|
+
const kind = aiReferenceKindLabel(reference);
|
|
2417
|
+
const name = aiMessageReferenceReadable(reference) && reference.name.trim()
|
|
2418
|
+
? reference.name.trim()
|
|
2419
|
+
: "已隐藏引用";
|
|
2420
|
+
bubble.className = "user-message-mention user-message-inline-mention";
|
|
2421
|
+
bubble.textContent = `${kind} · ${name}`;
|
|
2422
|
+
bubble.title = `${kind}:${name}`;
|
|
2423
|
+
bubble.setAttribute("aria-label", `${kind}:${name}`);
|
|
2424
|
+
return bubble;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function replaceAiReferenceMarkers(host, references) {
|
|
2428
|
+
if (!host || references.length === 0) return;
|
|
2429
|
+
const markers = new Map(references.map((reference) => [reference.marker, reference]));
|
|
2430
|
+
const textNodes = [];
|
|
2431
|
+
const walker = document.createTreeWalker(host, NodeFilter.SHOW_TEXT);
|
|
2432
|
+
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
2433
|
+
for (const textNode of textNodes) {
|
|
2434
|
+
const value = textNode.textContent ?? "";
|
|
2435
|
+
const matches = [...markers.keys()].filter((marker) => value.includes(marker));
|
|
2436
|
+
if (matches.length === 0) continue;
|
|
2437
|
+
const fragment = document.createDocumentFragment();
|
|
2438
|
+
let cursor = 0;
|
|
2439
|
+
while (cursor < value.length) {
|
|
2440
|
+
const match = [...markers.keys()]
|
|
2441
|
+
.map((marker) => ({ marker, index: value.indexOf(marker, cursor) }))
|
|
2442
|
+
.filter((item) => item.index >= 0)
|
|
2443
|
+
.sort((left, right) => left.index - right.index)[0];
|
|
2444
|
+
if (!match) {
|
|
2445
|
+
fragment.append(document.createTextNode(value.slice(cursor)));
|
|
2446
|
+
break;
|
|
2447
|
+
}
|
|
2448
|
+
if (match.index > cursor) fragment.append(document.createTextNode(value.slice(cursor, match.index)));
|
|
2449
|
+
fragment.append(createInlineUserMessageReference(markers.get(match.marker)));
|
|
2450
|
+
cursor = match.index + match.marker.length;
|
|
2451
|
+
}
|
|
2452
|
+
textNode.replaceWith(fragment);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
function userMessageMentionEntries(ids, items) {
|
|
2457
|
+
if (!Array.isArray(ids) || !Array.isArray(items)) return [];
|
|
2458
|
+
const names = new Map(items.map((item) => [String(item?.id ?? ""), String(item?.name ?? "").trim()]));
|
|
2459
|
+
const seen = new Set();
|
|
2460
|
+
const entries = [];
|
|
2461
|
+
for (const id of ids) {
|
|
2462
|
+
const value = String(id ?? "");
|
|
2463
|
+
if (!value || seen.has(value)) continue;
|
|
2464
|
+
seen.add(value);
|
|
2465
|
+
const name = names.get(value);
|
|
2466
|
+
if (name) entries.push({ id: value, name });
|
|
2467
|
+
}
|
|
2468
|
+
return entries;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2329
2471
|
function createAiReferenceChip(reference) {
|
|
2330
2472
|
const chip = document.createElement("span");
|
|
2331
2473
|
chip.className = "ai-prompt-reference";
|
|
@@ -2399,7 +2541,7 @@ function createAiChatTabState(input = {}) {
|
|
|
2399
2541
|
roleplayUserCharacter: input.roleplayUserCharacter ?? null,
|
|
2400
2542
|
citations: input.citations ?? [],
|
|
2401
2543
|
references: input.references ?? [],
|
|
2402
|
-
composer: input.composer ?? { text: "", citations: [], references: [], images: [], semanticSnapshot: null, sceneDirection: "", scenePin: emptyRoleplayScenePin() },
|
|
2544
|
+
composer: input.composer ?? { text: "", markup: "", citations: [], references: [], images: [], semanticSnapshot: null, sceneDirection: "", scenePin: emptyRoleplayScenePin() },
|
|
2403
2545
|
contextUsage: input.contextUsage ?? null,
|
|
2404
2546
|
contextWarning: input.contextWarning === true,
|
|
2405
2547
|
lastMessageAt: input.lastMessageAt ?? null,
|
|
@@ -2439,6 +2581,7 @@ function setAiChatTabComposerSnapshot(tab, snapshot) {
|
|
|
2439
2581
|
tab.references = snapshot.references.map((reference) => ({ ...reference }));
|
|
2440
2582
|
tab.composer = {
|
|
2441
2583
|
text: snapshot.text,
|
|
2584
|
+
markup: snapshot.markup,
|
|
2442
2585
|
citations: tab.citations.map((citation) => ({ ...citation })),
|
|
2443
2586
|
references: tab.references.map((reference) => ({ ...reference })),
|
|
2444
2587
|
images: normalizeAiChatImageAttachments(snapshot.images),
|
|
@@ -2451,6 +2594,7 @@ function setAiChatTabComposerSnapshot(tab, snapshot) {
|
|
|
2451
2594
|
function clearAiChatTabComposer(tab) {
|
|
2452
2595
|
setAiChatTabComposerSnapshot(tab, {
|
|
2453
2596
|
text: "",
|
|
2597
|
+
markup: "",
|
|
2454
2598
|
citations: [],
|
|
2455
2599
|
references: [],
|
|
2456
2600
|
images: [],
|
|
@@ -2477,7 +2621,7 @@ function applyAiChatTabState(tab) {
|
|
|
2477
2621
|
const selectedModelId = tab.modelId ?? tab.selectedModelId;
|
|
2478
2622
|
if (selectedModelId && state.models.some((model) => model.id === selectedModelId)) $("#ai-model").value = selectedModelId;
|
|
2479
2623
|
syncAiModelPicker();
|
|
2480
|
-
|
|
2624
|
+
setAiPromptMarkup(tab.composer.markup ?? tab.composer.text);
|
|
2481
2625
|
restoreAiSceneComposer(tab.composer);
|
|
2482
2626
|
renderAiCitations();
|
|
2483
2627
|
renderAiSemanticInjection();
|
|
@@ -5008,11 +5152,14 @@ function aiModelSupportsImageInput() {
|
|
|
5008
5152
|
function syncAiImageAttachmentControl() {
|
|
5009
5153
|
const button = $("#ai-attachment-button");
|
|
5010
5154
|
if (!button) return;
|
|
5011
|
-
const
|
|
5012
|
-
|
|
5155
|
+
const model = activeAiModel();
|
|
5156
|
+
const enabled = model?.multimodalEnabled === true;
|
|
5013
5157
|
button.disabled = !enabled || aiInteractionBusy();
|
|
5014
|
-
button.
|
|
5015
|
-
|
|
5158
|
+
button.title = enabled
|
|
5159
|
+
? "添加图片附件"
|
|
5160
|
+
: model
|
|
5161
|
+
? "当前模型不支持图片输入"
|
|
5162
|
+
: "选择多模态模型后可添加图片附件";
|
|
5016
5163
|
}
|
|
5017
5164
|
|
|
5018
5165
|
function renderAiImageAttachments() {
|
|
@@ -5133,6 +5280,7 @@ function clearAiPromptComposer({ collapseScenePanel = false } = {}) {
|
|
|
5133
5280
|
function captureAiPromptComposer() {
|
|
5134
5281
|
return {
|
|
5135
5282
|
text: aiPromptText(),
|
|
5283
|
+
markup: aiPromptMarkup(),
|
|
5136
5284
|
citations: state.aiCitations.map((citation) => ({ ...citation })),
|
|
5137
5285
|
references: state.aiReferences.map((reference) => ({ ...reference })),
|
|
5138
5286
|
images: normalizeAiChatImageAttachments(state.aiImageAttachments),
|
|
@@ -5147,7 +5295,7 @@ function restoreAiPromptComposer(snapshot) {
|
|
|
5147
5295
|
state.aiReferences = snapshot.references.map((reference) => ({ ...reference }));
|
|
5148
5296
|
state.aiImageAttachments = normalizeAiChatImageAttachments(snapshot.images);
|
|
5149
5297
|
state.aiSemanticSnapshot = snapshot.semanticSnapshot ? structuredClone(snapshot.semanticSnapshot) : null;
|
|
5150
|
-
|
|
5298
|
+
setAiPromptMarkup(snapshot.markup ?? snapshot.text);
|
|
5151
5299
|
restoreAiSceneComposer(snapshot);
|
|
5152
5300
|
renderAiCitations();
|
|
5153
5301
|
renderAiImageAttachments();
|
|
@@ -18266,6 +18414,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18266
18414
|
const requestComposerSnapshot = retry
|
|
18267
18415
|
? {
|
|
18268
18416
|
text: retry.prompt,
|
|
18417
|
+
markup: retry.prompt,
|
|
18269
18418
|
citations: retry.citations ?? [],
|
|
18270
18419
|
references: [],
|
|
18271
18420
|
images: retry.images ?? [],
|
|
@@ -18273,14 +18422,15 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18273
18422
|
scenePin: captureAiScenePin()
|
|
18274
18423
|
}
|
|
18275
18424
|
: composerSnapshot;
|
|
18276
|
-
const instruction = requestComposerSnapshot.text.trim();
|
|
18425
|
+
const instruction = String(requestComposerSnapshot.markup ?? requestComposerSnapshot.text).trim();
|
|
18426
|
+
const instructionText = String(requestComposerSnapshot.text ?? "").trim();
|
|
18277
18427
|
const sceneDirection = $("#ai-task").value === "roleplay"
|
|
18278
18428
|
? String(requestComposerSnapshot.sceneDirection ?? "").trim()
|
|
18279
18429
|
: "";
|
|
18280
18430
|
const scenePin = $("#ai-task").value === "roleplay"
|
|
18281
18431
|
? normalizeRoleplayScenePin(requestComposerSnapshot.scenePin)
|
|
18282
18432
|
: emptyRoleplayScenePin();
|
|
18283
|
-
if (!
|
|
18433
|
+
if (!instructionText && !sceneDirection) {
|
|
18284
18434
|
return toast($("#ai-task").value === "roleplay" ? "请输入台词或场景旁白" : "请输入指令", "error");
|
|
18285
18435
|
}
|
|
18286
18436
|
if ($("#ai-task").value === "roleplay" && !state.aiRoleplayCharacter) return toast("请先选择角色卡", "error");
|
|
@@ -18334,7 +18484,6 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18334
18484
|
let assistantMessage;
|
|
18335
18485
|
let assistantMetadata = {};
|
|
18336
18486
|
let persistedStreamMessage = null;
|
|
18337
|
-
let writingSuggestion = null;
|
|
18338
18487
|
const streamed = await streamChat(requestHolder, aiRetryStreamRequestBody({
|
|
18339
18488
|
instruction,
|
|
18340
18489
|
...(sceneDirection ? { sceneDirection } : {}),
|
|
@@ -18351,7 +18500,6 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18351
18500
|
assistantContent = streamed.content;
|
|
18352
18501
|
assistantMessage = streamed.message;
|
|
18353
18502
|
assistantMetadata = streamed.metadata;
|
|
18354
|
-
writingSuggestion = streamed.writingSuggestion;
|
|
18355
18503
|
persistedStreamMessage = streamed.messageId ? { id: streamed.messageId, createdAt: streamed.createdAt } : null;
|
|
18356
18504
|
applyAiConversationTitle(streamed.conversationTitle, streamedRequest.conversationId);
|
|
18357
18505
|
try {
|
|
@@ -18381,7 +18529,6 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18381
18529
|
attachMessageIdentity(assistantMessage, persistedAssistantMessage.id);
|
|
18382
18530
|
}
|
|
18383
18531
|
}
|
|
18384
|
-
if (writingSuggestion && assistantMessage) attachWritingSuggestion(assistantMessage, writingSuggestion, { tab });
|
|
18385
18532
|
} catch (error) {
|
|
18386
18533
|
if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
|
|
18387
18534
|
setAiChatTabStatus(tab, "error");
|
|
@@ -18552,7 +18699,6 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18552
18699
|
let persistedMessageId = null;
|
|
18553
18700
|
let persistedMessageCreatedAt = null;
|
|
18554
18701
|
let conversationTitle = null;
|
|
18555
|
-
let writingSuggestion = null;
|
|
18556
18702
|
let question = null;
|
|
18557
18703
|
let persistedUserMessage = null;
|
|
18558
18704
|
let contextAction = "ready";
|
|
@@ -18731,13 +18877,6 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18731
18877
|
persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
|
|
18732
18878
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
18733
18879
|
conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
|
|
18734
|
-
writingSuggestion = payload.writingSuggestion && typeof payload.writingSuggestion === "object"
|
|
18735
|
-
? payload.writingSuggestion
|
|
18736
|
-
: null;
|
|
18737
|
-
const writingSuggestionFailed = writingSuggestion?.guard?.status === "failed"
|
|
18738
|
-
|| writingSuggestion?.toolCalls?.some((toolCall) => toolCall.status === "failed")
|
|
18739
|
-
|| writingSuggestion?.processSteps?.some((step) => step?.toolCall?.status === "failed");
|
|
18740
|
-
if (writingSuggestionFailed) setAiChatTabStatus(tab, "error");
|
|
18741
18880
|
const announcedCompaction = contextAction === "compacted" || streamContextCompacted;
|
|
18742
18881
|
setAiChatTabContextUsage(tab, attachAiContextCacheHitPercent(payload.contextUsage, payload.cacheHitPercent), announcedCompaction);
|
|
18743
18882
|
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
@@ -18757,10 +18896,6 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18757
18896
|
toolCalls,
|
|
18758
18897
|
processSteps,
|
|
18759
18898
|
processDurationMs,
|
|
18760
|
-
...(writingSuggestion ? {
|
|
18761
|
-
activeSkills: [writingSuggestion.taskType === "continue" ? "continue-writing" : "polish-writing"],
|
|
18762
|
-
writingSuggestionId: writingSuggestion.id
|
|
18763
|
-
} : {})
|
|
18764
18899
|
};
|
|
18765
18900
|
renderStreamingProcessSteps(true, processDurationMs);
|
|
18766
18901
|
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent, "", processDurationMs);
|
|
@@ -18778,7 +18913,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18778
18913
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18779
18914
|
if (streamError) throw streamError;
|
|
18780
18915
|
assertAiStreamCompleted(streamCompleted);
|
|
18781
|
-
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle,
|
|
18916
|
+
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage, question };
|
|
18782
18917
|
} catch (error) {
|
|
18783
18918
|
const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
|
|
18784
18919
|
const interruptionCode = typeof streamFailure.code === "string" ? streamFailure.code.slice(0, 100) : "AI_STREAM_FAILED";
|
|
@@ -18868,10 +19003,14 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
18868
19003
|
if (errorCode) message.dataset.errorCode = errorCode;
|
|
18869
19004
|
if (isFailure && typeof metadata?.pendingQuestionId === "string") message.dataset.pendingQuestionId = metadata.pendingQuestionId;
|
|
18870
19005
|
const parsedUserTurn = role === "user" ? parseRoleplayUserTurn(text) : null;
|
|
19006
|
+
const userMessageContent = parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text;
|
|
19007
|
+
const inlineReferences = role === "user" ? parseAiReferenceMarkup(userMessageContent).references : [];
|
|
19008
|
+
const renderedUserMessage = role === "user" ? parseAiReferenceMarkup(userMessageContent).text : userMessageContent;
|
|
18871
19009
|
const messageBody = isFailure
|
|
18872
19010
|
? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
|
|
18873
|
-
: renderMarkdown(
|
|
19011
|
+
: renderMarkdown(renderedUserMessage);
|
|
18874
19012
|
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
19013
|
+
if (role === "user") replaceAiReferenceMarkers(message.querySelector(".message-body"), inlineReferences);
|
|
18875
19014
|
message.querySelector("[data-ai-tool-call-settings-link]")?.addEventListener("click", (event) => {
|
|
18876
19015
|
event.preventDefault();
|
|
18877
19016
|
openAiToolCallSettings().catch((error) => toast(`打开 AI 设置失败:${error.message}`, "error"));
|
|
@@ -18910,16 +19049,19 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
18910
19049
|
}))) ?? [];
|
|
18911
19050
|
const settingReferences = state.settings.map((setting) => ({ id: setting.id, name: setting.title }));
|
|
18912
19051
|
const contextSettingReferences = [{ id: "include-setting-info", name: "注入上下文设定" }];
|
|
19052
|
+
const inlineReferenceKeys = new Set(inlineReferences.map((reference) => aiReferenceKey(reference)));
|
|
18913
19053
|
const mentionGroups = role === "user"
|
|
18914
19054
|
? [
|
|
18915
|
-
["角色", metadata?.mentionCharacterIds, state.characters],
|
|
18916
|
-
["种族", metadata?.mentionRaceIds, state.races],
|
|
18917
|
-
["组织", metadata?.mentionOrganizationIds, state.organizations],
|
|
18918
|
-
["设定", metadata?.mentionSettingIds, settingReferences],
|
|
18919
|
-
["章节", metadata?.mentionChapterIds, chapterReferences],
|
|
18920
|
-
["能力", metadata?.mentionContextSettingIds, contextSettingReferences]
|
|
19055
|
+
["character", "角色", metadata?.mentionCharacterIds, state.characters],
|
|
19056
|
+
["race", "种族", metadata?.mentionRaceIds, state.races],
|
|
19057
|
+
["organization", "组织", metadata?.mentionOrganizationIds, state.organizations],
|
|
19058
|
+
["setting", "设定", metadata?.mentionSettingIds, settingReferences],
|
|
19059
|
+
["chapter", "章节", metadata?.mentionChapterIds, chapterReferences],
|
|
19060
|
+
["context-settings", "能力", metadata?.mentionContextSettingIds, contextSettingReferences]
|
|
18921
19061
|
]
|
|
18922
|
-
.flatMap(([kind, ids, items]) =>
|
|
19062
|
+
.flatMap(([referenceKind, kind, ids, items]) => userMessageMentionEntries(ids, items)
|
|
19063
|
+
.filter((reference) => !inlineReferenceKeys.has(`${referenceKind}:${reference.id}`))
|
|
19064
|
+
.map((reference) => ({ kind, name: reference.name })))
|
|
18923
19065
|
: [];
|
|
18924
19066
|
if (mentionGroups.length) {
|
|
18925
19067
|
const references = document.createElement("div");
|
|
@@ -19000,109 +19142,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
19000
19142
|
if (isFailure) renderMessageCardActions(message);
|
|
19001
19143
|
attachMessageIdentity(message, messageId);
|
|
19002
19144
|
feed.append(message);
|
|
19003
|
-
const writingSuggestionId = role === "assistant" && typeof metadata?.writingSuggestionId === "string"
|
|
19004
|
-
? metadata.writingSuggestionId
|
|
19005
|
-
: "";
|
|
19006
|
-
if (writingSuggestionId && !isFailure && !isInterrupted) {
|
|
19007
|
-
api(`/api/suggestions/${encodeURIComponent(writingSuggestionId)}`)
|
|
19008
|
-
.then((suggestion) => attachWritingSuggestion(message, suggestion, { tab }))
|
|
19009
|
-
.catch(() => undefined);
|
|
19010
|
-
}
|
|
19011
19145
|
scrollAiFeedToBottom(feed);
|
|
19012
19146
|
return message;
|
|
19013
19147
|
}
|
|
19014
19148
|
|
|
19015
|
-
function continuationGuardMarkup(guard) {
|
|
19016
|
-
if (!guard) return "";
|
|
19017
|
-
const issues = Array.isArray(guard.issues) ? guard.issues : [];
|
|
19018
|
-
const failure = typeof guard.failure === "string" && guard.failure.trim()
|
|
19019
|
-
? guard.failure.trim()
|
|
19020
|
-
: "无法完成检查,请谨慎采纳";
|
|
19021
|
-
return `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<details class="guard-failure-details"><summary>查看失败原因</summary><p>${esc(failure)}</p></details>` : issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>`;
|
|
19022
|
-
}
|
|
19023
|
-
|
|
19024
|
-
async function applyAcceptedWritingSuggestion(message, suggestion) {
|
|
19025
|
-
if (state.work?.id === suggestion.workId && state.chapter?.id === suggestion.chapterId
|
|
19026
|
-
&& (state.dirty || chapterSaveInFlight || chapterSaveGuardInFlight)) {
|
|
19027
|
-
throw new Error("当前章节有未保存修改或正在保存,请先完成保存,再重新生成正文建议");
|
|
19028
|
-
}
|
|
19029
|
-
const result = await api(`/api/suggestions/${encodeURIComponent(suggestion.id)}/accept`, { method: "POST", body: {} });
|
|
19030
|
-
const workId = result.chapter.workId;
|
|
19031
|
-
if (state.work?.id === workId && state.chapter?.id === result.chapter.id
|
|
19032
|
-
&& !state.dirty && !chapterSaveInFlight && !chapterSaveGuardInFlight) {
|
|
19033
|
-
cancelChapterAutoSave();
|
|
19034
|
-
state.chapter = result.chapter;
|
|
19035
|
-
resetChapterDraftLineIds(state.chapter);
|
|
19036
|
-
lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
|
|
19037
|
-
$("#chapter-title").value = state.chapter.title;
|
|
19038
|
-
$("#chapter-content").value = state.chapter.content;
|
|
19039
|
-
scheduleChapterLineNumbers();
|
|
19040
|
-
updateChapterStats();
|
|
19041
|
-
}
|
|
19042
|
-
if (state.work?.id === workId) {
|
|
19043
|
-
const work = await api(`/api/works/${workId}`);
|
|
19044
|
-
if (state.work?.id === workId) {
|
|
19045
|
-
state.work = work;
|
|
19046
|
-
renderTree();
|
|
19047
|
-
}
|
|
19048
|
-
}
|
|
19049
|
-
message.querySelector("[data-writing-suggestion-actions]").innerHTML = "<span>已采纳并生成新版本</span>";
|
|
19050
|
-
toast("AI 建议已采纳,正文已生成新版本");
|
|
19051
|
-
}
|
|
19052
|
-
|
|
19053
|
-
function attachWritingSuggestion(message, suggestion, options = {}) {
|
|
19054
|
-
if (!suggestion || suggestion.action === "note" || !suggestion.id) return message;
|
|
19055
|
-
const suggestionId = String(suggestion.id);
|
|
19056
|
-
if (message.dataset.writingSuggestionId === suggestionId) return message;
|
|
19057
|
-
message.dataset.writingSuggestionId = suggestionId;
|
|
19058
|
-
message.querySelector("[data-writing-suggestion-ui]")?.remove();
|
|
19059
|
-
const heading = message.querySelector(".message-heading > span");
|
|
19060
|
-
if (heading) heading.textContent = "助手建议";
|
|
19061
|
-
const host = document.createElement("div");
|
|
19062
|
-
host.dataset.writingSuggestionUi = "";
|
|
19063
|
-
host.className = "writing-suggestion-ui";
|
|
19064
|
-
host.innerHTML = `${continuationGuardMarkup(suggestion.guard)}<div class="message-actions" data-writing-suggestion-actions></div>`;
|
|
19065
|
-
const actions = host.querySelector("[data-writing-suggestion-actions]");
|
|
19066
|
-
if (suggestion.status === "accepted") {
|
|
19067
|
-
actions.innerHTML = "<span>已采纳并生成新版本</span>";
|
|
19068
|
-
} else if (suggestion.status === "rejected") {
|
|
19069
|
-
actions.innerHTML = "<span>已拒绝</span>";
|
|
19070
|
-
} else {
|
|
19071
|
-
actions.innerHTML = '<button type="button" data-action="accept">采纳到正文</button><button type="button" data-action="reject">拒绝</button>';
|
|
19072
|
-
actions.querySelector('[data-action="accept"]').addEventListener("click", async () => {
|
|
19073
|
-
try {
|
|
19074
|
-
await applyAcceptedWritingSuggestion(message, suggestion);
|
|
19075
|
-
} catch (error) {
|
|
19076
|
-
toast(error.message, "error");
|
|
19077
|
-
}
|
|
19078
|
-
});
|
|
19079
|
-
actions.querySelector('[data-action="reject"]').addEventListener("click", async () => {
|
|
19080
|
-
try {
|
|
19081
|
-
await api(`/api/suggestions/${encodeURIComponent(suggestion.id)}/reject`, { method: "POST", body: {} });
|
|
19082
|
-
actions.innerHTML = "<span>已拒绝</span>";
|
|
19083
|
-
} catch (error) {
|
|
19084
|
-
toast(error.message, "error");
|
|
19085
|
-
}
|
|
19086
|
-
});
|
|
19087
|
-
}
|
|
19088
|
-
message.append(host);
|
|
19089
|
-
const tab = options.tab ?? activeAiChatTab();
|
|
19090
|
-
scrollAiFeedToBottom(options.feed ?? tab?.feed ?? $("#ai-feed"));
|
|
19091
|
-
return message;
|
|
19092
|
-
}
|
|
19093
|
-
|
|
19094
|
-
function appendSuggestion(suggestion, createdAt = null, messageId = null, options = {}) {
|
|
19095
|
-
const tab = options.tab ?? activeAiChatTab();
|
|
19096
|
-
const feed = options.feed ?? tab?.feed ?? $("#ai-feed");
|
|
19097
|
-
const message = appendMessage("assistant", suggestion.content, [], createdAt, {
|
|
19098
|
-
modelDisplayName: suggestion.model?.displayName,
|
|
19099
|
-
outputTokens: suggestion.outputTokens,
|
|
19100
|
-
cacheHitPercent: suggestion.cacheHitPercent,
|
|
19101
|
-
processDurationMs: suggestion.processDurationMs
|
|
19102
|
-
}, messageId, { tab, feed });
|
|
19103
|
-
return attachWritingSuggestion(message, suggestion, { tab, feed });
|
|
19104
|
-
}
|
|
19105
|
-
|
|
19106
19149
|
function chapterVersionCompareOption(version) {
|
|
19107
19150
|
return `<option value="version:${Number(version.versionNo)}">v${Number(version.versionNo)} · ${esc(chapterVersionSourceLabel(version.source))}</option>`;
|
|
19108
19151
|
}
|
package/dist/public/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<script src="/theme-init.js?v=20260827-reader-prefetch-v1"></script>
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
|
-
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=ai-composer-compact-controls-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=
|
|
12
|
+
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=ai-composer-compact-controls-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=entity-editor-back-icon-v1&feature=toast-stack-v1&feature=chapter-header-compact-v4&feature=ai-attachment-disabled-v1&feature=ai-prose-acceptance-removed-v1">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="auth-pending">
|
|
15
15
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -433,7 +433,7 @@
|
|
|
433
433
|
<label for="ai-scene-direction">旁白 / 场景<textarea id="ai-scene-direction" rows="3" maxlength="20000" placeholder="写下本轮旁白或场景推进,会随下一条台词一起发送"></textarea></label>
|
|
434
434
|
</section>
|
|
435
435
|
<div class="prompt-composer-leading">
|
|
436
|
-
<button id="ai-attachment-button" class="ai-attachment-button
|
|
436
|
+
<button id="ai-attachment-button" class="ai-attachment-button" type="button" aria-label="添加图片附件" title="选择多模态模型后可添加图片附件" disabled><svg class="ai-image-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 5h6"></path><path d="M19 2v6"></path><path d="M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5"></path><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path><circle cx="9" cy="9" r="2"></circle></svg></button>
|
|
437
437
|
<button id="ai-scene-button" class="ai-scene-button hidden" type="button" aria-label="旁白与场景" title="旁白与场景" aria-expanded="false" aria-controls="ai-scene-panel"><svg class="ai-scene-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4.5" y="6.5" width="15" height="13" rx="1.5"></rect><path d="M8 10.5h8M8 14h5.5"></path></svg></button>
|
|
438
438
|
</div>
|
|
439
439
|
<input id="ai-attachment-input" class="ai-attachment-input" type="file" accept="image/png,image/jpeg,.jpg,.jpeg" multiple aria-label="选择图片附件">
|
|
@@ -1427,7 +1427,7 @@
|
|
|
1427
1427
|
</dialog>
|
|
1428
1428
|
|
|
1429
1429
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1430
|
-
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=
|
|
1430
|
+
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=setting-category-preservation-v1&feature=toast-stack-v1&feature=toast-stack-label-v2&feature=agent-tool-limits-300-v1&feature=server-dev-logo-v1&feature=chapter-directory-ai-reference-v3&feature=ai-error-origin-v1&feature=ai-stream-render-performance-v2&feature=ai-attachment-disabled-v1&feature=ai-prose-acceptance-removed-v1&feature=ai-inline-message-references-v1"></script>
|
|
1431
1431
|
|
|
1432
1432
|
</body>
|
|
1433
1433
|
</html>
|