@musnows/scriverse 1.0.2 → 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.
@@ -50,13 +50,17 @@ export function createAiRequestManager() {
50
50
  if (!current) return false;
51
51
  activeRequests.delete(key);
52
52
  current.controller.abort(createAiRequestAbortError(reason));
53
+ current.resolveFinished();
53
54
  return true;
54
55
  };
55
56
 
56
57
  const cancelAll = (reason = "AI 请求已取消") => {
57
58
  const requests = [...activeRequests.values()];
58
59
  activeRequests.clear();
59
- for (const current of requests) current.controller.abort(createAiRequestAbortError(reason));
60
+ for (const current of requests) {
61
+ current.controller.abort(createAiRequestAbortError(reason));
62
+ current.resolveFinished();
63
+ }
60
64
  return requests.length;
61
65
  };
62
66
 
@@ -67,7 +71,9 @@ export function createAiRequestManager() {
67
71
  generation += 1;
68
72
  const request = snapshot(input, controller, generation);
69
73
  if (!request.workId) throw new Error("AI 请求必须绑定作品");
70
- activeRequests.set(key, { controller, snapshot: request });
74
+ let resolveFinished;
75
+ const finished = new Promise((resolve) => { resolveFinished = resolve; });
76
+ activeRequests.set(key, { controller, snapshot: request, finished, resolveFinished });
71
77
  return request;
72
78
  };
73
79
 
@@ -98,6 +104,7 @@ export function createAiRequestManager() {
98
104
 
99
105
  const finish = (request) => {
100
106
  if (!isCurrent(request)) return false;
107
+ activeRequests.get(request.tabId).resolveFinished();
101
108
  activeRequests.delete(request.tabId);
102
109
  return true;
103
110
  };
@@ -108,6 +115,7 @@ export function createAiRequestManager() {
108
115
  cancel,
109
116
  cancelAll,
110
117
  finish,
118
+ whenIdle: (tabId) => activeRequests.get(requestKey({ tabId }))?.finished ?? Promise.resolve(),
111
119
  hasActive: (tabId = null) => tabId === null
112
120
  ? activeRequests.size > 0
113
121
  : activeRequests.has(requestKey({ tabId })),
@@ -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) {
@@ -18313,7 +18326,7 @@ function renderAiStreamingCharacterProgress(meta, visibleCharacters) {
18313
18326
  meta.replaceChildren("正在生成 · ", createAiStreamCharacterCount(visible), " 字");
18314
18327
  }
18315
18328
 
18316
- async function streamChat(requestHolder, body, idempotencyKey) {
18329
+ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null } = {}) {
18317
18330
  const tab = aiChatTabForRequest(requestHolder.snapshot);
18318
18331
  if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
18319
18332
  const feed = tab.feed;
@@ -18336,7 +18349,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18336
18349
  window.clearInterval(streamConnectionTimer);
18337
18350
  streamConnectionTimer = null;
18338
18351
  };
18339
- 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"]);
18340
18353
  const streamSpeedController = createStreamTypewriterSpeedController();
18341
18354
  let messageMounted = false;
18342
18355
  const mountAssistantMessage = () => {
@@ -18366,12 +18379,14 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18366
18379
  let persistedMessageCreatedAt = null;
18367
18380
  let conversationTitle = null;
18368
18381
  let writingSuggestion = null;
18382
+ let question = null;
18369
18383
  let persistedUserMessage = null;
18370
18384
  let contextAction = "ready";
18371
18385
  let warningOnly = false;
18372
18386
  let streamContextCompacted = false;
18373
18387
  const processStartedAt = Date.now();
18374
- const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
18388
+ let previousProcessDurationMs = 0;
18389
+ const elapsedProcessTime = () => previousProcessDurationMs + Math.max(0, Date.now() - processStartedAt);
18375
18390
  const processStepTypewriters = new Map();
18376
18391
  const processStepVisibleContents = new Map();
18377
18392
  const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
@@ -18400,7 +18415,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18400
18415
  };
18401
18416
  try {
18402
18417
  const request = assertAiRequestCurrent(requestHolder.snapshot);
18403
- const response = await fetch(`/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
18418
+ const response = await fetch(endpoint ?? `/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
18404
18419
  method: "POST",
18405
18420
  headers: { "Content-Type": "application/json", Accept: "text/event-stream", "X-CSRF-Token": state.csrfToken, "Idempotency-Key": idempotencyKey },
18406
18421
  body: JSON.stringify(body),
@@ -18415,7 +18430,23 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18415
18430
  const consume = async (eventName, payload) => {
18416
18431
  assertAiRequestCurrent(requestHolder.snapshot);
18417
18432
  if (streamConnectionEstablishedEvents.has(eventName)) stopStreamConnectionTimer();
18418
- 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") {
18419
18450
  contextAction = typeof payload.action === "string" ? payload.action : "ready";
18420
18451
  if (!tab.promptSent) setAiChatTabContextUsage(tab, payload.usage);
18421
18452
  if (payload.conversation?.id) {
@@ -18512,6 +18543,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18512
18543
  setAiChatTabContextUsage(tab, payload.contextUsage);
18513
18544
  if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
18514
18545
  } else if (eventName === "complete") {
18546
+ question = payload.question ?? null;
18515
18547
  if (payload.warningOnly === true) {
18516
18548
  warningOnly = true;
18517
18549
  setAiChatTabContextUsage(tab, payload.contextUsage);
@@ -18568,7 +18600,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18568
18600
  assertAiRequestCurrent(requestHolder.snapshot);
18569
18601
  if (streamError) throw streamError;
18570
18602
  assertAiStreamCompleted(streamCompleted);
18571
- 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 };
18572
18604
  } catch (error) {
18573
18605
  const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
18574
18606
  const interruptionCode = typeof streamFailure.code === "string" ? streamFailure.code.slice(0, 100) : "AI_STREAM_FAILED";
@@ -18793,7 +18825,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18793
18825
  function continuationGuardMarkup(guard) {
18794
18826
  if (!guard) return "";
18795
18827
  const issues = Array.isArray(guard.issues) ? guard.issues : [];
18796
- 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>`;
18797
18832
  }
18798
18833
 
18799
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-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">
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
  }
@@ -3092,6 +3092,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3092
3092
  .writing-suggestion-ui { display: contents; }
3093
3093
  .guard-card.warning { border-left-color: #c07b31; }.guard-card.failed { border-left-color: var(--accent); }
3094
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; }
3095
3098
  .message-actions { display: flex; gap: 7px; margin-top: 10px; }
3096
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; }
3097
3100
  .prompt-box { border-top: 1px solid var(--line); padding-top: 12px; }
@@ -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
+ }
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = "1.0.2";
1
+ export const APP_VERSION = "1.0.3";
2
2
  export const SCRIVERSE_BETA_COMMIT_ENV = "SCRIVERSE_BETA_COMMIT";
3
3
  export function resolveBetaVersionLabel(environment) {
4
4
  const commit = environment[SCRIVERSE_BETA_COMMIT_ENV]?.trim().toLocaleLowerCase() ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@musnows/scriverse",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Command-line client and local server for the Scriverse long-form fiction workspace",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "musnows",