@musnows/scriverse 0.5.9 → 0.5.10

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.
@@ -4,6 +4,7 @@ import { renderMarkdown } from "/markdown.js?v=20260725-ordered-list";
4
4
  import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
5
5
  import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
6
6
  import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
7
+ import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
7
8
  import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
8
9
  import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
9
10
  import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
@@ -141,6 +142,8 @@ let chapterMovePending = false;
141
142
 
142
143
  let timelineMultiSelectEnabled = false;
143
144
  let taskProgressRefreshTimer = null;
145
+ let taskAutoRunEditing = false;
146
+ let taskAutoRunEditingWorkId = null;
144
147
  let relationshipSearchIndexRefreshTimer = null;
145
148
  let backgroundTaskCenterTimer = null;
146
149
  let backgroundTaskCenterRequest = 0;
@@ -3749,6 +3752,10 @@ async function showModule(module) {
3749
3752
  }
3750
3753
  module = fallback;
3751
3754
  }
3755
+ if (module !== "tasks") {
3756
+ taskAutoRunEditing = false;
3757
+ taskAutoRunEditingWorkId = null;
3758
+ }
3752
3759
  if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
3753
3760
  if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
3754
3761
  state.module = module;
@@ -4771,12 +4778,14 @@ async function renderTasks(page = taskListPage) {
4771
4778
  const taskTotal = Number(taskPage.total ?? taskPage.stats?.total ?? tasks.length);
4772
4779
  mountModuleCount(taskTotal);
4773
4780
  const canConfigureAutoRun = canEditModule("tasks") && canEditModule("ai-settings");
4781
+ const autoRunEditing = canConfigureAutoRun && taskAutoRunEditing && taskAutoRunEditingWorkId === state.work.id;
4774
4782
  const pendingCount = Number(taskPage.stats?.pendingCount ?? 0);
4775
4783
  const runningCount = Number(taskPage.stats?.runningCount ?? 0);
4776
4784
  const activeTaskCount = pendingCount + runningCount;
4777
4785
  const runningProgress = runningCount ? analysisTaskProgressValue(taskPage.stats?.runningProgress) : 0;
4778
4786
  const autoRunPaused = Boolean(settings.autoRunEnabled && settings.autoRunPaused);
4779
4787
  const autoRunActive = Boolean(settings.autoRunEnabled && !autoRunPaused);
4788
+ const autoRunPauseLabel = autoRunPaused ? "恢复自动执行" : "暂停自动执行";
4780
4789
  const queueProgressLabel = runningCount
4781
4790
  ? "运行中任务平均进度"
4782
4791
  : autoRunPaused ? "自动执行已暂停" : autoRunActive ? "等待任务开始" : "自动执行已关闭";
@@ -4799,13 +4808,17 @@ async function renderTasks(page = taskListPage) {
4799
4808
  <small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
4800
4809
  <small>开启后会持续执行直到队列清空;临时错误自动退避重试,连续失败达到阈值后暂停。</small>
4801
4810
  </div>
4811
+ <div class="task-auto-run-actions">
4812
+ ${autoRunEditing ? "" : '<button id="task-auto-run-edit" class="ghost-button" type="button">编辑</button>'}
4813
+ </div>
4802
4814
  <div class="task-auto-run-controls">
4803
- <label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""}><span>持续自动执行</span></label>
4804
- <label>同时运行上限<input id="task-auto-run-concurrency" type="number" min="1" max="8" value="${esc(String(settings.autoRunConcurrency ?? 2))}"></label>
4805
- <label>每日任务上限<input id="task-auto-run-daily-limit" type="number" min="0" max="10000" value="${esc(String(settings.autoRunDailyTaskLimit ?? 0))}" aria-describedby="task-auto-run-daily-help"></label>
4806
- <label>连续失败暂停阈值<input id="task-auto-run-failure-threshold" type="number" min="1" max="10" value="${esc(String(settings.autoRunFailureThreshold ?? 3))}"></label>
4807
- <button id="task-auto-run-save" class="primary-button" type="button">保存并生效</button>
4808
- <button id="task-auto-run-toggle" class="ghost-button" type="button" ${settings.autoRunEnabled ? "" : "disabled"}>${autoRunPaused ? "恢复自动执行" : "暂停自动执行"}</button>
4815
+ <label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""} ${autoRunEditing ? "" : "disabled"}><span>持续自动执行</span></label>
4816
+ <label>同时运行上限<input id="task-auto-run-concurrency" type="number" min="1" max="8" value="${esc(String(settings.autoRunConcurrency ?? 2))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}"></label>
4817
+ <label>每日任务上限<input id="task-auto-run-daily-limit" type="number" min="0" max="10000" value="${esc(String(settings.autoRunDailyTaskLimit ?? 0))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}" aria-describedby="task-auto-run-daily-help"></label>
4818
+ <label>连续失败暂停阈值<input id="task-auto-run-failure-threshold" type="number" min="1" max="10" value="${esc(String(settings.autoRunFailureThreshold ?? 3))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}"></label>
4819
+ ${autoRunEditing
4820
+ ? `<button id="task-auto-run-save" class="primary-button" type="button">保存并生效</button><button id="task-auto-run-pause" class="ghost-button" type="button">${autoRunPauseLabel}</button>`
4821
+ : ""}
4809
4822
  </div>
4810
4823
  <p id="task-auto-run-daily-help" class="task-auto-run-help">每日任务上限填 0 表示不限制;达到上限后会在下一个 UTC 自然日自动恢复。</p>
4811
4824
  <p class="task-auto-run-meta">待执行队列 ${pendingCount} 个 · 正在运行 ${runningCount} 个 · ${autoRunPaused ? "已暂停" : autoRunActive ? "持续执行中" : "未开启"}</p>
@@ -4844,6 +4857,12 @@ async function renderTasks(page = taskListPage) {
4844
4857
  await renderTasks(Number(button.dataset.taskPage));
4845
4858
  }));
4846
4859
 
4860
+ $("#task-auto-run-edit")?.addEventListener("click", () => {
4861
+ taskAutoRunEditing = true;
4862
+ taskAutoRunEditingWorkId = state.work.id;
4863
+ void renderTasks(taskListPage).catch((error) => toast(error.message, "error"));
4864
+ });
4865
+
4847
4866
  $("#task-auto-run-save")?.addEventListener("click", async () => {
4848
4867
  const button = $("#task-auto-run-save");
4849
4868
  button.disabled = true;
@@ -4860,15 +4879,16 @@ async function renderTasks(page = taskListPage) {
4860
4879
  toast(updated.autoRunEnabled
4861
4880
  ? `持续自动执行已开启:同时最多 ${updated.autoRunConcurrency} 个`
4862
4881
  : "自动执行已关闭");
4882
+ taskAutoRunEditing = false;
4883
+ taskAutoRunEditingWorkId = null;
4863
4884
  await renderTasks();
4864
- window.setTimeout(() => $("#task-auto-run-save")?.focus(), 0);
4865
4885
  } catch (error) {
4866
4886
  toast(error.message, "error");
4867
4887
  button.disabled = false;
4868
4888
  }
4869
4889
  });
4870
- $("#task-auto-run-toggle")?.addEventListener("click", async () => {
4871
- const button = $("#task-auto-run-toggle");
4890
+ $("#task-auto-run-pause")?.addEventListener("click", async () => {
4891
+ const button = $("#task-auto-run-pause");
4872
4892
  button.disabled = true;
4873
4893
  try {
4874
4894
  if (autoRunPaused) {
@@ -4876,17 +4896,18 @@ async function renderTasks(page = taskListPage) {
4876
4896
  toast("自动执行已恢复");
4877
4897
  } else {
4878
4898
  await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { autoRunEnabled: false } });
4879
- toast("已暂停启动新的分析任务,正在运行的任务会继续完成");
4899
+ toast("已暂停自动执行");
4880
4900
  }
4901
+ taskAutoRunEditing = false;
4902
+ taskAutoRunEditingWorkId = null;
4881
4903
  await refreshBackgroundTaskCenter({ announce: false });
4882
4904
  await renderTasks();
4883
- window.setTimeout(() => $(autoRunPaused ? "#task-auto-run-toggle" : "#task-auto-run-enabled")?.focus(), 0);
4905
+ window.setTimeout(() => $("#task-auto-run-edit")?.focus(), 0);
4884
4906
  } catch (error) {
4885
4907
  toast(error.message, "error");
4886
4908
  button.disabled = false;
4887
4909
  }
4888
4910
  });
4889
-
4890
4911
  $("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
4891
4912
  if (button.disabled) return;
4892
4913
  button.disabled = true;
@@ -4957,6 +4978,51 @@ async function rerunAnalysisTask(taskId, button, { closeDetail = false } = {}) {
4957
4978
  }
4958
4979
  }
4959
4980
 
4981
+ async function rerunAnalysisTaskWithModel(task, button) {
4982
+ const originalLabel = button.textContent;
4983
+ button.disabled = true;
4984
+ button.textContent = "加载模型";
4985
+ try {
4986
+ const models = await api(`/api/works/${encodeURIComponent(task.workId)}/models`);
4987
+ const currentModelId = String(task.model?.id ?? "");
4988
+ const availableModels = models.filter((model) =>
4989
+ model.id !== currentModelId
4990
+ && model.enabled
4991
+ && model.providerStatus === "enabled"
4992
+ && model.providerConnectionStatus === "success"
4993
+ );
4994
+ if (!availableModels.length) throw new Error("当前没有其他可用模型,请先配置并测试模型");
4995
+ $("#form-dialog").close();
4996
+ const currentModelLabel = task.model?.displayName || "运行时默认模型";
4997
+ openDialog("选择重试模型",
4998
+ `<p class="task-rerun-model-intro">当前模型:${esc(currentModelLabel)}。选择其他模型后,将按原任务范围和最新资料创建新任务。</p><label>重试模型<select name="modelId" required aria-label="重试模型">
4999
+ ${availableModels.map((model) => `<option value="${esc(model.id)}">${esc(modelOptionLabel(model))}</option>`).join("")}
5000
+ </select></label>`,
5001
+ async (form) => {
5002
+ const modelId = String(form.get("modelId") ?? "").trim();
5003
+ const selectedModel = availableModels.find((model) => model.id === modelId);
5004
+ const rerun = await api(`/api/tasks/${encodeURIComponent(task.id)}/rerun`, {
5005
+ method: "POST",
5006
+ body: { modelId }
5007
+ });
5008
+ toast(`已使用${selectedModel ? `“${modelOptionLabel(selectedModel)}”` : "新模型"}创建重试任务 ${rerun.id}`);
5009
+ await refreshBackgroundTaskCenter({ announce: false });
5010
+ if (state.module === "tasks" && state.work?.id === task.workId) await renderTasks();
5011
+ },
5012
+ "AI 分析重试",
5013
+ {
5014
+ submitLabel: "换模型重试",
5015
+ pendingLabel: "创建中…",
5016
+ pendingMessage: "正在使用新模型创建重试任务,请稍候",
5017
+ errorPrefix: "换模型重试失败:"
5018
+ });
5019
+ } catch (error) {
5020
+ toast(error.message, "error");
5021
+ button.disabled = false;
5022
+ button.textContent = originalLabel;
5023
+ }
5024
+ }
5025
+
4960
5026
  function stopTaskProgressRefresh() {
4961
5027
  if (taskProgressRefreshTimer === null) return;
4962
5028
  window.clearTimeout(taskProgressRefreshTimer);
@@ -5407,7 +5473,7 @@ function openTaskDetailDialog(task, trace) {
5407
5473
  <div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
5408
5474
  <div><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
5409
5475
  <div><strong>结果摘要</strong>${resultPreview}</div>
5410
- ${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
5476
+ ${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><button class="ghost-button" type="button" data-rerun-task-model="${esc(task.id)}">换模型重试</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
5411
5477
  </section>
5412
5478
  ${renderTaskTraceVisualization(trace, task.id)}
5413
5479
  </div>`,
@@ -5419,6 +5485,9 @@ function openTaskDetailDialog(task, trace) {
5419
5485
  $("#dialog-fields").querySelector("[data-rerun-task-detail]")?.addEventListener("click", async (event) => {
5420
5486
  await rerunAnalysisTask(event.currentTarget.dataset.rerunTaskDetail, event.currentTarget, { closeDetail: true });
5421
5487
  });
5488
+ $("#dialog-fields").querySelector("[data-rerun-task-model]")?.addEventListener("click", async (event) => {
5489
+ await rerunAnalysisTaskWithModel(task, event.currentTarget);
5490
+ });
5422
5491
  $("#dialog-fields").querySelector("[data-task-identity-repair]")?.addEventListener("click", async (event) => {
5423
5492
  const button = event.currentTarget;
5424
5493
  button.disabled = true;
@@ -6962,6 +7031,107 @@ function toggleVditorLineNumbers(editor) {
6962
7031
  const enabled = host.classList.toggle("show-line-numbers");
6963
7032
  const button = host.querySelector(".vditor-line-number-button");
6964
7033
  if (button) button.setAttribute("aria-pressed", String(enabled));
7034
+ updateVditorLineNumbers(editor, getVditorMarkdown(editor));
7035
+ }
7036
+
7037
+ function getVditorMarkdown(editor) {
7038
+ if (typeof editor?.getValue === "function") {
7039
+ try {
7040
+ return String(editor.getValue() ?? "");
7041
+ } catch {
7042
+ return "";
7043
+ }
7044
+ }
7045
+ const host = editor?.vditor?.element;
7046
+ const valueField = host?.closest("[data-vditor-editor-field], .setting-markdown-field, .character-markdown-editor")?.querySelector("[data-vditor-value]")
7047
+ ?? host?.parentElement?.querySelector("[data-vditor-value]");
7048
+ return String(valueField?.value ?? "");
7049
+ }
7050
+
7051
+ function getVditorVisibleSurface(content) {
7052
+ const candidates = [
7053
+ content.querySelector(".vditor-ir .vditor-reset"),
7054
+ content.querySelector(".vditor-wysiwyg .vditor-reset"),
7055
+ content.querySelector(".vditor-sv textarea"),
7056
+ content.querySelector(".vditor-sv")
7057
+ ];
7058
+ return candidates.find((surface) => {
7059
+ if (!surface) return false;
7060
+ const rect = surface.getBoundingClientRect();
7061
+ const style = getComputedStyle(surface);
7062
+ return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
7063
+ }) ?? null;
7064
+ }
7065
+
7066
+ function getVditorLineHeight(surface) {
7067
+ const style = getComputedStyle(surface);
7068
+ return parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5 || 24;
7069
+ }
7070
+
7071
+ function collectVditorVisualLineRects(surface) {
7072
+ const rects = [];
7073
+ const scrollTop = Number(surface.scrollTop) || 0;
7074
+ const visit = (node) => {
7075
+ if (node.nodeType === Node.TEXT_NODE) {
7076
+ if (!node.textContent) return;
7077
+ const range = document.createRange();
7078
+ range.selectNodeContents(node);
7079
+ [...range.getClientRects()].forEach((rect) => {
7080
+ if (rect.height > 0) rects.push({ top: rect.top + scrollTop, height: rect.height });
7081
+ });
7082
+ return;
7083
+ }
7084
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
7085
+ if (node.tagName === "BR") {
7086
+ const rect = node.getBoundingClientRect();
7087
+ if (rect.height > 0) rects.push({ top: rect.top + scrollTop, height: rect.height });
7088
+ }
7089
+ [...node.childNodes].forEach(visit);
7090
+ };
7091
+ visit(surface);
7092
+ if (rects.length === 0) {
7093
+ const rect = surface.getBoundingClientRect();
7094
+ const style = getComputedStyle(surface);
7095
+ rects.push({ top: rect.top + scrollTop + (parseFloat(style.paddingTop) || 0), height: getVditorLineHeight(surface) });
7096
+ }
7097
+ return rects;
7098
+ }
7099
+
7100
+ function syncVditorLineNumberScroll(editor) {
7101
+ const surface = editor?.__vditorLineNumberSurface;
7102
+ const gutter = editor?.vditor?.element?.querySelector(".vditor-line-numbers");
7103
+ if (!surface || !gutter) return;
7104
+ gutter.style.transform = `translateY(${-surface.scrollTop}px)`;
7105
+ gutter.dataset.scrollTop = String(surface.scrollTop);
7106
+ }
7107
+
7108
+ function scheduleVditorLineNumbers(editor) {
7109
+ if (!editor || editor.__vditorLineNumberFrame) return;
7110
+ editor.__vditorLineNumberFrame = requestAnimationFrame(() => {
7111
+ editor.__vditorLineNumberFrame = null;
7112
+ updateVditorLineNumbers(editor, getVditorMarkdown(editor));
7113
+ });
7114
+ }
7115
+
7116
+ function setupVditorLineNumberSync(editor, content, surface) {
7117
+ if (editor.__vditorLineNumberSurface !== surface) {
7118
+ editor.__vditorLineNumberSurface?.removeEventListener("scroll", editor.__vditorLineNumberScrollHandler);
7119
+ editor.__vditorLineNumberSurface = surface;
7120
+ editor.__vditorLineNumberScrollHandler = () => syncVditorLineNumberScroll(editor);
7121
+ surface.addEventListener("scroll", editor.__vditorLineNumberScrollHandler, { passive: true });
7122
+ }
7123
+ if (!editor.__vditorLineNumberObserver) {
7124
+ editor.__vditorLineNumberObserver = new MutationObserver((mutations) => {
7125
+ if (mutations.some((mutation) => !(mutation.target instanceof Element) || !mutation.target.closest(".vditor-line-numbers"))) {
7126
+ scheduleVditorLineNumbers(editor);
7127
+ }
7128
+ });
7129
+ editor.__vditorLineNumberObserver.observe(content, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "style"] });
7130
+ }
7131
+ if (!editor.__vditorLineNumberResizeObserver && typeof ResizeObserver !== "undefined") {
7132
+ editor.__vditorLineNumberResizeObserver = new ResizeObserver(() => scheduleVditorLineNumbers(editor));
7133
+ editor.__vditorLineNumberResizeObserver.observe(content);
7134
+ }
6965
7135
  }
6966
7136
 
6967
7137
  function updateVditorLineNumbers(editor, markdown) {
@@ -6974,14 +7144,30 @@ function updateVditorLineNumbers(editor, markdown) {
6974
7144
  gutter.setAttribute("aria-hidden", "true");
6975
7145
  content.prepend(gutter);
6976
7146
  }
6977
- const lineCount = Math.max(1, String(markdown ?? "").split(/\r?\n/gu).length);
7147
+ const surface = getVditorVisibleSurface(content);
7148
+ if (!surface) return;
7149
+ setupVditorLineNumberSync(editor, content, surface);
7150
+ const contentRect = content.getBoundingClientRect();
7151
+ const lineHeight = getVditorLineHeight(surface);
7152
+ const normalizedMarkdown = String(markdown ?? "").replace(/\r\n?/gu, "\n");
7153
+ const blankLineCount = normalizedMarkdown.trim() === "" ? 0 : normalizedMarkdown.split("\n").filter((line) => line.trim() === "").length;
7154
+ const visualRows = buildVditorLineNumberRows(collectVditorVisualLineRects(surface), { lineHeight, blankLineCount });
7155
+ gutter.style.height = `${content.clientHeight}px`;
6978
7156
  const fragment = document.createDocumentFragment();
6979
- for (let line = 1; line <= lineCount; line += 1) {
7157
+ visualRows.forEach((row, index) => {
6980
7158
  const number = document.createElement("span");
6981
- number.textContent = String(line);
7159
+ number.className = "vditor-line-number";
7160
+ number.textContent = String(index + 1);
7161
+ number.dataset.lineNumber = String(index + 1);
7162
+ number.dataset.blank = String(row.blank);
7163
+ number.style.top = `${row.top - contentRect.top}px`;
7164
+ number.style.height = `${row.height}px`;
7165
+ number.style.lineHeight = `${row.height}px`;
6982
7166
  fragment.append(number);
6983
- }
7167
+ });
6984
7168
  gutter.replaceChildren(fragment);
7169
+ gutter.dataset.lineCount = String(visualRows.length);
7170
+ syncVditorLineNumberScroll(editor);
6985
7171
  }
6986
7172
 
6987
7173
  function updateVditorWordCount(editor, markdown) {
@@ -7026,6 +7212,12 @@ function ensureVditorIconScript() {
7026
7212
  function destroyVditorEditor(editor) {
7027
7213
  if (!editor) return;
7028
7214
  editor.__attachmentObserver?.disconnect();
7215
+ editor.__vditorLineNumberObserver?.disconnect();
7216
+ editor.__vditorLineNumberResizeObserver?.disconnect();
7217
+ if (editor.__vditorLineNumberSurface && editor.__vditorLineNumberScrollHandler) {
7218
+ editor.__vditorLineNumberSurface.removeEventListener("scroll", editor.__vditorLineNumberScrollHandler);
7219
+ }
7220
+ if (editor.__vditorLineNumberFrame) cancelAnimationFrame(editor.__vditorLineNumberFrame);
7029
7221
  const host = editor.vditor?.element;
7030
7222
  editor.destroy();
7031
7223
  if (host) delete host.__vditor;
@@ -8715,18 +8907,22 @@ async function showChapterInsight() {
8715
8907
  if (requestId !== chapterInsightRequestId || state.chapter?.id !== chapterId || $("#editor-view").classList.contains("hidden")) return;
8716
8908
  const insight = insights.find((item) => item.chapterVersion === state.chapter.versionNo) ?? insights[0];
8717
8909
  const region = $("#toast-region");
8910
+ const chapterTitle = state.chapter.title;
8718
8911
  const element = document.createElement("section");
8719
8912
  element.id = "chapter-insight-toast";
8720
8913
  element.className = "toast chapter-insight-toast";
8721
8914
  element.setAttribute("role", "region");
8722
- element.setAttribute("aria-label", "章节概览");
8915
+ element.setAttribute("aria-label", `章节概览:${chapterTitle}`);
8723
8916
  const header = document.createElement("header");
8724
8917
  header.className = "chapter-insight-toast-header";
8725
8918
  const heading = document.createElement("div");
8726
8919
  heading.className = "chapter-insight-toast-heading";
8727
8920
  const title = document.createElement("strong");
8728
8921
  title.textContent = "章节概览";
8729
- heading.append(title);
8922
+ const chapterName = document.createElement("span");
8923
+ chapterName.className = "chapter-insight-toast-chapter-title";
8924
+ chapterName.textContent = chapterTitle;
8925
+ heading.append(title, chapterName);
8730
8926
  if (insight && insight.chapterVersion !== state.chapter.versionNo) {
8731
8927
  const stale = document.createElement("span");
8732
8928
  stale.textContent = `基于旧版本 v${insight.chapterVersion}`;
@@ -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=20260729-continuous-analysis-queue-merge-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260729-analysis-rerun-vditor-ui-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -876,6 +876,6 @@
876
876
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
877
877
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
878
878
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
879
- <script type="module" src="/app.js?v=20260729-continuous-analysis-queue-merge-v1"></script>
879
+ <script type="module" src="/app.js?v=20260729-analysis-rerun-vditor-ui-v1"></script>
880
880
  </body>
881
881
  </html>
@@ -509,7 +509,7 @@ input[type="checkbox"][hidden] { display: none; }
509
509
 
510
510
  .left-panel, .ai-panel { position: relative; min-height: 0; background: var(--panel); }
511
511
  .left-panel { display: flex; flex-direction: column; border-right: 1px solid var(--line); padding: 0; overflow: hidden; }
512
- .left-panel-body { flex: 1 1 auto; min-height: 0; padding: 18px 14px 16px; overflow-y: auto; }
512
+ .left-panel-body { flex: 1 1 auto; min-height: 0; padding: 18px 28px 16px 14px; overflow-y: auto; scrollbar-gutter: stable; }
513
513
  .ai-panel { border-left: 1px solid var(--line); display: flex; flex-direction: column; padding: 14px 16px 18px; min-width: 0; }
514
514
  .panel-collapse-button { z-index: 8; flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--panel); color: var(--muted); font-size: 18px; line-height: 1; }
515
515
  .ai-heading #ai-panel-toggle { flex-basis: 30px; width: 30px; height: 30px; }
@@ -1346,6 +1346,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1346
1346
  .task-auto-run-copy { display: grid; grid-column: 1; gap: 4px; }
1347
1347
  .task-auto-run-copy strong { font-size: 13px; }
1348
1348
  .task-auto-run-copy small { color: var(--muted); font-size: 10px; line-height: 1.5; }
1349
+ .task-auto-run-actions { display: flex; grid-column: 2; grid-row: 1; align-items: start; justify-content: flex-end; min-height: 32px; }
1350
+ .task-auto-run-actions:empty { display: none; }
1349
1351
  .task-auto-run-controls {
1350
1352
  display: flex;
1351
1353
  grid-column: 1;
@@ -1558,6 +1560,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1558
1560
  .task-result-json-content textarea { width: 100%; min-height: 360px; max-height: 52vh; resize: vertical; overflow: auto; margin: 0; padding: 10px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface-soft); color: var(--ink); font-family: var(--font-latin), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 10px; line-height: 1.55; tab-size: 2; white-space: pre; }
1559
1561
  .task-detail-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 12px; }
1560
1562
  .task-detail-actions small { color: var(--muted); }
1563
+ .task-rerun-model-intro { grid-column: 1 / -1; color: var(--muted); }
1561
1564
  .relationship-identity-repair-entry { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-top: 10px; padding: 12px; border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--line)); border-radius: 6px; background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
1562
1565
  .relationship-identity-repair-entry > div { display: grid; gap: 4px; }
1563
1566
  .relationship-identity-repair-entry strong { color: var(--ink); }
@@ -2184,6 +2187,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2184
2187
  .chapter-insight-toast-heading { display: flex; min-width: 0; align-items: center; gap: 8px; }
2185
2188
  .chapter-insight-toast-heading strong { font-size: 12px; }
2186
2189
  .chapter-insight-toast-heading span { overflow: hidden; color: var(--accent-dark); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
2190
+ .chapter-insight-toast-heading .chapter-insight-toast-chapter-title { min-width: 0; color: var(--ink); font-size: 11px; }
2187
2191
  .chapter-insight-toast-close { flex: 0 0 auto; min-height: 28px; padding: 5px 9px; font-size: 10px; }
2188
2192
  .chapter-insight-toast-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; }
2189
2193
  .chapter-insight-toast-body p { margin: 0; white-space: pre-wrap; }
@@ -2498,9 +2502,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2498
2502
  .vditor-editor-host .vditor-toolbar { display: flex; align-items: center; flex: 0 0 auto; }
2499
2503
  .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; }
2500
2504
  .vditor-editor-host .vditor-content { position: relative; }
2501
- .vditor-editor-host .vditor-line-numbers { display: none; position: absolute; z-index: 1; inset: 0 auto 0 0; width: 42px; padding: 16px 8px 16px 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: right; user-select: none; pointer-events: none; }
2502
- .vditor-editor-host.vditor.show-line-numbers .vditor-line-numbers { display: grid; align-content: start; }
2503
- .vditor-editor-host.vditor.show-line-numbers .vditor-ir, .vditor-editor-host.vditor.show-line-numbers .vditor-wysiwyg, .vditor-editor-host.vditor.show-line-numbers .vditor-sv textarea { padding-left: 42px; }
2505
+ .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: right; user-select: none; pointer-events: none; will-change: transform; }
2506
+ .vditor-editor-host.vditor.show-line-numbers .vditor-line-numbers { display: block; }
2507
+ .vditor-editor-host .vditor-line-number { position: absolute; top: 0; right: 8px; left: 0; display: block; box-sizing: border-box; overflow: hidden; padding: 0; text-align: right; white-space: nowrap; }
2508
+ .vditor-editor-host.vditor.show-line-numbers .vditor-ir, .vditor-editor-host.vditor.show-line-numbers .vditor-wysiwyg, .vditor-editor-host.vditor.show-line-numbers .vditor-sv, .vditor-editor-host.vditor.show-line-numbers .vditor-sv textarea { padding-left: 42px; }
2504
2509
  .vditor-editor-host .vditor-line-number-button[aria-pressed="true"] { color: var(--accent-dark); background: color-mix(in srgb, var(--accent) 16%, transparent); }
2505
2510
  .vditor-editor-host .vditor-content { min-height: 0; }
2506
2511
  .vditor-editor-host .vditor-reset { color: var(--ink); font-family: var(--font-cjk), var(--font-latin), sans-serif; }
@@ -2711,7 +2716,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2711
2716
  }
2712
2717
 
2713
2718
  @media (max-width: 1100px) {
2714
- .left-panel-body { padding-inline: 10px; }
2719
+ .left-panel-body { padding-inline: 10px 24px; }
2715
2720
  .ai-panel { padding-inline: 14px; }
2716
2721
  .editor-toolbar { grid-template-areas: "path" "title" "actions"; grid-template-columns: minmax(0, 1fr); gap: 12px; padding-inline: 6%; }
2717
2722
  .editor-actions { justify-content: flex-end; flex-wrap: wrap; }
@@ -2725,7 +2730,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2725
2730
  .top-actions .ghost-button:not(.settings-button) { display: none; }
2726
2731
  .book-shelf { grid-template-columns: repeat(3, minmax(180px, 1fr)); }
2727
2732
  .task-auto-run-panel { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
2728
- .task-auto-run-copy, .task-auto-run-controls, .task-auto-run-help, .task-auto-run-meta, .task-auto-run-alert { grid-column: 1; }
2733
+ .task-auto-run-copy, .task-auto-run-actions, .task-auto-run-controls, .task-auto-run-meta { grid-column: 1; }
2734
+ .task-auto-run-actions { grid-row: auto; }
2735
+ .task-auto-run-help, .task-auto-run-alert { grid-column: 1; }
2729
2736
  .task-auto-run-progress { grid-column: 1; grid-row: auto; justify-self: stretch; width: 100%; }
2730
2737
  .task-auto-run-progress-ring { display: none; }
2731
2738
  .task-auto-run-progress-bar-layout { display: grid; gap: 6px; width: 100%; }
@@ -0,0 +1,14 @@
1
+ export type VditorLineRect = {
2
+ top: number;
3
+ height: number;
4
+ };
5
+
6
+ export type VditorLineNumberRow = VditorLineRect & {
7
+ blank: boolean;
8
+ };
9
+
10
+ export function normalizeVditorLineRects(rects: VditorLineRect[], tolerance?: number): VditorLineRect[];
11
+ export function buildVditorLineNumberRows(
12
+ rects: VditorLineRect[],
13
+ options?: { lineHeight?: number; blankLineCount?: number }
14
+ ): VditorLineNumberRow[];
@@ -0,0 +1,51 @@
1
+ export function normalizeVditorLineRects(rects, tolerance = 1.5) {
2
+ const rows = [];
3
+ const sorted = rects
4
+ .filter((rect) => Number.isFinite(Number(rect.top)) && Number.isFinite(Number(rect.height)) && Number(rect.height) > 0)
5
+ .map((rect) => ({ top: Number(rect.top), height: Number(rect.height) }))
6
+ .sort((left, right) => left.top - right.top);
7
+ for (const rect of sorted) {
8
+ const current = rows[rows.length - 1];
9
+ if (current && Math.abs(current.top - rect.top) <= tolerance) {
10
+ current.height = Math.max(current.height, rect.height);
11
+ continue;
12
+ }
13
+ rows.push(rect);
14
+ }
15
+ return rows;
16
+ }
17
+
18
+ export function buildVditorLineNumberRows(rects, { lineHeight = 0, blankLineCount = 0 } = {}) {
19
+ const normalizedLineHeight = Number(lineHeight) > 0 ? Number(lineHeight) : 0;
20
+ const normalizedBlankLineCount = Math.max(0, Math.floor(Number(blankLineCount) || 0));
21
+ const visualRows = normalizeVditorLineRects(rects);
22
+ if (visualRows.length === 0) {
23
+ return normalizedBlankLineCount > 0
24
+ ? Array.from({ length: normalizedBlankLineCount }, (_, index) => ({ top: index * normalizedLineHeight, height: normalizedLineHeight, blank: true }))
25
+ : normalizedLineHeight > 0 ? [{ top: 0, height: normalizedLineHeight, blank: true }] : [];
26
+ }
27
+ const rows = [];
28
+ let remainingBlankLines = normalizedBlankLineCount;
29
+ for (const visualRow of visualRows) {
30
+ const height = normalizedLineHeight || visualRow.height;
31
+ const top = visualRow.top - (height - visualRow.height) / 2;
32
+ const previous = rows[rows.length - 1];
33
+ if (previous && remainingBlankLines > 0 && normalizedLineHeight > 0) {
34
+ const availableSlots = Math.max(0, Math.round((top - previous.top) / normalizedLineHeight) - 1);
35
+ const blankLines = Math.min(remainingBlankLines, availableSlots);
36
+ for (let index = 1; index <= blankLines; index += 1) {
37
+ rows.push({ top: previous.top + normalizedLineHeight * index, height: normalizedLineHeight, blank: true });
38
+ }
39
+ remainingBlankLines -= blankLines;
40
+ }
41
+ rows.push({ top, height, blank: false });
42
+ }
43
+ const last = rows[rows.length - 1];
44
+ let nextTop = last.top;
45
+ while (remainingBlankLines > 0 && normalizedLineHeight > 0) {
46
+ nextTop += normalizedLineHeight;
47
+ rows.push({ top: nextTop, height: normalizedLineHeight, blank: true });
48
+ remainingBlankLines -= 1;
49
+ }
50
+ return rows;
51
+ }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const APP_VERSION = "0.5.9";
1
+ export const APP_VERSION = "0.5.10";
2
2
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC"}
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,QAAQ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@musnows/scriverse",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
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",