@musnows/scriverse 0.8.2 → 0.8.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.
@@ -48,7 +48,8 @@ export function formatAiContextUsagePercent(occupiedTokens, contextWindow) {
48
48
  const occupied = tokenCount(occupiedTokens);
49
49
  const window = tokenCount(contextWindow);
50
50
  const percent = window > 0 ? Math.min(100, occupied / window * 100) : 0;
51
- return `${percent.toFixed(1)}%`;
51
+ const roundedDecimal = Number(percent.toFixed(1));
52
+ return roundedDecimal < 10 ? `${roundedDecimal.toFixed(1)}%` : `${Math.round(percent)}%`;
52
53
  }
53
54
 
54
55
  export function normalizeAiContextTokenDistribution(usage) {
@@ -24,7 +24,8 @@ import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/
24
24
  import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
25
25
  import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
26
26
  import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
27
- import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260812-context-usage-remaining-v2";
27
+ import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260819-context-percent-v1";
28
+ import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
28
29
  import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
29
30
  import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
30
31
  import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
@@ -1331,12 +1332,15 @@ function constrainPanelLayout() {
1331
1332
  }
1332
1333
 
1333
1334
  function applyPanelLayout(persist = false) {
1335
+ const phoneClient = isPhoneClient();
1336
+ if (phoneClient && !aiConversationWorkspaceOpen) panelLayout.aiCollapsed = true;
1334
1337
  constrainPanelLayout();
1335
1338
  const app = $("#app");
1336
1339
  app.style.setProperty("--left-panel-width", `${panelLayout.leftWidth}px`);
1337
1340
  app.style.setProperty("--ai-panel-width", `${panelLayout.aiWidth}px`);
1338
1341
  app.classList.toggle("left-panel-collapsed", panelLayout.leftCollapsed);
1339
1342
  app.classList.toggle("ai-panel-collapsed", panelLayout.aiCollapsed);
1343
+ app.classList.toggle("phone-client", phoneClient);
1340
1344
  const mobileViewport = isMobileViewport();
1341
1345
  $("#left-panel-toggle").textContent = mobileViewport ? "›" : (panelLayout.leftCollapsed ? "›" : "‹");
1342
1346
  $("#left-panel-toggle").setAttribute("aria-expanded", String(!panelLayout.leftCollapsed));
@@ -1356,7 +1360,7 @@ function applyPanelLayout(persist = false) {
1356
1360
 
1357
1361
  function ensureAiPanelExpanded() {
1358
1362
  if (!state.work || !canReadPermissionModule(state.work, "ai-chat")) return;
1359
- if (isMobileViewport()) {
1363
+ if (isPhoneClient() || isMobileViewport()) {
1360
1364
  setAiConversationWorkspaceVisible(true);
1361
1365
  return;
1362
1366
  }
@@ -2219,7 +2223,7 @@ function setAiConversationSwitcherVisible(visible) {
2219
2223
  }
2220
2224
 
2221
2225
  function setAiConversationWorkspaceVisible(visible) {
2222
- const mobileWorkspace = isMobileViewport();
2226
+ const mobileWorkspace = isPhoneClient() || isMobileViewport();
2223
2227
  aiConversationWorkspaceOpen = Boolean(visible);
2224
2228
  panelLayout.aiCollapsed = aiConversationWorkspaceOpen ? false : mobileWorkspace;
2225
2229
  $("#app").classList.toggle("ai-workspace-mode", aiConversationWorkspaceOpen);
@@ -3251,6 +3255,10 @@ function renderAiRoleplayUserCharacterSelect() {
3251
3255
 
3252
3256
  const aiConversationOptionLockedMessage = "会话选项在会话开始后不支持修改,若需要修改,请新建会话";
3253
3257
 
3258
+ function aiConversationModelLocked() {
3259
+ return typeof state.aiConversationModelId === "string" && state.aiConversationModelId.trim().length > 0;
3260
+ }
3261
+
3254
3262
  function selectedAiModelLabel() {
3255
3263
  return $("#ai-model").selectedOptions[0]?.textContent?.trim() || "尚未选择模型";
3256
3264
  }
@@ -3259,20 +3267,21 @@ function syncAiModelPicker() {
3259
3267
  const select = $("#ai-model");
3260
3268
  const button = $("#ai-model-picker");
3261
3269
  const interactionBusy = aiInteractionBusy();
3270
+ const modelLocked = aiConversationModelLocked();
3262
3271
  const selectedLabel = selectedAiModelLabel();
3263
- const label = state.aiPromptSent
3272
+ const label = modelLocked
3264
3273
  ? `当前模型:${selectedLabel}。${aiConversationOptionLockedMessage}`
3265
3274
  : `选择实际使用模型:${selectedLabel}`;
3266
3275
  select.disabled = interactionBusy;
3267
- select.title = state.aiPromptSent ? aiConversationOptionLockedMessage : "";
3276
+ select.title = modelLocked ? aiConversationOptionLockedMessage : "";
3268
3277
  button.disabled = interactionBusy;
3269
3278
  button.title = label;
3270
3279
  button.setAttribute("aria-label", label);
3271
3280
  if (interactionBusy) setAiModelPickerVisible(false);
3272
3281
  }
3273
3282
 
3274
- function notifyAiConversationOptionLocked(select) {
3275
- if (!state.aiPromptSent) return false;
3283
+ function notifyAiConversationOptionLocked(select, locked = state.aiPromptSent) {
3284
+ if (!locked) return false;
3276
3285
  const now = Date.now();
3277
3286
  const lastToastAt = Number(select.dataset.lockedToastAt ?? 0);
3278
3287
  if (now - lastToastAt > 500) toast(aiConversationOptionLockedMessage);
@@ -3280,9 +3289,16 @@ function notifyAiConversationOptionLocked(select) {
3280
3289
  return true;
3281
3290
  }
3282
3291
 
3292
+ function notifyAiConversationModelLocked(control) {
3293
+ return notifyAiConversationOptionLocked(control, aiConversationModelLocked());
3294
+ }
3295
+
3283
3296
  function blockLockedAiConversationOptionInteraction(event) {
3284
3297
  const select = event.currentTarget;
3285
- if (!notifyAiConversationOptionLocked(select)) return;
3298
+ const locked = select.id === "ai-model"
3299
+ ? notifyAiConversationModelLocked(select)
3300
+ : notifyAiConversationOptionLocked(select);
3301
+ if (!locked) return;
3286
3302
  event.preventDefault();
3287
3303
  event.stopPropagation();
3288
3304
  }
@@ -3808,8 +3824,7 @@ function renderChapterAnnotations() {
3808
3824
  },
3809
3825
  locate: (annotation) => {
3810
3826
  $("#chapter-annotations-dialog").close();
3811
- paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
3812
- selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
3827
+ revealChapterLines(annotation.startLine, annotation.endLine);
3813
3828
  },
3814
3829
  overlayDialog: $("#chapter-annotations-dialog")
3815
3830
  });
@@ -6385,7 +6400,7 @@ async function runWorkSearch() {
6385
6400
  renderSearchResults(results, query);
6386
6401
  }
6387
6402
 
6388
- function revealChapterSearchLines(startLine, endLine) {
6403
+ function revealChapterLines(startLine, endLine) {
6389
6404
  const start = Math.max(0, Number(startLine) - 1);
6390
6405
  const end = Math.max(start, Number(endLine ?? startLine) - 1);
6391
6406
  const input = $("#chapter-content");
@@ -6440,7 +6455,7 @@ async function openSearchResult(result) {
6440
6455
  if (target.kind === "chapter") {
6441
6456
  await selectChapter(target.id);
6442
6457
  if (state.chapter?.id === target.id && target.startLine) {
6443
- revealChapterSearchLines(target.startLine, target.endLine);
6458
+ revealChapterLines(target.startLine, target.endLine);
6444
6459
  toast(target.startLine === target.endLine ? `已定位到第 ${target.startLine} 行` : `已定位到第 ${target.startLine}-${target.endLine} 行`);
6445
6460
  }
6446
6461
  return;
@@ -9441,10 +9456,10 @@ async function renderWorkChapterComments(page = moduleListPages.comments) {
9441
9456
  bindChapterAnnotationCards($("#module-content"), result.items, {
9442
9457
  refresh: () => renderWorkChapterComments(pageResult.page),
9443
9458
  locate: async (annotation) => {
9444
- await selectChapter(annotation.chapterId);
9459
+ const selected = await selectChapter(annotation.chapterId);
9460
+ if (!selected || String(state.chapter?.id ?? "") !== String(annotation.chapterId)) return;
9445
9461
  await new Promise((resolve) => window.requestAnimationFrame(resolve));
9446
- paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
9447
- selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
9462
+ revealChapterLines(annotation.startLine, annotation.endLine);
9448
9463
  }
9449
9464
  });
9450
9465
  }
@@ -12909,8 +12924,17 @@ function updateVditorWordCount(editor, markdown) {
12909
12924
  counter.title = "当前 Markdown 正文的字数,不计空白字符";
12910
12925
  toolbar.append(counter);
12911
12926
  }
12927
+ let value = counter.querySelector("[data-markdown-word-count-value]");
12928
+ if (!value) {
12929
+ value = document.createElement("span");
12930
+ value.className = "markdown-word-count-value";
12931
+ value.dataset.markdownWordCountValue = "true";
12932
+ const unit = document.createElement("span");
12933
+ unit.textContent = " 字";
12934
+ counter.replaceChildren(value, unit);
12935
+ }
12912
12936
  const count = Array.from(String(markdown ?? "").replace(/\s/gu, "")).length;
12913
- counter.textContent = `${count.toLocaleString("zh-CN")} 字`;
12937
+ value.textContent = count.toLocaleString("zh-CN");
12914
12938
  }
12915
12939
 
12916
12940
  function transformVditorPreview(html) {
@@ -15013,6 +15037,18 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
15013
15037
  }
15014
15038
  }
15015
15039
 
15040
+ function createAiStreamCharacterCount(value) {
15041
+ const count = document.createElement("span");
15042
+ count.className = "ai-stream-character-count";
15043
+ count.textContent = Math.max(0, Number(value) || 0).toLocaleString("zh-CN");
15044
+ return count;
15045
+ }
15046
+
15047
+ function renderAiStreamingCharacterProgress(meta, visibleCharacters) {
15048
+ const visible = Math.max(0, Number(visibleCharacters) || 0);
15049
+ meta.replaceChildren("正在生成 · ", createAiStreamCharacterCount(visible), " 字");
15050
+ }
15051
+
15016
15052
  async function streamChat(requestHolder, body, idempotencyKey) {
15017
15053
  const tab = aiChatTabForRequest(requestHolder.snapshot);
15018
15054
  if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
@@ -15039,10 +15075,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
15039
15075
  onRender: (text, progress) => {
15040
15076
  if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
15041
15077
  content.innerHTML = renderMarkdown(text);
15042
- const receivedCharacters = progress.visibleCharacters + progress.pendingCharacters;
15043
- meta.textContent = progress.pendingCharacters > 0
15044
- ? `正在生成 · ${progress.visibleCharacters} / ${receivedCharacters} 字`
15045
- : `正在生成 · ${progress.visibleCharacters} 字`;
15078
+ renderAiStreamingCharacterProgress(meta, progress.visibleCharacters);
15046
15079
  scrollAiFeedToBottom(feed);
15047
15080
  }
15048
15081
  });
@@ -15152,7 +15185,6 @@ async function streamChat(requestHolder, body, idempotencyKey) {
15152
15185
  streamedText += delta;
15153
15186
  streamedPendingText += delta;
15154
15187
  typewriter.append(delta);
15155
- meta.textContent = "正在生成回复……";
15156
15188
  } else if (eventName === "process_step") {
15157
15189
  mountAssistantMessage();
15158
15190
  const step = { ...payload };
@@ -16919,7 +16951,7 @@ $("#ai-assistant-entry").addEventListener("click", () => {
16919
16951
  return toast("当前账户没有创作助手读取权限", "error");
16920
16952
  }
16921
16953
  setModuleNavExpanded(false);
16922
- if (isMobileViewport()) {
16954
+ if (isPhoneClient() || isMobileViewport()) {
16923
16955
  panelLayout.leftCollapsed = true;
16924
16956
  applyPanelLayout(true);
16925
16957
  setAiConversationWorkspaceVisible(true);
@@ -16970,7 +17002,7 @@ $("#ai-model").addEventListener("focus", () => {
16970
17002
  ensureAiModelsLoaded().catch((error) => toast(`模型加载失败:${error.message}`, "error"));
16971
17003
  });
16972
17004
  $("#ai-model").addEventListener("change", (event) => {
16973
- if (state.aiPromptSent) {
17005
+ if (aiConversationModelLocked()) {
16974
17006
  event.currentTarget.value = state.aiConversationModelId ?? event.currentTarget.value;
16975
17007
  syncAiModelPicker();
16976
17008
  return toast(aiConversationOptionLockedMessage);
@@ -16981,7 +17013,7 @@ $("#ai-model").addEventListener("change", (event) => {
16981
17013
  });
16982
17014
  $("#ai-model-picker").addEventListener("click", async (event) => {
16983
17015
  const button = event.currentTarget;
16984
- if (notifyAiConversationOptionLocked(button)) return;
17016
+ if (notifyAiConversationModelLocked(button)) return;
16985
17017
  const willOpen = $("#ai-model-popover").classList.contains("hidden");
16986
17018
  setAiContextDistributionVisible(false);
16987
17019
  setAiModelPickerVisible(willOpen);
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <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=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1">
13
+ <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=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=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -1210,6 +1210,6 @@
1210
1210
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1211
1211
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1212
1212
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1213
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-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-v1&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-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-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-model-picker-v1"></script>
1213
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-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-v1&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-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-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-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=phone-client-entry-v1"></script>
1214
1214
  </body>
1215
1215
  </html>
@@ -0,0 +1,6 @@
1
+ export function isPhoneClient(navigatorLike = globalThis.navigator) {
2
+ const mobile = navigatorLike?.userAgentData?.mobile;
3
+ if (typeof mobile === "boolean") return mobile;
4
+ const userAgent = String(navigatorLike?.userAgent ?? "");
5
+ return /(?:Android.*Mobile|iPhone|iPod|IEMobile|Windows Phone|Opera Mini)/iu.test(userAgent);
6
+ }
@@ -561,6 +561,8 @@ input[type="checkbox"][hidden] { display: none; }
561
561
  .panel-resize-handle:hover::after, .panel-resize-handle:focus-visible::after, body.is-panel-resizing .panel-resize-handle::after { background: var(--accent); }
562
562
  body.is-panel-resizing { cursor: col-resize; user-select: none; }
563
563
  .app-shell.left-panel-collapsed { --left-panel-width: 42px !important; }.app-shell.ai-panel-collapsed { --ai-panel-width: 42px !important; }
564
+ .app-shell.phone-client.ai-panel-collapsed:not(.ai-workspace-mode) { --ai-panel-width: 0px !important; }
565
+ .app-shell.phone-client.ai-panel-collapsed:not(.ai-workspace-mode) .ai-panel { display: none; }
564
566
  .app-shell.left-panel-collapsed .left-panel, .app-shell.ai-panel-collapsed .ai-panel { padding: 0; overflow: hidden; }
565
567
  .app-shell.left-panel-collapsed .left-panel > .panel-resize-handle,
566
568
  .app-shell.left-panel-collapsed .left-panel-body > :not(.left-actions),
@@ -1415,7 +1417,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1415
1417
  .chapter-line-number { position: absolute; left: 0; box-sizing: border-box; display: flex; width: 100%; align-items: flex-start; justify-content: center; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; font-variant-numeric: tabular-nums; text-align: center; }
1416
1418
  .chapter-line-number-select { position: relative; display: flex; width: 100%; height: 100%; align-items: flex-start; justify-content: center; padding-inline: 3px; border: 0; background: transparent; color: inherit; font: inherit; font-variant-numeric: tabular-nums; cursor: pointer; }
1417
1419
  .chapter-line-number-select:focus-visible { z-index: 1; outline: 1px solid var(--accent); outline-offset: -1px; }
1418
- .chapter-line-annotation-count { position: absolute; top: -2px; right: 0; z-index: 2; display: grid; min-width: 14px; height: 14px; place-items: center; padding: 0 3px; border: 1px solid var(--panel); border-radius: 999px; background: var(--accent); color: var(--paper); font-family: var(--font-latin), monospace; font-size: 9px; font-weight: 700; line-height: 1; cursor: pointer; box-shadow: 0 1px 3px rgba(49,42,32,.16); }
1420
+ .chapter-line-annotation-count { position: absolute; top: -4px; right: -10px; z-index: 2; display: grid; min-width: 14px; height: 14px; place-items: center; padding: 0 3px; border: 1px solid var(--panel); border-radius: 999px; background: var(--accent); color: var(--paper); font-family: var(--font-latin), monospace; font-size: 9px; font-weight: 700; line-height: 1; cursor: pointer; box-shadow: 0 1px 3px rgba(49,42,32,.16); }
1419
1421
  .chapter-line-annotation-count:hover, .chapter-line-annotation-count:focus-visible { background: var(--accent-dark); }
1420
1422
  .chapter-line-annotation-count:focus-visible { outline: 1px solid var(--accent-dark); outline-offset: 1px; }
1421
1423
  .chapter-line-number:hover { background: rgba(139,61,44,.08); color: var(--accent-dark); }
@@ -2655,6 +2657,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2655
2657
  .message-body .markdown-image img { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
2656
2658
  .message-body .markdown-image small { color: var(--muted); font-size: 9px; line-height: 1.45; text-align: center; }
2657
2659
  .message-meta { margin-top: 8px; color: var(--muted); font-size: 9px; }
2660
+ .message-meta .ai-stream-character-count { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; text-align: right; }
2658
2661
  .ai-process-details { margin: 0 0 10px; overflow: hidden; border: 1px solid var(--line); border-radius: 5px; background: color-mix(in srgb, var(--surface-soft) 82%, transparent); }
2659
2662
  .ai-process-details > summary { display: flex; align-items: center; justify-content: space-between; gap: 9px; padding: 8px 10px; color: var(--muted); cursor: pointer; font-size: 10px; list-style: none; }
2660
2663
  .ai-process-details > summary::-webkit-details-marker { display: none; }
@@ -3321,6 +3324,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3321
3324
  .vditor-editor-host.vditor--dark { --border-color: var(--line); --panel-background-color: var(--surface); --toolbar-background-color: var(--surface-soft); --toolbar-icon-color: var(--muted); --toolbar-icon-hover-color: var(--accent); --textarea-background-color: var(--surface); --textarea-text-color: var(--ink); --heading-border-color: var(--line); --blockquote-color: var(--muted); }
3322
3325
  .vditor-editor-host .vditor-toolbar { display: flex; align-items: center; flex: 0 0 auto; }
3323
3326
  .vditor-editor-host .markdown-word-count { display: inline-flex; align-items: center; align-self: center; min-height: 28px; margin-left: 24px; padding: 0 10px; color: var(--muted); font: 10px/1 var(--font-latin), var(--font-cjk), sans-serif; white-space: nowrap; }
3327
+ .vditor-editor-host .markdown-word-count-value { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; text-align: right; }
3324
3328
  .vditor-editor-host .vditor-content { position: relative; }
3325
3329
  .vditor-editor-host .vditor-line-numbers { display: none; position: absolute; z-index: 1; inset: 0 auto 0 0; width: 42px; padding: 0; overflow: hidden; border-right: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 88%, transparent); color: var(--muted); font: 12px/1.75 var(--font-latin), monospace; text-align: center; user-select: none; pointer-events: none; will-change: transform; }
3326
3330
  .vditor-editor-host.vditor.show-line-numbers .vditor-line-numbers { display: block; }
package/dist/store.js CHANGED
@@ -5582,12 +5582,18 @@ export class Store {
5582
5582
  throw notFound("AI 对话");
5583
5583
  if (requiredString(conversation, "work_id") !== workId)
5584
5584
  throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
5585
- const message = this.db.get(`SELECT metadata_json FROM ai_conversation_messages
5585
+ return this.aiConversationLockedModelId(conversationId);
5586
+ }
5587
+ aiConversationLockedModelId(conversationId) {
5588
+ const messages = this.db.all(`SELECT metadata_json FROM ai_conversation_messages
5586
5589
  WHERE conversation_id = ? AND role = 'user'
5587
- ORDER BY created_at, rowid
5588
- LIMIT 1`, conversationId);
5589
- const metadata = message ? json(requiredString(message, "metadata_json"), {}) : {};
5590
- return typeof metadata.modelId === "string" && metadata.modelId.trim() ? metadata.modelId.trim() : null;
5590
+ ORDER BY created_at, rowid`, conversationId);
5591
+ for (const message of messages) {
5592
+ const metadata = json(requiredString(message, "metadata_json"), {});
5593
+ if (typeof metadata.modelId === "string" && metadata.modelId.trim())
5594
+ return metadata.modelId.trim();
5595
+ }
5596
+ return null;
5591
5597
  }
5592
5598
  getAiConversationInjectedEntities(conversationId, workId) {
5593
5599
  const conversation = this.db.get("SELECT work_id, injected_entities_json FROM ai_conversations WHERE id = ?", conversationId);
@@ -6070,7 +6076,11 @@ export class Store {
6070
6076
  ? JSON.stringify(normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools))
6071
6077
  : String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, timestamp, timestamp, currentRequestActor()?.userId ?? null);
6072
6078
  for (const message of messages.slice(0, targetIndex + 1)) {
6073
- this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", id("message"), forkId, requiredString(message, "role"), requiredString(message, "content"), requiredString(message, "citations_json"), requiredString(message, "metadata_json"), optionalString(message, "request_id"), requiredString(message, "created_at"), currentRequestActor()?.userId ?? null);
6079
+ const role = requiredString(message, "role");
6080
+ const inheritedMetadata = json(requiredString(message, "metadata_json"), {});
6081
+ if (role === "user")
6082
+ delete inheritedMetadata.modelId;
6083
+ this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", id("message"), forkId, role, requiredString(message, "content"), requiredString(message, "citations_json"), JSON.stringify(inheritedMetadata), optionalString(message, "request_id"), requiredString(message, "created_at"), currentRequestActor()?.userId ?? null);
6074
6084
  }
6075
6085
  if (normalizedRequestId) {
6076
6086
  this.db.run("INSERT INTO ai_conversation_forks (source_conversation_id, source_message_id, request_id, conversation_id, created_at) VALUES (?, ?, ?, ?, ?)", conversationId, messageId, normalizedRequestId, forkId, timestamp);
@@ -6110,16 +6120,7 @@ export class Store {
6110
6120
  mapAiConversation(row) {
6111
6121
  const roleplayCharacterId = optionalString(row, "roleplay_character_id");
6112
6122
  const roleplayUserCharacterId = optionalString(row, "roleplay_user_character_id");
6113
- const firstUserMessage = this.db.get(`SELECT metadata_json FROM ai_conversation_messages
6114
- WHERE conversation_id = ? AND role = 'user'
6115
- ORDER BY created_at, rowid
6116
- LIMIT 1`, requiredString(row, "id"));
6117
- const firstUserMetadata = firstUserMessage
6118
- ? json(requiredString(firstUserMessage, "metadata_json"), {})
6119
- : {};
6120
- const lockedModelId = typeof firstUserMetadata.modelId === "string" && firstUserMetadata.modelId.trim()
6121
- ? firstUserMetadata.modelId.trim()
6122
- : null;
6123
+ const lockedModelId = this.aiConversationLockedModelId(requiredString(row, "id"));
6123
6124
  const roleplayCharacter = roleplayCharacterId
6124
6125
  ? this.db.get("SELECT id, name, code FROM characters WHERE id = ? AND work_id = ?", roleplayCharacterId, requiredString(row, "work_id"))
6125
6126
  : undefined;