@musnows/scriverse 1.0.3 → 1.0.4

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.
@@ -51,6 +51,17 @@ export function aiQuestionStatusLabel(status) {
51
51
  return QUESTION_STATUS_LABELS[String(status)] ?? String(status);
52
52
  }
53
53
 
54
+ /** 待回答或尚未加载完成时不允许静默关闭提问窗口。 */
55
+ export function aiQuestionDialogCanClose(question) {
56
+ return ["answered", "rejected", "expired"].includes(String(question?.status ?? ""));
57
+ }
58
+
59
+ /** 优先读取结构化错误码,并兼容修复前只保存了错误正文的历史消息。 */
60
+ export function aiFailureCode(text, metadata = {}) {
61
+ if (typeof metadata?.errorCode === "string" && metadata.errorCode) return metadata.errorCode;
62
+ return String(text ?? "").match(/(?:^|\n)错误码:([^\n]+)(?:\n|$)/u)?.[1]?.trim() ?? "";
63
+ }
64
+
54
65
  /** 状态徽章色调:CSS 里按 data-tone 展示统一配色。 */
55
66
  export function statusTone(status) {
56
67
  switch (String(status)) {
@@ -114,19 +125,63 @@ export function parseInteractiveToolPayload(toolCall) {
114
125
  error
115
126
  };
116
127
  }
117
- const options = Array.isArray(toolCall?.arguments?.options) ? toolCall.arguments.options : [];
128
+ const rawArguments = toolCall?.arguments;
129
+ let suppliedArguments = rawArguments;
130
+ if (typeof rawArguments === "string") {
131
+ try { suppliedArguments = JSON.parse(rawArguments); } catch { suppliedArguments = null; }
132
+ }
133
+ const argumentQuestions = Array.isArray(suppliedArguments?.questions)
134
+ ? suppliedArguments.questions
135
+ : suppliedArguments?.question
136
+ ? [{ question: suppliedArguments.question, options: suppliedArguments.options }]
137
+ : [];
138
+ const resultQuestion = ok && result?.question && typeof result.question === "object" && !Array.isArray(result.question)
139
+ ? result.question
140
+ : null;
141
+ const question = resultQuestion && (!Array.isArray(resultQuestion.questions) || resultQuestion.questions.length === 0) && argumentQuestions.length > 0
142
+ ? { ...resultQuestion, questions: argumentQuestions }
143
+ : resultQuestion;
118
144
  return {
119
145
  kind: "question",
120
146
  ok,
121
147
  name,
122
148
  calledAt: toolCall?.calledAt ?? "",
123
- question: ok ? result.question ?? null : null,
124
- argumentOptions: options.map((option) => String(option)),
149
+ question,
150
+ argumentQuestions,
125
151
  message: typeof result?.message === "string" ? result.message : "",
126
152
  error
127
153
  };
128
154
  }
129
155
 
156
+ /** 兼容历史单题、批量题目、字符串选项与完整 API 选项对象。 */
157
+ export function normalizeAiQuestionItems(question) {
158
+ const items = Array.isArray(question?.questions) && question.questions.length > 0
159
+ ? question.questions
160
+ : [{
161
+ question: question?.question ?? "",
162
+ options: question?.options ?? [],
163
+ selectedOption: question?.selectedOption ?? null,
164
+ customAnswer: question?.customAnswer ?? "",
165
+ answerText: question?.answerText ?? "",
166
+ isCustomAnswer: question?.isCustomAnswer === true
167
+ }];
168
+ return items.map((item, index) => ({
169
+ ...item,
170
+ index,
171
+ question: String(item?.question ?? ""),
172
+ options: (Array.isArray(item?.options) ? item.options : []).map((option, optionIndex) => (
173
+ option && typeof option === "object" && !Array.isArray(option)
174
+ ? {
175
+ ...option,
176
+ index: Number.isInteger(option.index) ? Number(option.index) : optionIndex,
177
+ label: String(option.label ?? ""),
178
+ recommended: option.recommended === true || (option.recommended === undefined && optionIndex === 0)
179
+ }
180
+ : { index: optionIndex, label: String(option ?? ""), recommended: optionIndex === 0 }
181
+ ))
182
+ }));
183
+ }
184
+
130
185
  /**
131
186
  * 会话内的最新审批详情缓存:确认/撤销之后写回,避免卡片在重渲染时回退到提交时刻的快照。
132
187
  */
@@ -294,9 +349,7 @@ function buildQuestionCard(model, actions) {
294
349
  card.append(body);
295
350
  }
296
351
 
297
- const options = question?.options?.length
298
- ? question.options.map((option) => option.label)
299
- : model.argumentOptions;
352
+ const options = normalizeAiQuestionItems(question).flatMap((item) => item.options.map((option) => option.label));
300
353
  if (options.length > 0) {
301
354
  const list = document.createElement("ol");
302
355
  list.className = "ai-question-option-preview";
@@ -41,15 +41,18 @@ import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
41
41
  import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
42
42
  import {
43
43
  AI_WRITE_TOOLS_META,
44
+ aiFailureCode,
45
+ aiQuestionDialogCanClose,
44
46
  cacheAiQuestionView,
45
47
  cacheAiWritePlanDetail,
46
48
  createInteractiveToolCard,
49
+ normalizeAiQuestionItems,
47
50
  parseInteractiveToolPayload,
48
51
  renderApprovalCenterRows,
49
52
  renderWritePlanDetailMarkup,
50
53
  isInteractiveToolPending,
51
54
  aiFormatDateTime
52
- } from "/ai-interactive.js?v=20260903-question-batch-v7";
55
+ } from "/ai-interactive.js?v=20260906-question-recovery-v1";
53
56
  import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
54
57
  import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
55
58
  import { clipboardImageFiles } from "/character-markdown.js?v=20260820-ai-chat-image-attachments-v1";
@@ -2898,6 +2901,41 @@ function aiRetryStreamRequestBody(body, retry) {
2898
2901
  : body;
2899
2902
  }
2900
2903
 
2904
+ async function discardPendingAiQuestion(message, button) {
2905
+ const feed = message.closest(".ai-feed");
2906
+ const tab = aiChatTabManager.get(feed?.dataset.aiTabId);
2907
+ if (!tab?.conversationId || !tab.workId || String(tab.workId) !== String(state.work?.id ?? "")) {
2908
+ throw new Error("无法确定待回答问题所属的当前对话");
2909
+ }
2910
+ const label = button.querySelector("span");
2911
+ button.disabled = true;
2912
+ if (label) label.textContent = "正在作废提问";
2913
+ try {
2914
+ const expectedQuestionId = String(message.dataset.pendingQuestionId ?? "");
2915
+ const parameters = new URLSearchParams({ conversationId: tab.conversationId, status: "pending", limit: "1" });
2916
+ const payload = await api(`/api/works/${encodeURIComponent(tab.workId)}/ai/questions?${parameters}`);
2917
+ const questions = Array.isArray(payload) ? payload : (Array.isArray(payload?.questions) ? payload.questions : []);
2918
+ const questionId = String(questions.find((question) => String(question?.id ?? "") === expectedQuestionId)?.id ?? questions[0]?.id ?? "");
2919
+ if (!questionId) {
2920
+ if (label) label.textContent = "提问已处理";
2921
+ button.setAttribute("aria-label", "待回答提问已处理");
2922
+ toast("当前对话已经没有待回答问题,可以重新发送消息");
2923
+ return;
2924
+ }
2925
+ await respondAiUserQuestion(questionId, { action: "reject" });
2926
+ for (const action of tab.feed.querySelectorAll(".ai-question-discard-button")) {
2927
+ action.disabled = true;
2928
+ action.setAttribute("aria-label", "待回答提问已作废");
2929
+ const actionLabel = action.querySelector("span");
2930
+ if (actionLabel) actionLabel.textContent = "提问已作废";
2931
+ }
2932
+ } catch (error) {
2933
+ button.disabled = false;
2934
+ if (label) label.textContent = "作废提问并继续";
2935
+ throw error;
2936
+ }
2937
+ }
2938
+
2901
2939
  function renderMessageCardActions(message) {
2902
2940
  let actions = message.querySelector(".message-card-actions");
2903
2941
  if (!actions) {
@@ -2927,18 +2965,30 @@ function renderMessageCardActions(message) {
2927
2965
  actions.append(copy);
2928
2966
  }
2929
2967
  if (message.dataset.status === "failed" && message.classList.contains("assistant-message")) {
2930
- const retry = document.createElement("button");
2931
- retry.type = "button";
2932
- retry.className = "message-retry-button";
2933
- retry.setAttribute("aria-label", "重试");
2934
- retry.innerHTML = '<svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 0 0-14.7-4L4 9"/><path d="M4 4v5h5"/><path d="M4 13a8 8 0 0 0 14.7 4L20 15"/><path d="M20 20v-5h-5"/></svg><span>重试</span>';
2935
- retry.addEventListener("click", () => {
2936
- retry.disabled = true;
2937
- void retryAiMessage(message).finally(() => {
2938
- if (retry.isConnected) retry.disabled = false;
2968
+ if (message.dataset.errorCode === "AI_QUESTION_PENDING") {
2969
+ const discard = document.createElement("button");
2970
+ discard.type = "button";
2971
+ discard.className = "message-retry-button ai-question-discard-button";
2972
+ discard.setAttribute("aria-label", "作废待回答提问并让 AI 继续");
2973
+ discard.innerHTML = '<svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5l14 14M19 5 5 19"/><path d="M15 12h6M18 9l3 3-3 3"/></svg><span>作废提问并继续</span>';
2974
+ discard.addEventListener("click", () => {
2975
+ void discardPendingAiQuestion(message, discard).catch((error) => toast(`作废提问失败:${error.message}`, "error"));
2939
2976
  });
2940
- });
2941
- actions.append(retry);
2977
+ actions.append(discard);
2978
+ } else {
2979
+ const retry = document.createElement("button");
2980
+ retry.type = "button";
2981
+ retry.className = "message-retry-button";
2982
+ retry.setAttribute("aria-label", "重试");
2983
+ retry.innerHTML = '<svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 0 0-14.7-4L4 9"/><path d="M4 4v5h5"/><path d="M4 13a8 8 0 0 0 14.7 4L20 15"/><path d="M20 20v-5h-5"/></svg><span>重试</span>';
2984
+ retry.addEventListener("click", () => {
2985
+ retry.disabled = true;
2986
+ void retryAiMessage(message).finally(() => {
2987
+ if (retry.isConnected) retry.disabled = false;
2988
+ });
2989
+ });
2990
+ actions.append(retry);
2991
+ }
2942
2992
  } else if (message.dataset.messageId && message.classList.contains("assistant-message")) {
2943
2993
  const fork = document.createElement("button");
2944
2994
  fork.type = "button";
@@ -3262,14 +3312,14 @@ function handleInteractiveToolCallEvent(toolCall) {
3262
3312
  return;
3263
3313
  }
3264
3314
  if (name !== "ask_user_question" || toolCall.status === "failed") return;
3265
- const question = toolCall.result?.question;
3315
+ const question = parseInteractiveToolPayload(toolCall)?.question;
3266
3316
  if (!question?.id) return;
3267
3317
  cacheAiQuestionView(question);
3268
3318
  const questionId = String(question.id);
3269
3319
  if (autoOpenedQuestionIds.has(questionId)) return;
3270
3320
  autoOpenedQuestionIds.add(questionId);
3271
3321
  // 直接弹出回答框等待作者选择;不会预填任何答案。
3272
- openAiUserQuestionDialog(questionId).catch(() => undefined);
3322
+ openAiUserQuestionDialog(questionId, question).catch(() => undefined);
3273
3323
  }
3274
3324
 
3275
3325
  function questionsEndpoint(path) {
@@ -3392,22 +3442,7 @@ async function fetchAiUserQuestion(questionId) {
3392
3442
  }
3393
3443
 
3394
3444
  function aiQuestionItems(question) {
3395
- const items = Array.isArray(question?.questions) && question.questions.length > 0
3396
- ? question.questions
3397
- : [{
3398
- question: question?.question ?? "",
3399
- options: question?.options ?? [],
3400
- selectedOption: question?.selectedOption ?? null,
3401
- customAnswer: question?.customAnswer ?? "",
3402
- answerText: question?.answerText ?? "",
3403
- isCustomAnswer: question?.isCustomAnswer === true
3404
- }];
3405
- return items.map((item, index) => ({
3406
- ...item,
3407
- index,
3408
- question: String(item?.question ?? ""),
3409
- options: Array.isArray(item?.options) ? item.options : []
3410
- }));
3445
+ return normalizeAiQuestionItems(question);
3411
3446
  }
3412
3447
 
3413
3448
  function resetAiQuestionDialogState(question) {
@@ -3448,6 +3483,14 @@ function syncAiQuestionAnswerCount() {
3448
3483
  $("#ai-question-answer-count").textContent = `已填写 ${input.value.length} / ${maximum}`;
3449
3484
  }
3450
3485
 
3486
+ function syncAiQuestionDismissState() {
3487
+ const closeButton = $("#ai-question-close");
3488
+ const canClose = aiQuestionDialogCanClose(currentAiQuestionDialogView);
3489
+ closeButton.disabled = !canClose;
3490
+ closeButton.classList.toggle("hidden", !canClose);
3491
+ $("#ai-question-dialog").dataset.dismissLocked = String(!canClose);
3492
+ }
3493
+
3451
3494
  function renderAiUserQuestionOptions(question) {
3452
3495
  const host = $("#ai-question-options");
3453
3496
  const customInput = $("#ai-question-custom-answer");
@@ -3502,6 +3545,7 @@ function renderAiUserQuestionOptions(question) {
3502
3545
  // 提交按钮由选择状态驱动:待回答且已选择(或输入)时才可提交。
3503
3546
  if (isPending) syncAiQuestionSubmitState();
3504
3547
  else $("#ai-question-submit").disabled = true;
3548
+ syncAiQuestionDismissState();
3505
3549
  $("#ai-question-submit").textContent = items.length > 1 ? "提交全部回答" : "提交回答";
3506
3550
  $("#ai-question-skip").disabled = !isPending;
3507
3551
  $("#ai-question-expiry").textContent = isPending
@@ -3517,17 +3561,25 @@ async function refreshAiQuestionDialog() {
3517
3561
  return question;
3518
3562
  }
3519
3563
 
3520
- async function openAiUserQuestionDialog(questionId) {
3564
+ async function openAiUserQuestionDialog(questionId, initialQuestion = null) {
3521
3565
  aiQuestionDialogQuestionId = String(questionId);
3522
3566
  const dialog = $("#ai-question-dialog");
3523
3567
  if (!dialog.open) dialog.showModal();
3524
- $("#ai-question-text").textContent = "";
3525
- $("#ai-question-options").replaceChildren();
3526
- $("#ai-question-custom-answer").value = "";
3527
- $("#ai-question-navigation").classList.add("hidden");
3528
- $("#ai-question-progress").textContent = "正在加载问题";
3529
- syncAiQuestionAnswerCount();
3530
- $("#ai-question-expiry").textContent = "正在加载问题……";
3568
+ if (initialQuestion?.id && String(initialQuestion.id) === aiQuestionDialogQuestionId) {
3569
+ currentAiQuestionDialogView = initialQuestion;
3570
+ resetAiQuestionDialogState(initialQuestion);
3571
+ renderAiUserQuestionOptions(initialQuestion);
3572
+ } else {
3573
+ currentAiQuestionDialogView = null;
3574
+ $("#ai-question-text").textContent = "";
3575
+ $("#ai-question-options").replaceChildren();
3576
+ $("#ai-question-custom-answer").value = "";
3577
+ $("#ai-question-navigation").classList.add("hidden");
3578
+ $("#ai-question-progress").textContent = "正在加载问题";
3579
+ syncAiQuestionAnswerCount();
3580
+ $("#ai-question-expiry").textContent = "正在加载问题……";
3581
+ syncAiQuestionDismissState();
3582
+ }
3531
3583
  try {
3532
3584
  return await refreshAiQuestionDialog();
3533
3585
  } catch (error) {
@@ -5908,6 +5960,15 @@ function formatAiFailureMessage(error) {
5908
5960
  return lines.join("\n");
5909
5961
  }
5910
5962
 
5963
+ function aiFailureMessageMetadata(error) {
5964
+ const details = error?.details && typeof error.details === "object" && !Array.isArray(error.details) ? error.details : {};
5965
+ return {
5966
+ ...(typeof error?.code === "string" ? { errorCode: error.code.slice(0, 100) } : {}),
5967
+ ...(Number.isInteger(error?.status) ? { errorStatus: error.status } : {}),
5968
+ ...(typeof details.questionId === "string" ? { pendingQuestionId: details.questionId } : {})
5969
+ };
5970
+ }
5971
+
5911
5972
  function isAgentToolCallLimitFailure(text) {
5912
5973
  return /more than \d+ tool calls in one response cycle\./u.test(String(text ?? ""));
5913
5974
  }
@@ -18290,6 +18351,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
18290
18351
  return;
18291
18352
  }
18292
18353
  const failureMessage = formatAiFailureMessage(error);
18354
+ const failureMetadata = aiFailureMessageMetadata(error);
18293
18355
  let persistedFailureMessage = null;
18294
18356
  try {
18295
18357
  persistedFailureMessage = await persistAiConversationMessage(
@@ -18297,13 +18359,13 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
18297
18359
  "assistant",
18298
18360
  failureMessage,
18299
18361
  [],
18300
- {},
18362
+ failureMetadata,
18301
18363
  { requestId: aiAssistantRequestId(request) }
18302
18364
  );
18303
18365
  updateAiConversationSummaryFromMessage(persistedFailureMessage);
18304
18366
  } catch { /* 主请求错误已显示,历史记录保存失败不覆盖原始错误 */ }
18305
18367
  if (aiRequestTargetsCurrentState(request)) {
18306
- appendMessage("assistant", failureMessage, [], persistedFailureMessage?.createdAt, {}, persistedFailureMessage?.id, { tab });
18368
+ appendMessage("assistant", failureMessage, [], persistedFailureMessage?.createdAt, failureMetadata, persistedFailureMessage?.id, { tab });
18307
18369
  }
18308
18370
  } finally {
18309
18371
  aiRequestManager.finish(requestHolder.snapshot);
@@ -18686,6 +18748,9 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18686
18748
  const isInterrupted = role === "assistant" && metadata?.interrupted === true;
18687
18749
  const interruptionCode = typeof metadata?.interruptionCode === "string" ? metadata.interruptionCode : "AI_STREAM_FAILED";
18688
18750
  message.className = `${role === "user" ? "user-message" : "assistant-message"}${isFailure || isInterrupted ? " is-error" : ""}`;
18751
+ const errorCode = isFailure ? aiFailureCode(text, metadata) : "";
18752
+ if (errorCode) message.dataset.errorCode = errorCode;
18753
+ if (isFailure && typeof metadata?.pendingQuestionId === "string") message.dataset.pendingQuestionId = metadata.pendingQuestionId;
18689
18754
  const parsedUserTurn = role === "user" ? parseRoleplayUserTurn(text) : null;
18690
18755
  const messageBody = isFailure
18691
18756
  ? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
@@ -18723,11 +18788,20 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18723
18788
  scene.append(sceneLabel, sceneBody);
18724
18789
  message.querySelector(".message-body")?.prepend(scene);
18725
18790
  }
18791
+ const chapterReferences = state.work?.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
18792
+ id: chapter.id,
18793
+ name: `${volume.title} / ${chapter.title}`
18794
+ }))) ?? [];
18795
+ const settingReferences = state.settings.map((setting) => ({ id: setting.id, name: setting.title }));
18796
+ const contextSettingReferences = [{ id: "include-setting-info", name: "注入上下文设定" }];
18726
18797
  const mentionGroups = role === "user"
18727
18798
  ? [
18728
18799
  ["角色", metadata?.mentionCharacterIds, state.characters],
18729
18800
  ["种族", metadata?.mentionRaceIds, state.races],
18730
- ["组织", metadata?.mentionOrganizationIds, state.organizations]
18801
+ ["组织", metadata?.mentionOrganizationIds, state.organizations],
18802
+ ["设定", metadata?.mentionSettingIds, settingReferences],
18803
+ ["章节", metadata?.mentionChapterIds, chapterReferences],
18804
+ ["能力", metadata?.mentionContextSettingIds, contextSettingReferences]
18731
18805
  ]
18732
18806
  .flatMap(([kind, ids, items]) => userMessageMentionNames(ids, items).map((name) => ({ kind, name })))
18733
18807
  : [];
@@ -21317,7 +21391,15 @@ $("#ai-write-plan-undo").addEventListener("click", () => {
21317
21391
  if (!aiWritePlanDialogPlanId) return;
21318
21392
  undoAiWritePlan(aiWritePlanDialogPlanId);
21319
21393
  });
21320
- $("#ai-question-close").addEventListener("click", () => $("#ai-question-dialog").close());
21394
+ $("#ai-question-close").addEventListener("click", () => {
21395
+ if (!aiQuestionDialogCanClose(currentAiQuestionDialogView)) return;
21396
+ $("#ai-question-dialog").close();
21397
+ });
21398
+ $("#ai-question-dialog").addEventListener("cancel", (event) => {
21399
+ if (aiQuestionDialogCanClose(currentAiQuestionDialogView)) return;
21400
+ event.preventDefault();
21401
+ toast("请先提交全部回答,或选择“暂不回答”让 AI 继续", "warning");
21402
+ });
21321
21403
 
21322
21404
  function syncAiQuestionSubmitState() {
21323
21405
  const submit = $("#ai-question-submit");
@@ -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-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>
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&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3"></script>
1429
1429
  </body>
1430
1430
  </html>