@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.
@@ -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, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
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("&", "&amp;").replaceAll("<", "&lt;");
2331
+ }
2332
+
2333
+ function escapeAiReferenceXmlAttribute(value) {
2334
+ return escapeAiReferenceXmlText(value).replaceAll('"', "&quot;");
2335
+ }
2336
+
2337
+ function unescapeAiReferenceXmlText(value) {
2338
+ return String(value ?? "").replaceAll("&quot;", '"').replaceAll("&lt;", "<").replaceAll("&amp;", "&");
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
- setAiPromptText(tab.composer.text);
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 enabled = aiModelSupportsImageInput();
5012
- button.classList.toggle("hidden", !enabled);
5155
+ const model = activeAiModel();
5156
+ const enabled = model?.multimodalEnabled === true;
5013
5157
  button.disabled = !enabled || aiInteractionBusy();
5014
- button.setAttribute("aria-hidden", String(!enabled));
5015
- button.title = enabled ? "添加图片附件" : "当前模型不支持图片输入";
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
- setAiPromptText(snapshot.text);
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 (!instruction && !sceneDirection) {
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, writingSuggestion, userMessage: persistedUserMessage, question };
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(parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
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]) => userMessageMentionNames(ids, items).map((name) => ({ kind, name })))
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
  }
@@ -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=continuation-guard-failure-details-v1&feature=entity-editor-back-icon-v1&feature=toast-stack-v1">
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 hidden" type="button" aria-label="添加图片附件" title="添加图片附件"><svg class="ai-image-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="4.5" width="17" height="15" rx="2"></rect><circle cx="9" cy="10" r="1.5"></circle><path d="m5.5 17 4.4-4.4 3.3 3.1 2.2-2.2 3.1 3.5"></path></svg></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=continuation-guard-failure-details-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=ai-suggestion-editor-boundary-v1&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"></script>
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>