@musnows/scriverse 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1";
2
2
  import { formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
3
+ import { countProseWords } from "/text-count.js?v=20260906-chapter-word-count-consistency-v1";
3
4
  import { renderMarkdown } from "/markdown.js?v=20260830-adjacent-blockquotes-v1";
4
5
  import { createImWorkspace } from "/im.js?v=20260904-im-judge-outcomes-v106";
5
6
  import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
@@ -14,7 +15,7 @@ import {
14
15
  } from "/roleplay-turn.js?v=20260823-ai-roleplay-scene-turn-v2";
15
16
  import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
16
17
  import { createAiChatTabManager, normalizeAiChatTabLimit } from "/ai-chat-tabs.js?v=20260816-ai-chat-switcher-v2";
17
- import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManager, isAiRequestCancellation } from "/ai-request-manager.js?v=20260816-ai-chat-tabs-v1";
18
+ import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManager, isAiRequestCancellation } from "/ai-request-manager.js?v=20260905-question-stream-v2";
18
19
  import { calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
19
20
  import { buildChapterLineMirror, findChapterLineWindow } from "/chapter-editor-virtualization.js?v=20260810-visible-lines-v1";
20
21
  import { CHAPTER_PARAGRAPH_INDENT, calculateChapterCaretScroll, chapterLineIndexAtOffset, insertIndentedParagraph } from "/chapter-editor-behavior.js?v=20260828-centered-scroll-v1";
@@ -31,7 +32,7 @@ import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_
31
32
  import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260822-private-ai-endpoint-hint-v1";
32
33
  import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
33
34
  import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
34
- import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260818-ai-agent-turn-process-v1";
35
+ import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260906-background-stream-v2";
35
36
  import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
36
37
  import { buildUsageCalendar, formatCacheHitRate, formatEstimatedCost, formatTokenCount, usageCalendarYears } from "/ai-usage.js?v=20260830-ai-usage-year-v1";
37
38
  import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
@@ -665,8 +666,8 @@ function aiSendButtonIconMarkup(stateName) {
665
666
 
666
667
  function syncAiRequestControls() {
667
668
  const activeTabId = activeAiChatTab()?.id;
668
- const sending = aiRequestManager.hasActive(activeTabId);
669
669
  const continuingQuestion = activeTabId ? aiQuestionContinuationTabIds.has(activeTabId) : false;
670
+ const sending = aiRequestManager.hasActive(activeTabId) && !continuingQuestion;
670
671
  const switching = aiConversationNavigationPending !== null;
671
672
  const button = $("#ai-send");
672
673
  const stateName = sending ? "stop" : (switching || continuingQuestion) ? "switching" : "send";
@@ -2952,6 +2953,7 @@ function renderMessageCardActions(message) {
2952
2953
  return;
2953
2954
  }
2954
2955
  fork.disabled = true;
2956
+ const dismissForkingToast = persistentToast("正在创建分支对话…");
2955
2957
  try {
2956
2958
  const sourceTab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
2957
2959
  if (!sourceTab?.conversationId) throw new Error("无法确定消息所属对话");
@@ -2965,6 +2967,8 @@ function renderMessageCardActions(message) {
2965
2967
  } catch (error) {
2966
2968
  fork.disabled = false;
2967
2969
  toast(error.message, "error");
2970
+ } finally {
2971
+ dismissForkingToast();
2968
2972
  }
2969
2973
  });
2970
2974
  actions.append(fork);
@@ -3532,18 +3536,23 @@ async function openAiUserQuestionDialog(questionId) {
3532
3536
  }
3533
3537
  }
3534
3538
 
3535
- function beginAiQuestionContinuationUi(conversationId) {
3539
+ async function beginAiQuestionContinuationUi(conversationId) {
3536
3540
  const tab = conversationId ? aiChatTabManager.findByConversation(conversationId) : null;
3537
3541
  if (!tab) return null;
3542
+ const workId = state.work.id;
3543
+ await aiRequestManager.whenIdle(tab.id);
3544
+ if (state.work?.id !== workId || !aiChatTabManager.get(tab.id)) throw createAiRequestAbortError("AI 请求目标已切换");
3545
+ const requestHolder = { snapshot: aiRequestManager.begin({ tabId: tab.id, workId, conversationId }) };
3538
3546
  aiQuestionContinuationTabIds.add(tab.id);
3539
3547
  setAiChatTabStatus(tab, "streaming");
3540
3548
  if (isActiveAiChatTab(tab)) syncAiRequestControls();
3541
3549
  scrollAiFeedToBottom(tab.feed);
3542
- return { tab };
3550
+ return { tab, requestHolder };
3543
3551
  }
3544
3552
 
3545
3553
  function finishAiQuestionContinuationUi(continuationUi, failed = false) {
3546
3554
  if (!continuationUi) return;
3555
+ aiRequestManager.finish(continuationUi.requestHolder.snapshot);
3547
3556
  aiQuestionContinuationTabIds.delete(continuationUi.tab.id);
3548
3557
  if (aiChatTabManager.get(continuationUi.tab.id)) setAiChatTabStatus(continuationUi.tab, failed ? "error" : "ready");
3549
3558
  if (isActiveAiChatTab(continuationUi.tab)) syncAiRequestControls();
@@ -3557,7 +3566,7 @@ async function reloadAiQuestionConversation(conversationId) {
3557
3566
  if (String(conversation.workId ?? "") !== String(state.work?.id ?? "")) throw new Error("AI 对话不属于当前作品");
3558
3567
  upsertAiConversationSummary(conversation);
3559
3568
  applyConversationToAiChatTab(tab, conversation);
3560
- activateAiChatTab(tab.id, { persistCurrent: false, force: true });
3569
+ if (isActiveAiChatTab(tab)) activateAiChatTab(tab.id, { persistCurrent: false, force: true });
3561
3570
  return conversation;
3562
3571
  }
3563
3572
 
@@ -3577,9 +3586,13 @@ async function respondAiUserQuestion(questionId, payload) {
3577
3586
  const conversationId = typeof knownQuestion?.conversationId === "string" ? knownQuestion.conversationId : null;
3578
3587
  if (questionDialog.open) questionDialog.close();
3579
3588
  if (approvalCenterDialog.open) approvalCenterDialog.close();
3580
- continuationUi = beginAiQuestionContinuationUi(conversationId);
3589
+ continuationUi = await beginAiQuestionContinuationUi(conversationId);
3581
3590
  let question;
3582
- if (payload.action === "reject") {
3591
+ if (continuationUi) {
3592
+ const endpoint = questionsEndpoint(`/${encodeURIComponent(String(questionId))}/${payload.action === "reject" ? "reject" : "answer"}`);
3593
+ const streamed = await streamChat(continuationUi.requestHolder, payload.action === "reject" ? {} : { answers: payload.answers }, createAiIdempotencyKey(), { endpoint });
3594
+ question = streamed.question;
3595
+ } else if (payload.action === "reject") {
3583
3596
  question = await api(questionsEndpoint(`/${encodeURIComponent(String(questionId))}/reject`), { method: "POST" });
3584
3597
  } else {
3585
3598
  question = await api(questionsEndpoint(`/${encodeURIComponent(String(questionId))}/answer`), {
@@ -3588,8 +3601,7 @@ async function respondAiUserQuestion(questionId, payload) {
3588
3601
  });
3589
3602
  }
3590
3603
  cacheAiQuestionView(question);
3591
- currentAiQuestionDialogView = question;
3592
- aiQuestionDialogQuestionId = String(question.id);
3604
+ if (aiQuestionDialogQuestionId === String(question.id)) currentAiQuestionDialogView = question;
3593
3605
  await reloadAiQuestionConversation(question.conversationId ?? conversationId);
3594
3606
  toast(payload.action === "reject" ? "已跳过该提问批次,AI 已继续处理" : "全部回答已提交,AI 已继续处理");
3595
3607
  return question;
@@ -3609,6 +3621,7 @@ async function respondAiUserQuestion(questionId, payload) {
3609
3621
  } finally {
3610
3622
  finishAiQuestionContinuationUi(continuationUi, continuationFailed);
3611
3623
  aiQuestionDialogBusy = false;
3624
+ if (questionDialog.open && currentAiQuestionDialogView?.status === "pending") renderAiUserQuestionOptions(currentAiQuestionDialogView);
3612
3625
  }
3613
3626
  }
3614
3627
 
@@ -9293,8 +9306,8 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
9293
9306
  function updateChapterStats() {
9294
9307
  if (!state.chapter) return;
9295
9308
  const text = $("#chapter-content").value;
9296
- const count = Array.from(text.replace(/\s/g, "")).length;
9297
- $("#chapter-stats").textContent = `${count} 字 · v${state.chapter.versionNo}`;
9309
+ const count = countProseWords(text);
9310
+ $("#chapter-stats").textContent = `${count.toLocaleString("zh-CN")} 字 · v${state.chapter.versionNo}`;
9298
9311
  }
9299
9312
 
9300
9313
  function readReadingStorage(key) {
@@ -9825,7 +9838,7 @@ const moduleMeta = {
9825
9838
  comments: ["正文协作", "正文评论与待办", "集中查看并处理当前作品所有章节的评论与待办。", ""],
9826
9839
  reviews: ["作者决策", "审核队列", "集中处理冲突、候选设定、低置信度关系和时间问题。", "新增审核项"],
9827
9840
  tasks: ["AI 深度分析", "AI 分析中心", "对全书或指定章节运行人物关系、世界观、设定、事件与一致性分析。", "开始 AI 分析"],
9828
- "ai-settings": ["书籍提示词", "本书 AI 设置", "本书系统提示词会追加在内置提示词和平台全局提示词之后;任务默认模型只作用于当前作品。", "保存设置"]
9841
+ "ai-settings": ["书籍提示词", "本书 AI 设置", "本书系统提示词默认追加在内置提示词和平台全局提示词之后;可在折叠的高级设置中改为彻底覆写。任务默认模型只作用于当前作品。", "保存设置"]
9829
9842
  };
9830
9843
 
9831
9844
  async function showModule(module) {
@@ -13423,6 +13436,53 @@ function startBackgroundTaskCenter(workId) {
13423
13436
  void refreshBackgroundTaskCenter();
13424
13437
  }
13425
13438
 
13439
+ function systemPromptOverrideDetails({ inputId, checked, scope }) {
13440
+ const platformScope = scope === "platform";
13441
+ const consequence = platformScope
13442
+ ? "保存后,普通 AI 请求只会把上方平台提示词作为 system 消息;Scriverse 内置规则、本书提示词、工具指引和时间信息都不会发送。"
13443
+ : "保存后,本书的普通 AI 请求只会把上方本书提示词作为 system 消息;Scriverse 内置规则、平台提示词、工具指引和时间信息都不会发送。";
13444
+ return `<details class="system-prompt-override-settings${checked ? " is-enabled" : ""}" ${checked ? "open" : ""}><summary><span>高级设置</span><strong data-system-prompt-override-state>${checked ? "已选择覆写" : "默认追加"}</strong></summary><div class="system-prompt-override-body"><label class="checkbox-field config-checkbox-field"><input id="${inputId}" type="checkbox" ${checked ? "checked" : ""} aria-describedby="${inputId}-warning">彻底覆写系统提示词</label><div id="${inputId}-warning" class="system-prompt-override-warning" role="alert"><strong>高风险设置</strong><p>${consequence} 空提示词会让请求不携带 system 消息,可能导致上下文、工具和安全约束失效。</p></div></div></details>`;
13445
+ }
13446
+
13447
+ function syncSystemPromptOverridePresentation(input) {
13448
+ const details = input.closest(".system-prompt-override-settings");
13449
+ details?.classList.toggle("is-enabled", input.checked);
13450
+ const stateLabel = details?.querySelector("[data-system-prompt-override-state]");
13451
+ if (stateLabel) stateLabel.textContent = input.checked ? "已选择覆写" : "默认追加";
13452
+ }
13453
+
13454
+ const systemPromptOverrideConfirmationByInput = new WeakMap();
13455
+
13456
+ async function confirmSystemPromptOverride(input, scopeLabel) {
13457
+ if (!input.checked) {
13458
+ syncSystemPromptOverridePresentation(input);
13459
+ return;
13460
+ }
13461
+ const pendingConfirmation = systemPromptOverrideConfirmationByInput.get(input);
13462
+ if (pendingConfirmation) return pendingConfirmation;
13463
+ input.disabled = true;
13464
+ const confirmation = (async () => {
13465
+ const confirmed = await confirmToast(
13466
+ `开启后,${scopeLabel}提示词将彻底替换 Scriverse 内置系统提示词,模型不会收到内置规则、工具指引和其他追加提示。错误配置可能导致功能异常或安全约束失效,确认继续吗?`,
13467
+ { title: "确认开启系统提示词覆写", confirmLabel: "确认开启" }
13468
+ );
13469
+ if (!confirmed) input.checked = false;
13470
+ syncSystemPromptOverridePresentation(input);
13471
+ })().finally(() => {
13472
+ input.disabled = false;
13473
+ if (systemPromptOverrideConfirmationByInput.get(input) === confirmation) {
13474
+ systemPromptOverrideConfirmationByInput.delete(input);
13475
+ }
13476
+ });
13477
+ systemPromptOverrideConfirmationByInput.set(input, confirmation);
13478
+ return confirmation;
13479
+ }
13480
+
13481
+ async function waitForSystemPromptOverrideConfirmation(input) {
13482
+ const confirmation = systemPromptOverrideConfirmationByInput.get(input);
13483
+ if (confirmation) await confirmation;
13484
+ }
13485
+
13426
13486
  async function renderPlatformAiConfig() {
13427
13487
  const [providers, models, settings, protocolOptions] = await Promise.all([
13428
13488
  api("/api/platform/ai/providers"),
@@ -13440,12 +13500,23 @@ async function renderPlatformAiConfig() {
13440
13500
  const available = isSelectableModel({ ...model, providerStatus: provider?.status, providerConnectionStatus: provider?.connectionStatus });
13441
13501
  return `<option value="${esc(model.id)}" ${model.id === settings.imageToolModelId ? "selected" : ""} ${available || model.id === settings.imageToolModelId ? "" : "disabled"}>${esc(`${available ? "" : "不可用 · "}${modelOptionLabel({ ...model, providerName: model.providerName || provider?.name })}`)}</option>`;
13442
13502
  }).join("");
13443
- host.innerHTML = `<section class="config-section platform-system-prompt-section"><div class="config-section-header"><div><h2>平台全局系统提示词</h2><p>会追加在内置系统提示词之后,并在所有作品的专属提示词之前发送给模型。</p></div></div><div class="field-label"><textarea id="platform-system-prompt" rows="7" aria-label="全局系统提示词" placeholder="例如:默认使用简体中文,避免代替作者做最终决定。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-platform-system-prompt" class="primary-button">保存全局提示词</button></div></section><section class="config-section platform-image-tool-section"><div class="config-section-header"><div><h2>多模态读图默认模型</h2><p>Agent 的 image 工具使用这里配置的模型读取设定库图片;作品可以在自己的 AI 设置中覆盖此选择。</p></div></div><div class="platform-image-tool-panel"><label class="platform-image-tool-field"><span>当前平台默认模型</span><select id="platform-image-tool-model" aria-label="平台多模态读图默认模型"><option value="">未配置</option>${imageModelOptions}</select></label><button id="save-platform-image-tool-model" class="ghost-button config-save-button" type="button">保存默认模型</button></div></section><section class="config-section platform-stream-timeout-section"><div class="config-section-header"><div><h2>AI 流事件空闲超时</h2><p>首个流事件或相邻流事件在此时间内没有新数据时,请求会被关闭。默认 90 秒,最低 30 秒,最高 600 秒。</p></div></div><div class="platform-stream-timeout-panel"><label class="platform-stream-timeout-field"><span>超时时间(秒)</span><input id="platform-ai-stream-idle-timeout" type="number" min="30" max="600" step="1" value="${esc(String(settings.streamIdleTimeoutSeconds ?? 90))}" aria-label="AI 流事件空闲超时时间(秒)"></label><button id="save-platform-ai-stream-idle-timeout" class="ghost-button config-save-button" type="button">保存流超时设置</button></div></section><section class="config-section platform-providers-section"><div class="config-section-header"><div><h2>模型供应商配置</h2><p>管理供应商连接、模型列表和连接状态;模型的多模态能力在对应模型配置中设置。</p></div></div>${renderProviderCards(providers, models, protocolOptions)}</section>`;
13503
+ host.innerHTML = `<section class="config-section platform-system-prompt-section"><div class="config-section-header"><div><h2>平台全局系统提示词</h2><p>默认追加在内置系统提示词之后,并在所有作品的专属提示词之前发送给模型。</p></div></div><div class="field-label"><textarea id="platform-system-prompt" rows="7" aria-label="全局系统提示词" placeholder="例如:默认使用简体中文,避免代替作者做最终决定。">${esc(settings.systemPrompt)}</textarea></div>${systemPromptOverrideDetails({ inputId: "platform-system-prompt-override", checked: settings.systemPromptOverride === true, scope: "platform" })}<div class="card-actions"><button id="save-platform-system-prompt" class="primary-button">保存全局提示词</button></div></section><section class="config-section platform-image-tool-section"><div class="config-section-header"><div><h2>多模态读图默认模型</h2><p>Agent 的 image 工具使用这里配置的模型读取设定库图片;作品可以在自己的 AI 设置中覆盖此选择。</p></div></div><div class="platform-image-tool-panel"><label class="platform-image-tool-field"><span>当前平台默认模型</span><select id="platform-image-tool-model" aria-label="平台多模态读图默认模型"><option value="">未配置</option>${imageModelOptions}</select></label><button id="save-platform-image-tool-model" class="ghost-button config-save-button" type="button">保存默认模型</button></div></section><section class="config-section platform-stream-timeout-section"><div class="config-section-header"><div><h2>AI 流事件空闲超时</h2><p>首个流事件或相邻流事件在此时间内没有新数据时,请求会被关闭。默认 90 秒,最低 30 秒,最高 600 秒。</p></div></div><div class="platform-stream-timeout-panel"><label class="platform-stream-timeout-field"><span>超时时间(秒)</span><input id="platform-ai-stream-idle-timeout" type="number" min="30" max="600" step="1" value="${esc(String(settings.streamIdleTimeoutSeconds ?? 90))}" aria-label="AI 流事件空闲超时时间(秒)"></label><button id="save-platform-ai-stream-idle-timeout" class="ghost-button config-save-button" type="button">保存流超时设置</button></div></section><section class="config-section platform-providers-section"><div class="config-section-header"><div><h2>模型供应商配置</h2><p>管理供应商连接、模型列表和连接状态;模型的多模态能力在对应模型配置中设置。</p></div></div>${renderProviderCards(providers, models, protocolOptions)}</section>`;
13504
+ $("#platform-system-prompt-override").addEventListener("change", (event) => {
13505
+ void confirmSystemPromptOverride(event.currentTarget, "平台全局");
13506
+ });
13444
13507
  $("#save-platform-system-prompt").addEventListener("click", async () => {
13445
13508
  const button = $("#save-platform-system-prompt");
13446
13509
  button.disabled = true;
13447
13510
  try {
13448
- await api("/api/platform/ai/settings", { method: "PATCH", body: { systemPrompt: $("#platform-system-prompt").value } });
13511
+ const overrideInput = $("#platform-system-prompt-override");
13512
+ await waitForSystemPromptOverrideConfirmation(overrideInput);
13513
+ await api("/api/platform/ai/settings", {
13514
+ method: "PATCH",
13515
+ body: {
13516
+ systemPrompt: $("#platform-system-prompt").value,
13517
+ systemPromptOverride: overrideInput.checked
13518
+ }
13519
+ });
13449
13520
  toast("平台全局系统提示词已保存");
13450
13521
  } catch (error) {
13451
13522
  toast(error.message, "error");
@@ -13861,6 +13932,18 @@ async function renderBookAiSettings() {
13861
13932
  const available = isAvailableConfiguredModel(model);
13862
13933
  return `<option value="${esc(model.id)}" ${model.id === selectedId ? "selected" : ""} ${available || model.id === selectedId ? "" : "disabled"}>${esc(`${available ? "" : "不可用 · "}${modelOptionLabel(model)}`)}</option>`;
13863
13934
  }).join("");
13935
+ const workSystemPrompt = host.querySelector("#work-system-prompt");
13936
+ const workSystemPromptDescription = workSystemPromptSection?.querySelector(".config-section-header p");
13937
+ if (workSystemPromptDescription) {
13938
+ workSystemPromptDescription.textContent = `默认追加在内置系统提示词和平台全局系统提示词之后,只影响《${state.work.title}》的 AI 请求。`;
13939
+ }
13940
+ workSystemPrompt?.closest(".field-label")?.insertAdjacentHTML(
13941
+ "afterend",
13942
+ systemPromptOverrideDetails({ inputId: "work-system-prompt-override", checked: settings.systemPromptOverride === true, scope: "work" })
13943
+ );
13944
+ $("#work-system-prompt-override").addEventListener("change", (event) => {
13945
+ void confirmSystemPromptOverride(event.currentTarget, "本书");
13946
+ });
13864
13947
  const semanticSection = `<section id="semantic-search-settings" class="config-section semantic-search-settings"><div class="config-section-header"><div><h2>主动语义检索(RAG)</h2><p>与拼音索引并列维护。Embedding 和 rerank 复用平台供应商的端点与凭证保险库;普通聊天、续写、润色和分析不会自动调用此通道。</p></div></div><div class="semantic-settings-grid"><label class="checkbox-field config-checkbox-field semantic-enabled-field"><input id="semantic-search-enabled" type="checkbox" ${settings.semanticSearchEnabled ? "checked" : ""}>启用主动语义检索</label><label>Embedding 模型<select id="semantic-embedding-model" aria-label="RAG Embedding 模型"><option value="">请选择 embedding 模型</option>${semanticModelOptions("embedding", settings.semanticEmbeddingModelId)}</select></label><label>Rerank 模型(可选)<select id="semantic-rerank-model" aria-label="RAG Rerank 模型"><option value="">不使用 rerank</option>${semanticModelOptions("rerank", settings.semanticRerankModelId)}</select></label><label>向量维度<input id="semantic-vector-dimension" type="number" min="1" max="65536" value="${esc(String(settings.semanticVectorDimension ?? 1024))}"></label><label>语义召回数量<input id="semantic-recall-limit" type="number" min="1" max="200" value="${esc(String(settings.semanticRecallLimit ?? 20))}"></label><label>展示结果数量<input id="semantic-result-limit" type="number" min="1" max="100" value="${esc(String(settings.semanticResultLimit ?? 12))}"></label><label>上下文预算(Token)<input id="semantic-budget-tokens" type="number" min="256" max="100000" value="${esc(String(settings.semanticBudgetTokens ?? 4000))}"></label><label>RRF 语义通道权重<input id="semantic-channel-weight" type="number" min="0.1" max="5" step="0.1" value="${esc(String(settings.semanticChannelWeight ?? 1))}"></label></div><p class="usage-measurement-note">API Key 由所选模型所属供应商的凭证保险库加密保存;此页面不会读取或返回明文密钥。更换端点、模型、维度或分片规则后必须完整重建,旧向量不会继续参与检索。</p><div id="semantic-search-index-status" role="status" aria-live="polite">${semanticIndexStatusMarkup(semanticIndex)}</div><div class="relationship-index-actions"><button id="save-semantic-search-settings" class="primary-button config-save-button" type="button">保存 RAG 配置</button><button id="sync-semantic-search-index" class="ghost-button config-save-button" type="button">同步增量</button><button id="refresh-semantic-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-semantic-search-index" class="ghost-button config-save-button" type="button">完整重建 RAG</button></div></section>`;
13865
13948
  const relationshipSection = [...host.querySelectorAll(".config-section")].find((section) => section.querySelector("h2")?.textContent === "人物关系拼音索引");
13866
13949
  relationshipSection?.insertAdjacentHTML("afterend", semanticSection);
@@ -14123,7 +14206,15 @@ async function renderBookAiSettings() {
14123
14206
  const button = $("#save-work-system-prompt");
14124
14207
  button.disabled = true;
14125
14208
  try {
14126
- await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { systemPrompt: $("#work-system-prompt").value } });
14209
+ const overrideInput = $("#work-system-prompt-override");
14210
+ await waitForSystemPromptOverrideConfirmation(overrideInput);
14211
+ await api(`/api/works/${state.work.id}/ai-settings`, {
14212
+ method: "PATCH",
14213
+ body: {
14214
+ systemPrompt: $("#work-system-prompt").value,
14215
+ systemPromptOverride: overrideInput.checked
14216
+ }
14217
+ });
14127
14218
  toast("本书系统提示词已保存");
14128
14219
  setAiContextMeter(null);
14129
14220
  } catch (error) {
@@ -18235,7 +18326,7 @@ function renderAiStreamingCharacterProgress(meta, visibleCharacters) {
18235
18326
  meta.replaceChildren("正在生成 · ", createAiStreamCharacterCount(visible), " 字");
18236
18327
  }
18237
18328
 
18238
- async function streamChat(requestHolder, body, idempotencyKey) {
18329
+ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null } = {}) {
18239
18330
  const tab = aiChatTabForRequest(requestHolder.snapshot);
18240
18331
  if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
18241
18332
  const feed = tab.feed;
@@ -18258,7 +18349,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18258
18349
  window.clearInterval(streamConnectionTimer);
18259
18350
  streamConnectionTimer = null;
18260
18351
  };
18261
- const streamConnectionEstablishedEvents = new Set(["delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
18352
+ const streamConnectionEstablishedEvents = new Set(["continuation", "delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
18262
18353
  const streamSpeedController = createStreamTypewriterSpeedController();
18263
18354
  let messageMounted = false;
18264
18355
  const mountAssistantMessage = () => {
@@ -18288,12 +18379,14 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18288
18379
  let persistedMessageCreatedAt = null;
18289
18380
  let conversationTitle = null;
18290
18381
  let writingSuggestion = null;
18382
+ let question = null;
18291
18383
  let persistedUserMessage = null;
18292
18384
  let contextAction = "ready";
18293
18385
  let warningOnly = false;
18294
18386
  let streamContextCompacted = false;
18295
18387
  const processStartedAt = Date.now();
18296
- const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
18388
+ let previousProcessDurationMs = 0;
18389
+ const elapsedProcessTime = () => previousProcessDurationMs + Math.max(0, Date.now() - processStartedAt);
18297
18390
  const processStepTypewriters = new Map();
18298
18391
  const processStepVisibleContents = new Map();
18299
18392
  const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
@@ -18322,7 +18415,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18322
18415
  };
18323
18416
  try {
18324
18417
  const request = assertAiRequestCurrent(requestHolder.snapshot);
18325
- const response = await fetch(`/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
18418
+ const response = await fetch(endpoint ?? `/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
18326
18419
  method: "POST",
18327
18420
  headers: { "Content-Type": "application/json", Accept: "text/event-stream", "X-CSRF-Token": state.csrfToken, "Idempotency-Key": idempotencyKey },
18328
18421
  body: JSON.stringify(body),
@@ -18337,7 +18430,23 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18337
18430
  const consume = async (eventName, payload) => {
18338
18431
  assertAiRequestCurrent(requestHolder.snapshot);
18339
18432
  if (streamConnectionEstablishedEvents.has(eventName)) stopStreamConnectionTimer();
18340
- if (eventName === "context") {
18433
+ if (eventName === "continuation") {
18434
+ if (String(payload.conversationId ?? "") !== requestHolder.snapshot.conversationId) throw new Error("流式续接返回了其他对话");
18435
+ toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : [];
18436
+ processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : [];
18437
+ previousProcessDurationMs = Math.max(0, Number(payload.processDurationMs) || 0);
18438
+ const previousMessage = [...feed.querySelectorAll(".assistant-message[data-message-id]")]
18439
+ .find((candidate) => candidate.dataset.messageId === String(payload.messageId));
18440
+ if (previousMessage) {
18441
+ attachMessageHeading(message, aiAssistantLabel("正在生成", tab.roleplayCharacter), undefined, tab);
18442
+ previousMessage.replaceWith(message);
18443
+ messageMounted = true;
18444
+ } else mountAssistantMessage();
18445
+ attachMessageIdentity(message, payload.messageId);
18446
+ renderStreamingProcessSteps(false);
18447
+ meta.textContent = "已收到回答,正在继续思考与执行……";
18448
+ scrollAiFeedToBottom(feed);
18449
+ } else if (eventName === "context") {
18341
18450
  contextAction = typeof payload.action === "string" ? payload.action : "ready";
18342
18451
  if (!tab.promptSent) setAiChatTabContextUsage(tab, payload.usage);
18343
18452
  if (payload.conversation?.id) {
@@ -18434,6 +18543,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18434
18543
  setAiChatTabContextUsage(tab, payload.contextUsage);
18435
18544
  if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
18436
18545
  } else if (eventName === "complete") {
18546
+ question = payload.question ?? null;
18437
18547
  if (payload.warningOnly === true) {
18438
18548
  warningOnly = true;
18439
18549
  setAiChatTabContextUsage(tab, payload.contextUsage);
@@ -18490,7 +18600,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18490
18600
  assertAiRequestCurrent(requestHolder.snapshot);
18491
18601
  if (streamError) throw streamError;
18492
18602
  assertAiStreamCompleted(streamCompleted);
18493
- return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, writingSuggestion, userMessage: persistedUserMessage };
18603
+ return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, writingSuggestion, userMessage: persistedUserMessage, question };
18494
18604
  } catch (error) {
18495
18605
  const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
18496
18606
  const interruptionCode = typeof streamFailure.code === "string" ? streamFailure.code.slice(0, 100) : "AI_STREAM_FAILED";
@@ -18715,7 +18825,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18715
18825
  function continuationGuardMarkup(guard) {
18716
18826
  if (!guard) return "";
18717
18827
  const issues = Array.isArray(guard.issues) ? guard.issues : [];
18718
- 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" ? `<p>${esc(guard.failure || "无法完成检查,请谨慎采纳")}</p>` : 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>`;
18828
+ const failure = typeof guard.failure === "string" && guard.failure.trim()
18829
+ ? guard.failure.trim()
18830
+ : "无法完成检查,请谨慎采纳";
18831
+ 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>`;
18719
18832
  }
18720
18833
 
18721
18834
  async function applyAcceptedWritingSuggestion(message, suggestion) {
@@ -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=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-header-toolbar-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-v4">
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=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">
13
13
  </head>
14
14
  <body class="auth-pending">
15
15
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -1425,6 +1425,6 @@
1425
1425
  </dialog>
1426
1426
 
1427
1427
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1428
- <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-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-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=im-settings-gear-v1&feature=compact-sidebar-directory-v2"></script>
1428
+ <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-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"></script>
1429
1429
  </body>
1430
1430
  </html>
@@ -28,4 +28,5 @@ export function createStreamTypewriter<FrameHandle = number>(options: {
28
28
  cancelFrame?: (handle: FrameHandle) => void;
29
29
  reducedMotion?: boolean;
30
30
  speedController?: StreamTypewriterSpeedController | null;
31
+ visibilitySource?: Pick<Document, "visibilityState" | "addEventListener" | "removeEventListener"> | null;
31
32
  }): StreamTypewriter;
@@ -74,7 +74,8 @@ export function createStreamTypewriter({
74
74
  scheduleFrame = (callback) => window.requestAnimationFrame(callback),
75
75
  cancelFrame = (handle) => window.cancelAnimationFrame(handle),
76
76
  reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true,
77
- speedController = null
77
+ speedController = null,
78
+ visibilitySource = typeof document === "undefined" ? null : document
78
79
  }) {
79
80
  if (typeof onRender !== "function") throw new TypeError("onRender must be a function");
80
81
 
@@ -83,10 +84,18 @@ export function createStreamTypewriter({
83
84
  const idleResolvers = [];
84
85
  let scheduledFrame = null;
85
86
  let finishing = false;
87
+ let listeningForVisibility = false;
86
88
 
87
89
  const snapshot = () => visibleCharacters.join("");
90
+ const visibilityChanged = () => {
91
+ if (pendingCharacters.length) typewriter.reveal();
92
+ };
88
93
  const resolveIdle = () => {
89
94
  if (pendingCharacters.length || scheduledFrame !== null) return;
95
+ if (listeningForVisibility) {
96
+ visibilitySource.removeEventListener("visibilitychange", visibilityChanged);
97
+ listeningForVisibility = false;
98
+ }
90
99
  const value = snapshot();
91
100
  for (const resolve of idleResolvers.splice(0)) resolve(value);
92
101
  };
@@ -97,7 +106,15 @@ export function createStreamTypewriter({
97
106
  });
98
107
  };
99
108
  const schedule = () => {
109
+ if (visibilitySource?.visibilityState === "hidden") {
110
+ typewriter.reveal();
111
+ return;
112
+ }
100
113
  if (scheduledFrame !== null || pendingCharacters.length === 0) return;
114
+ if (visibilitySource && !listeningForVisibility) {
115
+ visibilitySource.addEventListener("visibilitychange", visibilityChanged);
116
+ listeningForVisibility = true;
117
+ }
101
118
  scheduledFrame = scheduleFrame(() => {
102
119
  scheduledFrame = null;
103
120
  const batchSize = reducedMotion
@@ -114,7 +131,7 @@ export function createStreamTypewriter({
114
131
  });
115
132
  };
116
133
 
117
- return {
134
+ const typewriter = {
118
135
  append(value) {
119
136
  const characters = Array.from(String(value ?? ""));
120
137
  if (!characters.length) return;
@@ -137,8 +154,9 @@ export function createStreamTypewriter({
137
154
  finish() {
138
155
  if (!pendingCharacters.length && scheduledFrame === null) return Promise.resolve(snapshot());
139
156
  finishing = true;
157
+ const completed = new Promise((resolve) => idleResolvers.push(resolve));
140
158
  schedule();
141
- return new Promise((resolve) => idleResolvers.push(resolve));
159
+ return completed;
142
160
  },
143
161
  reveal() {
144
162
  if (scheduledFrame !== null) {
@@ -152,4 +170,5 @@ export function createStreamTypewriter({
152
170
  return snapshot();
153
171
  }
154
172
  };
173
+ return typewriter;
155
174
  }
@@ -1440,6 +1440,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1440
1440
  display: grid;
1441
1441
  grid-template-columns: auto minmax(0, 2em);
1442
1442
  align-items: start;
1443
+ justify-content: start;
1443
1444
  word-break: break-all;
1444
1445
  }
1445
1446
  .module-nav > button[data-module="outlines"] {
@@ -1778,6 +1779,21 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1778
1779
  .config-section { margin-top: 28px; padding-top: 24px; border-top: 1px solid var(--line); }
1779
1780
  .config-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
1780
1781
  .platform-system-prompt-section { margin-bottom: 36px; }
1782
+ .system-prompt-override-settings { margin-top: 12px; overflow: hidden; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
1783
+ .system-prompt-override-settings.is-enabled { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }
1784
+ .system-prompt-override-settings summary { display: grid; min-height: 40px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 9px 12px; cursor: pointer; color: var(--muted); font-size: 11px; list-style: none; }
1785
+ .system-prompt-override-settings summary::-webkit-details-marker { display: none; }
1786
+ .system-prompt-override-settings summary::before { content: "›"; color: var(--muted); font-size: 16px; line-height: 1; transform: rotate(0deg); transition: transform .16s ease; }
1787
+ .system-prompt-override-settings[open] summary::before { transform: rotate(90deg); }
1788
+ .system-prompt-override-settings summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
1789
+ .system-prompt-override-settings summary strong { color: var(--ink); font-size: 10px; font-weight: 600; }
1790
+ .system-prompt-override-settings.is-enabled summary strong { color: var(--accent-dark); }
1791
+ .system-prompt-override-body { display: grid; gap: 10px; padding: 12px; border-top: 1px solid var(--line); }
1792
+ .system-prompt-override-body > .config-checkbox-field { display: inline-flex; width: fit-content; align-items: center; gap: 8px; margin: 0; color: var(--ink); font-size: 11px; }
1793
+ .system-prompt-override-body > .config-checkbox-field input { width: 18px; min-width: 18px; height: 18px; }
1794
+ .system-prompt-override-warning { padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--accent) 48%, var(--line)); border-radius: 5px; background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }
1795
+ .system-prompt-override-warning strong { color: var(--ink); font-size: 11px; }
1796
+ .system-prompt-override-warning p { margin: 3px 0 0; color: var(--muted); font-size: 10px; line-height: 1.55; }
1781
1797
  .platform-image-tool-section { margin-bottom: 0; }
1782
1798
  .platform-image-tool-panel { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; padding: 16px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
1783
1799
  .platform-image-tool-field { display: grid; flex: 1 1 360px; gap: 7px; min-width: 0; color: var(--muted); font-size: 10px; }
@@ -3076,6 +3092,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3076
3092
  .writing-suggestion-ui { display: contents; }
3077
3093
  .guard-card.warning { border-left-color: #c07b31; }.guard-card.failed { border-left-color: var(--accent); }
3078
3094
  .guard-card strong { display: block; margin-bottom: 5px; font-size: 10px; }.guard-card p { margin: 4px 0; color: var(--muted); font-size: 9px; line-height: 1.45; }.guard-card p b { color: var(--ink); }
3095
+ .guard-failure-details { margin-top: 6px; }
3096
+ .guard-failure-details summary { width: max-content; max-width: 100%; color: var(--accent-dark); font-size: 9px; line-height: 1.45; cursor: pointer; }
3097
+ .guard-failure-details p { margin-top: 6px; padding: 7px 8px; border: 1px solid var(--line); border-radius: 3px; background: var(--surface); white-space: pre-wrap; overflow-wrap: anywhere; }
3079
3098
  .message-actions { display: flex; gap: 7px; margin-top: 10px; }
3080
3099
  .message-actions button { border: 1px solid rgba(139,61,44,.25); background: var(--surface); color: var(--accent-dark); font-size: 10px; padding: 5px 8px; border-radius: 3px; }
3081
3100
  .prompt-box { border-top: 1px solid var(--line); padding-top: 12px; }
@@ -3520,6 +3539,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3520
3539
  .dialog-fields .form-field-note { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.65; }
3521
3540
  .dialog-fields label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
3522
3541
  .dialog-fields input, .dialog-fields textarea, .dialog-fields select { width: 100%; padding: 9px 10px; font-size: 13px; }
3542
+ .dialog-fields input[type="checkbox"] { flex: 0 0 16px; width: 16px; min-width: 16px; height: 16px; min-height: 16px; margin: 0; padding: 0; }
3523
3543
  .dialog-fields select { padding-inline-end: 28px; }
3524
3544
  .dialog-fields .character-extraction-candidate-toggle { display: inline-flex; width: auto; align-items: center; gap: 7px; margin: 0; color: var(--ink); font-size: 9px; white-space: nowrap; }
3525
3545
  .dialog-fields .character-extraction-candidate-toggle input[type="checkbox"] { flex: 0 0 18px; width: 18px; min-width: 18px; height: 18px; padding: 0; }
@@ -3570,6 +3590,14 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3570
3590
  .model-capability-option small, .model-multimodal-note { color: var(--muted); font-size: 9px; line-height: 1.5; }
3571
3591
  .model-capability-option:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
3572
3592
  .model-multimodal-note { margin: 0 2px; }
3593
+ #form-dialog:has(.model-kind-fields) { width: min(720px, 92vw); }
3594
+ #dialog-fields:has(.model-kind-fields) { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 14px; }
3595
+ #dialog-fields:has(.model-kind-fields) [data-chat-model-fields] { display: contents; }
3596
+ #dialog-fields:has(.model-kind-fields) > .model-kind-fields,
3597
+ #dialog-fields:has(.model-kind-fields) > .checkbox-field,
3598
+ #dialog-fields:has(.model-kind-fields) .chip-field,
3599
+ #dialog-fields:has(.model-kind-fields) .model-multimodal-fields,
3600
+ #dialog-fields:has(.model-kind-fields) .model-connection-test { grid-column: 1 / -1; }
3573
3601
  .analysis-type-description { margin: 0; color: var(--muted); font-size: 10px; line-height: 1.55; }
3574
3602
  .task-chapter-field { transition: opacity .15s ease; }
3575
3603
  .task-chapter-field.is-disabled { opacity: .48; }
@@ -4467,6 +4495,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4467
4495
  margin: auto;
4468
4496
  border-radius: 10px;
4469
4497
  }
4498
+ #form-dialog:has(.model-kind-fields) { width: calc(100vw - 20px); max-width: calc(100vw - 20px); }
4470
4499
  .dialog-header { padding: 14px 16px 12px; }
4471
4500
  .dialog-header h2 { font-size: 20px; }
4472
4501
  .dialog-title-input { width: min(100%, calc(100vw - 100px)); font-size: 20px; }
@@ -4573,6 +4602,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4573
4602
  .onboarding-popover { width: calc(100vw - 20px); }
4574
4603
  }
4575
4604
 
4605
+ @media (max-width: 480px) {
4606
+ #dialog-fields:has(.model-kind-fields) { grid-template-columns: minmax(0, 1fr); }
4607
+ }
4608
+
4576
4609
  @media (max-width: 850px) {
4577
4610
  #onboarding-menu-button { display: none; }
4578
4611
  }
@@ -0,0 +1 @@
1
+ export function countProseWords(text: unknown): number;
@@ -0,0 +1,7 @@
1
+ export function countProseWords(text) {
2
+ const value = String(text ?? "");
3
+ const chinese = value.match(/[\p{Script=Han}]/gu)?.length ?? 0;
4
+ const withoutChinese = value.replace(/[\p{Script=Han}]/gu, " ");
5
+ const latin = withoutChinese.match(/[\p{L}\p{N}]+/gu)?.length ?? 0;
6
+ return chinese + latin;
7
+ }