@musnows/scriverse 0.8.6 → 0.8.7

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.
@@ -16,7 +16,7 @@ import {
16
16
  visibleForeshadowReminders
17
17
  } from "/foreshadow-reminder.js?v=20260812-editor-reminder-v1";
18
18
  import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
19
- import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, modelThinkingEffortLabel, supportsMultimodalModelProtocol } from "/model-config.js?v=20260821-ai-model-thinking-label-v1&feature=ai-provider-responses-v1";
19
+ import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, modelThinkingEffortLabel, supportsMultimodalModelProtocol } from "/model-config.js?v=20260822-ai-model-thinking-label-v3&feature=ai-provider-responses-v1";
20
20
  import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260812-connectivity-cooldown-v1";
21
21
  import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
22
22
  import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
@@ -6986,6 +6986,7 @@ async function showSettingsHub() {
6986
6986
  state.dirty = false;
6987
6987
  }
6988
6988
  dismissChapterInsightToast();
6989
+ dismissTokenUsageDetails({ restoreFocus: false });
6989
6990
  dismissDeleteToasts();
6990
6991
  updateDocumentTitle(state.work);
6991
6992
  $("#app").classList.add("shelf-mode");
@@ -7034,6 +7035,7 @@ async function showPlatformAi() {
7034
7035
  if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
7035
7036
  state.dirty = false;
7036
7037
  dismissChapterInsightToast();
7038
+ dismissTokenUsageDetails({ restoreFocus: false });
7037
7039
  dismissDeleteToasts();
7038
7040
  updateDocumentTitle();
7039
7041
  $("#app").classList.add("shelf-mode");
@@ -7057,6 +7059,7 @@ async function showPlatformUsage() {
7057
7059
  if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入 Token 用量面板将放弃本地修改。是否继续?"))) return false;
7058
7060
  state.dirty = false;
7059
7061
  dismissChapterInsightToast();
7062
+ dismissTokenUsageDetails({ restoreFocus: false });
7060
7063
  dismissDeleteToasts();
7061
7064
  updateDocumentTitle();
7062
7065
  $("#app").classList.add("shelf-mode");
@@ -11705,6 +11708,135 @@ function scrollUsageCalendarsToLatest(root) {
11705
11708
  });
11706
11709
  }
11707
11710
 
11711
+ let tokenUsageDetailsTrigger = null;
11712
+
11713
+ function tokenUsageDetailCount(value) {
11714
+ return Math.max(0, Math.round(Number(value) || 0)).toLocaleString("zh-CN");
11715
+ }
11716
+
11717
+ function dismissTokenUsageDetails({ restoreFocus = true } = {}) {
11718
+ const region = $("#toast-region");
11719
+ const element = region.querySelector("#token-usage-details-toast");
11720
+ if (!element) return;
11721
+ element.remove();
11722
+ const trigger = tokenUsageDetailsTrigger;
11723
+ tokenUsageDetailsTrigger = null;
11724
+ region.classList.remove("token-usage-details-region");
11725
+ if (trigger) {
11726
+ trigger.setAttribute("aria-expanded", "false");
11727
+ if (restoreFocus && trigger.isConnected) trigger.focus({ preventScroll: true });
11728
+ }
11729
+ if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
11730
+ region.hidePopover();
11731
+ }
11732
+ }
11733
+
11734
+ function appendTokenUsageDetailRow(parent, label, usage, isSummary = false) {
11735
+ const row = document.createElement("tr");
11736
+ if (isSummary) row.className = "is-summary";
11737
+ const model = document.createElement("th");
11738
+ model.scope = "row";
11739
+ model.textContent = label;
11740
+ row.append(model);
11741
+ ["directInputTokens", "cacheWriteInputTokens", "cacheReadInputTokens", "outputTokens", "totalTokens", "requestCount", "estimatedRequestCount"].forEach((key) => {
11742
+ const cell = document.createElement("td");
11743
+ cell.textContent = tokenUsageDetailCount(usage?.[key]);
11744
+ row.append(cell);
11745
+ });
11746
+ parent.append(row);
11747
+ }
11748
+
11749
+ function showTokenUsageDetails(usage, title, trigger) {
11750
+ dismissTokenUsageDetails({ restoreFocus: false });
11751
+ const region = $("#toast-region");
11752
+ const element = document.createElement("section");
11753
+ element.id = "token-usage-details-toast";
11754
+ element.className = "toast token-usage-details-toast";
11755
+ element.setAttribute("role", "dialog");
11756
+ element.setAttribute("aria-labelledby", "token-usage-details-title");
11757
+ element.setAttribute("aria-describedby", "token-usage-details-description");
11758
+
11759
+ const header = document.createElement("header");
11760
+ header.className = "token-usage-details-header";
11761
+ const heading = document.createElement("div");
11762
+ heading.className = "token-usage-details-heading";
11763
+ const headingTitle = document.createElement("strong");
11764
+ headingTitle.id = "token-usage-details-title";
11765
+ headingTitle.textContent = "Token 用量详细数据";
11766
+ const headingScope = document.createElement("span");
11767
+ headingScope.textContent = title;
11768
+ heading.append(headingTitle, headingScope);
11769
+ const close = document.createElement("button");
11770
+ close.className = "ghost-button token-usage-details-close";
11771
+ close.type = "button";
11772
+ close.textContent = "关闭";
11773
+ close.setAttribute("aria-label", "关闭 Token 用量详细数据");
11774
+ header.append(heading, close);
11775
+
11776
+ const body = document.createElement("div");
11777
+ body.className = "token-usage-details-body";
11778
+ const description = document.createElement("p");
11779
+ description.id = "token-usage-details-description";
11780
+ description.className = "token-usage-details-note";
11781
+ description.textContent = "Raw Input 为不含缓存的输入 Token;Cache Write 与 Cache Read 也是输入组成部分,并已包含在总计中。汇总行包含当前范围内全部模型。";
11782
+ body.append(description);
11783
+
11784
+ const tableScroll = document.createElement("div");
11785
+ tableScroll.className = "token-usage-details-table-scroll";
11786
+ const table = document.createElement("table");
11787
+ table.className = "token-usage-details-table";
11788
+ const caption = document.createElement("caption");
11789
+ caption.textContent = `${title}的模型 Token 用量`;
11790
+ table.append(caption);
11791
+ const head = document.createElement("thead");
11792
+ const headRow = document.createElement("tr");
11793
+ ["模型", "Raw Input", "Cache Write", "Cache Read", "Output", "总计", "调用", "估算调用"].forEach((label) => {
11794
+ const cell = document.createElement("th");
11795
+ cell.scope = "col";
11796
+ cell.textContent = label;
11797
+ headRow.append(cell);
11798
+ });
11799
+ head.append(headRow);
11800
+ table.append(head);
11801
+ const bodyRows = document.createElement("tbody");
11802
+ const models = Array.isArray(usage?.models) ? usage.models : [];
11803
+ models.forEach((model) => appendTokenUsageDetailRow(bodyRows, model.modelId || "未指定模型", model));
11804
+ if (!models.length) {
11805
+ const emptyRow = document.createElement("tr");
11806
+ const emptyCell = document.createElement("td");
11807
+ emptyCell.colSpan = 8;
11808
+ emptyCell.textContent = "还没有模型用量记录。";
11809
+ emptyRow.append(emptyCell);
11810
+ bodyRows.append(emptyRow);
11811
+ }
11812
+ table.append(bodyRows);
11813
+ const foot = document.createElement("tfoot");
11814
+ appendTokenUsageDetailRow(foot, "汇总", usage?.summary ?? {}, true);
11815
+ table.append(foot);
11816
+ tableScroll.append(table);
11817
+ body.append(tableScroll);
11818
+ element.append(header, body);
11819
+
11820
+ close.addEventListener("click", () => dismissTokenUsageDetails());
11821
+ element.addEventListener("keydown", (event) => {
11822
+ if (event.key !== "Escape") return;
11823
+ event.preventDefault();
11824
+ dismissTokenUsageDetails();
11825
+ });
11826
+ region.append(element);
11827
+ region.classList.add("token-usage-details-region");
11828
+ tokenUsageDetailsTrigger = trigger;
11829
+ trigger.setAttribute("aria-expanded", "true");
11830
+ raiseToastRegion();
11831
+ close.focus({ preventScroll: true });
11832
+ }
11833
+
11834
+ function bindTokenUsageDetails(host, usage, title) {
11835
+ const button = host.querySelector("[data-token-usage-details]");
11836
+ if (!button) return;
11837
+ button.addEventListener("click", () => showTokenUsageDetails(usage, title, button));
11838
+ }
11839
+
11708
11840
  function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false } = {}) {
11709
11841
  const summary = usage?.summary ?? {};
11710
11842
  const totalTokens = Number(summary.totalTokens) || 0;
@@ -11721,7 +11853,7 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
11721
11853
  ? "供应商尚未返回可计算的缓存明细"
11722
11854
  : `${cachedInputTokens.toLocaleString("zh-CN")} / ${cacheEligibleInputTokens.toLocaleString("zh-CN")} 个可统计输入 Token 命中缓存`;
11723
11855
  const estimatedCost = formatEstimatedCost(summary.estimatedCost);
11724
- const pricingDescription = "根据 LiteLLM 模型 ID 价格表估算,价格单位为美元;未匹配模型不计入。";
11856
+ const pricingDescription = "根据多来源模型价格表估算,价格单位为美元;未匹配模型不计入。";
11725
11857
  const pricingBadge = summary.pricingAvailable === true && summary.estimatedCost !== null && summary.estimatedCost !== undefined
11726
11858
  ? `<span class="usage-cost-bubble" title="${esc(pricingDescription)}">估价 ${esc(estimatedCost)}</span>`
11727
11859
  : "";
@@ -11739,7 +11871,7 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
11739
11871
  <td>${Number(work.requestCount || 0).toLocaleString("zh-CN")}</td>
11740
11872
  </tr>`).join("");
11741
11873
  return `<section class="usage-overview" aria-labelledby="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">
11742
- <div class="config-section-header"><div><h2 id="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">${esc(title || "Token 用量")}</h2><p>${esc(description || "统计该范围内的全部 AI 调用。")}</p></div></div>
11874
+ <div class="config-section-header usage-overview-header"><div><h2 id="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">${esc(title || "Token 用量")}</h2><p>${esc(description || "统计该范围内的全部 AI 调用。")}</p></div><button class="ghost-button usage-details-button" type="button" data-token-usage-details aria-haspopup="dialog" aria-expanded="false" aria-controls="token-usage-details-toast">详细数据</button></div>
11743
11875
  <div class="usage-stat-grid">
11744
11876
  <article class="usage-stat is-primary"><div class="usage-stat-label"><span>总消耗</span>${pricingBadge}</div><strong title="${esc(exactTotal)} Token">${esc(formatTokenCount(totalTokens))}</strong><small>${esc(exactTotal)} Token</small></article>
11745
11877
  <article class="usage-stat"><span>输入 Token</span><strong>${esc(formatTokenCount(summary.inputTokens))}</strong><small title="${esc(inputDescription)}">${esc(inputDescription)}</small></article>
@@ -11765,6 +11897,7 @@ async function renderPlatformTokenUsage() {
11765
11897
  description: "汇总所有作品迄今产生的输入与输出 Token;缓存命中率仅基于供应商返回了缓存明细的调用。",
11766
11898
  showWorks: true
11767
11899
  });
11900
+ bindTokenUsageDetails(host, usage, "项目累计用量");
11768
11901
  bindUsageCalendarInteractions(host);
11769
11902
  scrollUsageCalendarsToLatest(host);
11770
11903
  }
@@ -11809,6 +11942,7 @@ async function renderBookAiSettings() {
11809
11942
  title: "本书 Token 用量",
11810
11943
  description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
11811
11944
  })}</section><section class="config-section"><div class="config-section-header"><div><h2>每日 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然日可使用的输入与输出 Token 总量。额度必须设置为大于 0 的整数;低于 10,000 时仅提示风险;达到额度后,新的 AI 请求会等到后端时区的次日零点重置后再执行。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="daily-token-quota-enabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}>启用每日额度</label><label class="daily-token-quota-field">每日额度<input id="daily-token-quota" type="number" min="1" max="2000000000" step="1" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="本书每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label><button id="save-daily-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="daily-token-quota-status" class="usage-measurement-note" role="status">${esc(quotaStatusText)}</p></section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>该阈值按对话历史的独立预算计算,用于显示可选择压缩或忽略的提醒;整次请求达到模型上下文窗口 95% 时仍会强制压缩较早消息,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>设定上下文注入</h2><p>开启后,本书的普通 AI 请求会自动注入锁定设定、组织、种族与相关约束;即使本轮同时使用“@注入上下文设定”,也只会注入一次。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="always-include-setting-info" type="checkbox" ${settings.alwaysIncludeSettingInfo ? "checked" : ""}>是否注入设定</label><button id="save-always-include-setting-info" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>Agent 工具调用上限</h2><p>限制单次回答里 Agent 可调用工具的次数,并用「全局倍数」给整次回答加一道不会因 Compact 重置的熔断阀,防止工具死循环空耗 Token。调用上限 5–48(默认 12);全局倍数 1–6(默认 3,全局上限 = 调用上限 × 倍数)。<a class="config-doc-link" href="https://scriverse.top/docs/global-tool-call-limit.html" target="_blank" rel="noopener noreferrer">了解原理与推荐设置</a></p></div></div><div class="config-inline-save"><label class="agent-tool-call-limit-field">调用上限<input id="agent-tool-call-limit" type="number" min="5" max="48" value="${esc(String(settings.agentToolCallLimit ?? 12))}" aria-label="Agent 工具调用上限"></label><div class="agent-tool-call-global-multiplier-field"><span id="agent-tool-call-global-multiplier-label">全局倍数</span><div class="settings-layout-toggle agent-tool-call-global-multiplier-toggle" role="group" aria-labelledby="agent-tool-call-global-multiplier-label">${[1, 2, 3, 4, 5, 6].map((value) => `<button type="button" data-global-multiplier="${value}" aria-pressed="${Number(settings.agentToolCallGlobalMultiplier ?? 3) === value}">${value}</button>`).join("")}</div><input id="agent-tool-call-global-multiplier" type="hidden" value="${esc(String(Math.min(6, Math.max(1, Number(settings.agentToolCallGlobalMultiplier ?? 3) || 3))))}" aria-label="Agent 工具调用全局倍数"></div><button id="save-agent-tool-call-limit" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section ai-agent-tools-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。已开始的对话会锁定创建时的工具集,修改后仅对新对话生效,避免打断 prompt cache。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名、拼音或短关键词混合检索设定、人物、组织、时间线、关系、大纲和伏笔;非语义问答。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults, settings)}`;
11945
+ bindTokenUsageDetails(host, usage, "本书 Token 用量");
11812
11946
  const dailyQuotaSection = host.querySelector("#daily-token-quota-enabled")?.closest(".config-section");
11813
11947
  dailyQuotaSection?.insertAdjacentHTML("afterend", `<section class="config-section"><div class="config-section-header"><div><h2>每月 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然月(当月 1 日至月末)可使用的输入与输出 Token 总量。额度必须设置为大于 0 的整数;低于 1,000,000 时仅提示风险;达到额度后,新的 AI 请求会等到下月 1 日零点重置后再执行。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="monthly-token-quota-enabled" type="checkbox" ${monthlyTokenQuota === null ? "" : "checked"}>启用每月额度</label><label class="monthly-token-quota-field">每月额度<input id="monthly-token-quota" type="number" min="1" max="2000000000" step="1" value="${esc(String(monthlyTokenQuota ?? 10000))}" aria-label="本书每月 Token 额度" ${monthlyTokenQuota === null ? "disabled" : ""}></label><button id="save-monthly-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="monthly-token-quota-status" class="usage-measurement-note" role="status">${esc(monthlyQuotaStatusText)}</p></section>`);
11814
11948
  const configureTokenQuotaInput = (input, warningId, threshold) => {
@@ -15824,9 +15958,23 @@ async function streamChat(requestHolder, body, idempotencyKey) {
15824
15958
  const message = document.createElement("div");
15825
15959
  message.className = "assistant-message is-streaming";
15826
15960
  message.dataset.testid = "ai-stream-message";
15827
- message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流……</div>';
15961
+ const streamConnectionStartedAt = Date.now();
15962
+ message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流…… <span class="ai-stream-connection-seconds" data-testid="ai-stream-connection-seconds"></span> 秒</div>';
15828
15963
  const content = message.querySelector(".message-body");
15829
15964
  const meta = message.querySelector(".message-meta");
15965
+ const connectionSeconds = message.querySelector(".ai-stream-connection-seconds");
15966
+ const renderStreamConnectionElapsed = () => {
15967
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - streamConnectionStartedAt) / 1000));
15968
+ connectionSeconds.textContent = String(elapsedSeconds);
15969
+ };
15970
+ renderStreamConnectionElapsed();
15971
+ let streamConnectionTimer = window.setInterval(renderStreamConnectionElapsed, 1000);
15972
+ const stopStreamConnectionTimer = () => {
15973
+ if (streamConnectionTimer === null) return;
15974
+ window.clearInterval(streamConnectionTimer);
15975
+ streamConnectionTimer = null;
15976
+ };
15977
+ const streamConnectionEstablishedEvents = new Set(["delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
15830
15978
  const streamSpeedController = createStreamTypewriterSpeedController();
15831
15979
  let messageMounted = false;
15832
15980
  const mountAssistantMessage = () => {
@@ -15903,6 +16051,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
15903
16051
  let streamError = null;
15904
16052
  const consume = async (eventName, payload) => {
15905
16053
  assertAiRequestCurrent(requestHolder.snapshot);
16054
+ if (streamConnectionEstablishedEvents.has(eventName)) stopStreamConnectionTimer();
15906
16055
  if (eventName === "context") {
15907
16056
  contextAction = typeof payload.action === "string" ? payload.action : "ready";
15908
16057
  if (!tab.promptSent) setAiChatTabContextUsage(tab, payload.usage);
@@ -16014,7 +16163,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
16014
16163
  assertAiRequestCurrent(requestHolder.snapshot);
16015
16164
  message.classList.remove("is-streaming");
16016
16165
  content.setAttribute("aria-busy", "false");
16017
- message.querySelector(".message-heading > span").textContent = "助手";
16166
+ message.querySelector(".message-heading > span").textContent = aiAssistantLabel("", tab.roleplayCharacter);
16018
16167
  toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : toolCalls;
16019
16168
  processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : processSteps;
16020
16169
  const processDurationMs = Number.isFinite(payload.processDurationMs) && payload.processDurationMs >= 0
@@ -16082,6 +16231,8 @@ async function streamChat(requestHolder, body, idempotencyKey) {
16082
16231
  if (streamedText) attachAssistantCopyAction(message, streamedText);
16083
16232
  scrollAiFeedToBottom(feed);
16084
16233
  throw streamFailure;
16234
+ } finally {
16235
+ stopStreamConnectionTimer();
16085
16236
  }
16086
16237
  }
16087
16238
 
@@ -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&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&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">
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&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">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -1245,6 +1245,6 @@
1245
1245
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1246
1246
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1247
1247
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1248
- <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-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-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=phone-client-entry-v1&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&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-v1&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"></script>
1248
+ <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-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-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-v1&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"></script>
1249
1249
  </body>
1250
1250
  </html>
@@ -1,5 +1,5 @@
1
1
  export const MODEL_PURPOSE_OPTIONS: ReadonlyArray<readonly [string, string]>;
2
- export const MODEL_THINKING_EFFORT_OPTIONS: ReadonlyArray<readonly ["default" | "low" | "medium" | "high" | "xhigh" | "max", string]>;
2
+ export const MODEL_THINKING_EFFORT_OPTIONS: ReadonlyArray<readonly ["default" | "auto" | "low" | "medium" | "high" | "xhigh" | "max", string]>;
3
3
  export const MIN_MODEL_CONTEXT_WINDOW: number;
4
4
  export const RECOMMENDED_MODEL_CONTEXT_WINDOW: number;
5
5
  export type ModelProviderProtocolOption = { value: string; supportsMultimodal?: boolean };
@@ -13,7 +13,7 @@ export type ModelFormValues = {
13
13
  temperature: number;
14
14
  maxTokens: number;
15
15
  thinkingEnabled: boolean;
16
- thinkingEffort: "default" | "low" | "medium" | "high" | "xhigh" | "max";
16
+ thinkingEffort: "default" | "auto" | "low" | "medium" | "high" | "xhigh" | "max";
17
17
  multimodalEnabled: boolean;
18
18
  imageToolDefault: boolean;
19
19
  enabled: boolean;
@@ -23,6 +23,6 @@ export function normalizeModelPurposes(purposes: unknown): string[];
23
23
  export function isKimiModelId(modelId: unknown): boolean;
24
24
  export function modelContextWindowGuidance(value: unknown): { belowMinimum: boolean; showRecommendation: boolean };
25
25
  export function modelFormValues(model?: Record<string, unknown> | null): ModelFormValues;
26
- export function modelPayload(values: ModelFormValues, existingPreset?: Record<string, unknown>): Record<string, unknown> & { thinkingEnabled: boolean; thinkingEffort: "default" | "low" | "medium" | "high" | "xhigh" | "max" };
26
+ export function modelPayload(values: ModelFormValues, existingPreset?: Record<string, unknown>): Record<string, unknown> & { thinkingEnabled: boolean; thinkingEffort: "default" | "auto" | "low" | "medium" | "high" | "xhigh" | "max" };
27
27
  export function modelOptionLabel(model: Record<string, unknown> | null | undefined): string;
28
28
  export function modelThinkingEffortLabel(model: Record<string, unknown> | null | undefined): string;
@@ -11,6 +11,7 @@ export const MODEL_PURPOSE_OPTIONS = Object.freeze([
11
11
 
12
12
  export const MODEL_THINKING_EFFORT_OPTIONS = Object.freeze([
13
13
  ["default", "模型默认"],
14
+ ["auto", "自动(auto)"],
14
15
  ["low", "低(low)"],
15
16
  ["medium", "中(medium)"],
16
17
  ["high", "高(high)"],
@@ -104,5 +105,5 @@ export function modelOptionLabel(model) {
104
105
  export function modelThinkingEffortLabel(model) {
105
106
  if (model?.thinkingEnabled !== true) return "";
106
107
  const thinkingEffort = String(model?.thinkingEffort ?? "").trim().toLowerCase();
107
- return thinkingEffort && thinkingEffort !== "default" ? thinkingEffort : "auto";
108
+ return thinkingEffort && thinkingEffort !== "default" ? thinkingEffort : "default";
108
109
  }
@@ -667,6 +667,9 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
667
667
  .settings-work-note { max-width: 1000px; margin: 18px auto 0; color: var(--muted); font-size: 11px; }
668
668
  .usage-page-content { width: 100%; max-width: 1400px; margin: 0 auto; padding-top: 0; }
669
669
  .usage-overview { min-width: 0; container-name: usage-overview; container-type: inline-size; }
670
+ .usage-overview-header { align-items: flex-start; gap: 16px; }
671
+ .usage-overview-header > div { min-width: 0; }
672
+ .usage-details-button { flex: 0 0 auto; min-height: 32px; padding: 7px 11px; font-size: 11px; }
670
673
  .usage-stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
671
674
  .usage-stat {
672
675
  display: grid;
@@ -738,10 +741,41 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
738
741
  .usage-work-table th:first-child, .usage-work-table td:first-child { text-align: left; }
739
742
  .usage-work-table tbody th { max-width: 320px; overflow: hidden; color: var(--ink); font-weight: 600; text-overflow: ellipsis; }
740
743
  .usage-work-table tbody tr:last-child > * { border-bottom: 0; }
744
+ .toast.token-usage-details-toast {
745
+ display: grid;
746
+ grid-template-rows: auto minmax(0, 1fr);
747
+ width: min(720px, calc(100vw - 48px));
748
+ max-width: none;
749
+ max-height: min(72dvh, 560px);
750
+ padding: 0;
751
+ overflow: hidden;
752
+ border-color: color-mix(in srgb, var(--accent) 42%, var(--line));
753
+ background: var(--panel);
754
+ color: var(--ink);
755
+ pointer-events: auto;
756
+ }
757
+ .token-usage-details-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 11px 12px 10px 14px; border-bottom: 1px solid var(--line); }
758
+ .token-usage-details-heading { display: flex; min-width: 0; align-items: center; gap: 8px; }
759
+ .token-usage-details-heading strong { font-size: 12px; }
760
+ .token-usage-details-heading span { min-width: 0; overflow: hidden; color: var(--accent-dark); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
761
+ .token-usage-details-close { flex: 0 0 auto; min-height: 28px; padding: 5px 9px; font-size: 10px; }
762
+ .token-usage-details-body { display: grid; gap: 10px; min-height: 0; padding: 12px 14px 14px; overflow-y: auto; color: var(--muted); font-size: 11px; line-height: 1.6; overscroll-behavior: contain; }
763
+ .token-usage-details-note { margin: 0; }
764
+ .token-usage-details-table-scroll { min-width: 0; overflow-x: auto; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); }
765
+ .token-usage-details-table { width: 100%; min-width: 690px; border-collapse: collapse; color: var(--ink); font: 10px/1.45 var(--font-latin), var(--font-cjk), monospace; }
766
+ .token-usage-details-table caption { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
767
+ .token-usage-details-table th, .token-usage-details-table td { padding: 9px 10px; border-bottom: 1px solid var(--line); text-align: right; white-space: nowrap; }
768
+ .token-usage-details-table thead th { background: var(--surface-soft); color: var(--muted); font-size: 9px; font-weight: 600; letter-spacing: .03em; }
769
+ .token-usage-details-table th:first-child, .token-usage-details-table td:first-child { text-align: left; }
770
+ .token-usage-details-table tbody th { max-width: 240px; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
771
+ .token-usage-details-table tfoot th, .token-usage-details-table tfoot td { border-top: 1px solid color-mix(in srgb, var(--accent) 38%, var(--line)); border-bottom: 0; background: color-mix(in srgb, var(--accent) 6%, var(--surface-soft)); font-weight: 700; }
772
+ .token-usage-details-table tbody tr:last-child > * { border-bottom: 0; }
741
773
  @container usage-overview (max-width: 760px) {
742
774
  .usage-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
743
775
  }
744
776
  @container usage-overview (max-width: 420px) {
777
+ .usage-overview-header { flex-wrap: wrap; }
778
+ .usage-details-button { width: 100%; }
745
779
  .usage-stat-grid { grid-template-columns: minmax(0, 1fr); }
746
780
  }
747
781
  .product-footer { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px 12px; width: 100%; max-width: 1400px; margin: auto auto 0; padding-top: 20px; border-top: 1px solid color-mix(in srgb, var(--line) 76%, transparent); color: var(--muted); font-size: 10px; line-height: 1.5; }
@@ -2716,6 +2750,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2716
2750
  .message-body .markdown-image small { color: var(--muted); font-size: 9px; line-height: 1.45; text-align: center; }
2717
2751
  .message-meta { margin-top: 8px; color: var(--muted); font-size: 9px; }
2718
2752
  .message-meta .ai-stream-character-count { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; text-align: right; }
2753
+ .message-meta .ai-stream-connection-seconds { display: inline-block; min-width: 3ch; font-variant-numeric: tabular-nums; text-align: right; }
2719
2754
  .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); }
2720
2755
  .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; }
2721
2756
  .ai-process-details > summary::-webkit-details-marker { display: none; }
@@ -2907,6 +2942,17 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2907
2942
  .toast-region[data-position="top-right"]:popover-open { top: 78px; right: 24px; bottom: auto; left: auto; }
2908
2943
  .toast-region[data-position="bottom-right"],
2909
2944
  .toast-region[data-position="bottom-right"]:popover-open { top: auto; right: 24px; bottom: 24px; left: auto; }
2945
+ .toast-region.token-usage-details-region,
2946
+ .toast-region.token-usage-details-region:popover-open {
2947
+ top: 50%;
2948
+ right: auto;
2949
+ bottom: auto;
2950
+ left: 50%;
2951
+ width: min(720px, calc(100vw - 32px));
2952
+ max-width: calc(100vw - 32px);
2953
+ transform: translate(-50%, -50%);
2954
+ }
2955
+ .toast-region.token-usage-details-region .token-usage-details-toast { width: 100%; }
2910
2956
  .toast {
2911
2957
  max-width: 360px;
2912
2958
  padding: 11px 15px;
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = "0.8.6";
1
+ export const APP_VERSION = "0.8.7";
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": "0.8.6",
3
+ "version": "0.8.7",
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",