@musnows/scriverse 0.7.6 → 0.7.8

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.
@@ -8,6 +8,7 @@ import { DEFAULT_ATTACHMENT_IMAGE_MAX_BYTES, formatUploadLimit } from "./upload-
8
8
  const maximumPixels = 25_000_000;
9
9
  const maximumAnimationFrames = 100;
10
10
  const maximumAnimationPixels = 50_000_000;
11
+ const maximumGifFrames = 10_000;
11
12
  const allowedFormats = new Set(["png", "jpeg", "webp", "gif"]);
12
13
  function mimeType(format) {
13
14
  if (format === "png")
@@ -120,6 +121,9 @@ export class AttachmentStorage {
120
121
  if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(pageHeight) || pageHeight <= 0) {
121
122
  throw new AppError(415, "INVALID_ATTACHMENT_IMAGE", "无法读取附件图片尺寸");
122
123
  }
124
+ if (isGif && pageCount > maximumGifFrames) {
125
+ throw new AppError(413, "ATTACHMENT_GIF_TOO_MANY_FRAMES", "GIF 动画不能超过 10,000 帧");
126
+ }
123
127
  if (!isGif && width * pageHeight > maximumPixels)
124
128
  throw new AppError(413, "ATTACHMENT_IMAGE_TOO_LARGE", "附件图片像素尺寸过大");
125
129
  if (!isGif && (!Number.isInteger(pageCount) || pageCount > maximumAnimationFrames)) {
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-storage.js","sourceRoot":"","sources":["../src/attachment-storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"attachment-storage.js","sourceRoot":"","sources":["../src/attachment-storage.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,21 @@
1
- export function formatAiMessageMeta(modelDisplayName, outputTokens, cacheHitPercent, suffix = "") {
1
+ export function resolveAiTokensPerSecond(outputTokens, durationMs) {
2
+ const tokenCount = Number(outputTokens);
3
+ const duration = Number(durationMs);
4
+ if (!Number.isFinite(tokenCount) || tokenCount < 0 || !Number.isFinite(duration) || duration <= 0) return null;
5
+ return Math.max(0, Math.round(tokenCount / (duration / 1000)));
6
+ }
7
+
8
+ export function formatAiMessageMeta(modelDisplayName, outputTokens, cacheHitPercent, suffix = "", durationMs) {
2
9
  const modelName = String(modelDisplayName || "模型").trim();
3
10
  const tokenCount = Math.max(0, Math.round(Number(outputTokens) || 0)).toLocaleString("zh-CN");
11
+ const tokensPerSecond = resolveAiTokensPerSecond(outputTokens, durationMs);
4
12
  const cachePercent = Number(cacheHitPercent);
5
13
  const roundedCachePercent = Math.round((cachePercent + Number.EPSILON) * 10) / 10;
6
14
  const cacheLabel = Number.isFinite(cachePercent)
7
15
  ? `缓存命中 ${Math.max(0, Math.min(100, roundedCachePercent)).toLocaleString("zh-CN")}%`
8
16
  : "";
9
- return [modelName, `${tokenCount} tok`, cacheLabel, String(suffix || "").trim()].filter(Boolean).join(" · ");
17
+ const speedLabel = tokensPerSecond === null ? "" : `${tokensPerSecond.toLocaleString("zh-CN")}\u00a0tok/s`;
18
+ return [modelName, `${tokenCount} tok`, speedLabel, cacheLabel, String(suffix || "").trim()].filter(Boolean).join(" · ");
10
19
  }
11
20
 
12
21
  export function estimateAiMessageTokens(value) {
@@ -18,7 +18,7 @@ import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260
18
18
  import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, supportsMultimodalModelProtocol } from "/model-config.js?v=20260803-multimodal-model-config-v2";
19
19
  import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260812-connectivity-cooldown-v1";
20
20
  import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
21
- import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
21
+ import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
22
22
  import { createStreamTypewriter } from "/stream-typewriter.js?v=20260730-ai-stream-typewriter-v3";
23
23
  import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
24
24
  import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
@@ -156,6 +156,7 @@ const state = {
156
156
  aiTaskType: "chat",
157
157
  aiContextScope: { type: "none" },
158
158
  aiConversationId: null,
159
+ aiConversationModelId: null,
159
160
  aiConversations: [],
160
161
  aiRoleplayCharacter: null,
161
162
  aiLastMessageAt: null,
@@ -2324,6 +2325,7 @@ async function openAiConversation(conversationId, hideHistory = true, focusMessa
2324
2325
  if (String(conversation.workId ?? "") !== navigation.workId) throw new Error("AI 对话不属于当前作品");
2325
2326
  upsertAiConversationSummary(conversation);
2326
2327
  state.aiConversationId = conversation.id;
2328
+ state.aiConversationModelId = typeof conversation.modelId === "string" ? conversation.modelId : null;
2327
2329
  state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
2328
2330
  applyAiConversationTaskType(conversation.taskType);
2329
2331
  applyAiConversationContextScope(conversation.contextScope);
@@ -2366,6 +2368,7 @@ function focusAiConversationMessage(messageId) {
2366
2368
  function applyNewAiConversation(conversation) {
2367
2369
  upsertAiConversationSummary(conversation);
2368
2370
  state.aiConversationId = conversation.id;
2371
+ state.aiConversationModelId = null;
2369
2372
  state.aiPromptSent = false;
2370
2373
  applyAiConversationTaskType(conversation.taskType);
2371
2374
  applyAiConversationContextScope(conversation.contextScope);
@@ -2401,7 +2404,7 @@ function renderAiRoleplayCharacterSelect() {
2401
2404
  const availableCharacters = state.characters.filter((character) => !character.mergedIntoCharacterId);
2402
2405
  const options = [{ id: "", name: "选择角色卡" }, ...availableCharacters.map((character) => ({
2403
2406
  id: String(character.id),
2404
- name: String(character.name)
2407
+ name: `${String(character.name)}${character.isDead ? "(已死亡)" : ""}`
2405
2408
  }))];
2406
2409
  if (selectedId && !options.some((option) => option.id === selectedId)) {
2407
2410
  options.push({ id: selectedId, name: String(state.aiRoleplayCharacter.name) });
@@ -2435,6 +2438,8 @@ function syncAiTaskOptions() {
2435
2438
  $("#ai-scope").title = state.aiPromptSent
2436
2439
  ? "对话开始后不能切换上下文引用"
2437
2440
  : roleplaySelected ? "角色扮演模式只使用角色自身的记忆" : "";
2441
+ $("#ai-model").disabled = interactionBusy || state.aiPromptSent;
2442
+ $("#ai-model").title = state.aiPromptSent ? "对话开始后不能切换模型" : "";
2438
2443
  }
2439
2444
 
2440
2445
  function applyAiConversationTaskType(taskType) {
@@ -3740,6 +3745,19 @@ function raiseToastRegion() {
3740
3745
  region.showPopover();
3741
3746
  }
3742
3747
 
3748
+ function dismissDeleteToasts() {
3749
+ const region = $("#toast-region");
3750
+ region.querySelectorAll(".delete-toast").forEach((element) => element.remove());
3751
+ if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
3752
+ region.hidePopover();
3753
+ }
3754
+ }
3755
+
3756
+ function deleteToast(message) {
3757
+ toast(message);
3758
+ $("#toast-region").lastElementChild?.classList.add("delete-toast");
3759
+ }
3760
+
3743
3761
  function dismissChapterInsightToast() {
3744
3762
  chapterInsightRequestId += 1;
3745
3763
  const region = $("#toast-region");
@@ -4263,6 +4281,7 @@ async function initializePage() {
4263
4281
  function showShelf() {
4264
4282
  stopBackgroundTaskCenter();
4265
4283
  dismissChapterInsightToast();
4284
+ dismissDeleteToasts();
4266
4285
  state.dirty = false;
4267
4286
  settingsReturnContext = null;
4268
4287
  updateDocumentTitle();
@@ -4563,6 +4582,7 @@ async function showWorkAudit() {
4563
4582
  workAuditRecords = [];
4564
4583
  workAuditNextPage = null;
4565
4584
  dismissChapterInsightToast();
4585
+ dismissDeleteToasts();
4566
4586
  updateDocumentTitle(state.work);
4567
4587
  $("#app").classList.add("shelf-mode");
4568
4588
  $("#shelf-view").classList.add("hidden");
@@ -4740,7 +4760,7 @@ function renderS3BackupTargets() {
4740
4760
  try {
4741
4761
  await api(`/api/platform/backups/targets/${encodeURIComponent(target.id)}`, { method: "DELETE" });
4742
4762
  await loadS3BackupData();
4743
- toast(`备份目标“${target.name}”已删除`);
4763
+ deleteToast(`备份目标“${target.name}”已删除`);
4744
4764
  } catch (error) {
4745
4765
  button.disabled = false;
4746
4766
  toast(error.message, "error");
@@ -5108,7 +5128,7 @@ function renderMembers(members) {
5108
5128
  const updated = await api(`/api/works/${encodeURIComponent(work.id)}/members/${encodeURIComponent(button.dataset.removeMember)}`, { method: "DELETE" });
5109
5129
  renderMembers(updated);
5110
5130
  renderMemberSelector();
5111
- toast("协作者已移除");
5131
+ deleteToast("协作者已移除");
5112
5132
  } catch (error) { toast(error.message, "error"); }
5113
5133
  }));
5114
5134
  }
@@ -5485,6 +5505,7 @@ async function showSettingsHub() {
5485
5505
  state.dirty = false;
5486
5506
  }
5487
5507
  dismissChapterInsightToast();
5508
+ dismissDeleteToasts();
5488
5509
  updateDocumentTitle(state.work);
5489
5510
  $("#app").classList.add("shelf-mode");
5490
5511
  $("#shelf-view").classList.add("hidden");
@@ -5532,6 +5553,7 @@ async function showPlatformAi() {
5532
5553
  if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
5533
5554
  state.dirty = false;
5534
5555
  dismissChapterInsightToast();
5556
+ dismissDeleteToasts();
5535
5557
  updateDocumentTitle();
5536
5558
  $("#app").classList.add("shelf-mode");
5537
5559
  $("#shelf-view").classList.add("hidden");
@@ -5553,6 +5575,8 @@ async function showPlatformAi() {
5553
5575
  async function showPlatformUsage() {
5554
5576
  if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入 Token 用量面板将放弃本地修改。是否继续?"))) return false;
5555
5577
  state.dirty = false;
5578
+ dismissChapterInsightToast();
5579
+ dismissDeleteToasts();
5556
5580
  updateDocumentTitle();
5557
5581
  $("#app").classList.add("shelf-mode");
5558
5582
  $("#shelf-view").classList.add("hidden");
@@ -5648,6 +5672,7 @@ function resetWorkScopedUiCaches() {
5648
5672
  state.aiReferences = [];
5649
5673
  state.aiPromptSent = false;
5650
5674
  state.aiConversationId = null;
5675
+ state.aiConversationModelId = null;
5651
5676
  state.aiConversations = [];
5652
5677
  state.aiRoleplayCharacter = null;
5653
5678
  renderAiCitations();
@@ -6107,7 +6132,7 @@ async function deleteChapter(chapterId) {
6107
6132
  } else {
6108
6133
  renderTree();
6109
6134
  }
6110
- toast(`已删除章节“${chapter.title}”,正文和版本记录已保留`);
6135
+ deleteToast(`已删除章节“${chapter.title}”,正文和版本记录已保留`);
6111
6136
  } catch (error) {
6112
6137
  resumeAutoSave();
6113
6138
  toast(error.message, "error");
@@ -6316,6 +6341,7 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
6316
6341
  if (selectionRequestId === chapterSelectionRequestId) void loadChapterForeshadowReminders();
6317
6342
  return false;
6318
6343
  }
6344
+ dismissDeleteToasts();
6319
6345
  if (selectionGeneration !== chapterSelectionRequestGeneration || selectionRequestId !== chapterSelectionRequestId || state.work?.id !== workId) return false;
6320
6346
  cancelChapterAutoSave();
6321
6347
  let selectedChapter = state.chapter;
@@ -6865,6 +6891,7 @@ function showWelcome(hasWork = false) {
6865
6891
  chapterSelectionRequestId += 1;
6866
6892
  clearChapterForeshadowReminders({ invalidateRequest: true });
6867
6893
  dismissChapterInsightToast();
6894
+ dismissDeleteToasts();
6868
6895
  $("#editor-view").classList.add("hidden");
6869
6896
  $("#module-view").classList.add("hidden");
6870
6897
  $("#welcome-view").classList.remove("hidden");
@@ -6909,6 +6936,7 @@ async function showModule(module) {
6909
6936
  }
6910
6937
  if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
6911
6938
  if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
6939
+ dismissDeleteToasts();
6912
6940
  state.module = module;
6913
6941
  if (module !== "editor") {
6914
6942
  chapterSelectionRequestId += 1;
@@ -7107,7 +7135,7 @@ async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning
7107
7135
  await onDeleted?.();
7108
7136
  await refresh();
7109
7137
  await loadAiReferences();
7110
- toast(`已删除${typeLabel}“${item.name}”`);
7138
+ deleteToast(`已删除${typeLabel}“${item.name}”`);
7111
7139
  } catch (error) {
7112
7140
  toast(error.message, "error");
7113
7141
  }
@@ -7409,28 +7437,17 @@ async function deleteDraft(item) {
7409
7437
  if (!await confirmToast(`确认删除想法“${item.title}”吗?想法将从当前列表移除。`, {
7410
7438
  title: "删除想法",
7411
7439
  confirmLabel: "继续删除"
7412
- })) {
7413
- openDraftDialog(item);
7414
- return;
7415
- }
7440
+ })) return;
7416
7441
  if (!await confirmToast(`删除想法“${item.title}”后将从想法列表移除,版本历史仍会保留。仍要删除吗?`, {
7417
7442
  title: "删除操作需要再次确认",
7418
7443
  confirmLabel: "确认删除"
7419
- })) {
7420
- openDraftDialog(item);
7421
- return;
7422
- }
7444
+ })) return;
7423
7445
  try {
7424
7446
  await api(`/api/drafts/${encodeURIComponent(item.id)}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
7425
7447
  await renderDrafts(moduleListPages.drafts);
7426
- toast("想法已删除");
7448
+ deleteToast("想法已删除");
7427
7449
  } catch (error) {
7428
7450
  toast(error.message, "error");
7429
- try {
7430
- openDraftDialog(await api(`/api/drafts/${encodeURIComponent(item.id)}`));
7431
- } catch (reloadError) {
7432
- toast(reloadError.message, "error");
7433
- }
7434
7451
  }
7435
7452
  }
7436
7453
 
@@ -7438,19 +7455,14 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
7438
7455
  const viewOnly = readOnly || !canEditModule("drafts");
7439
7456
  const volumeOptions = [["", "全局(不绑定分卷)"], ...(state.work?.volumes ?? []).map((volume) => [volume.id, volume.title])];
7440
7457
  const settingModuleOptions = [["", "全局(不绑定设定模块)"], ...draftSettingModules];
7441
- const management = item && !viewOnly ? `<section class="entity-dialog-management" aria-label="想法操作">
7442
- <div><strong>想法操作</strong><small>删除后将从想法列表移除,版本历史仍会保留。</small></div>
7443
- <div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-draft-delete>删除想法</button></div>
7444
- </section>` : "";
7445
7458
  const fields = field("draftType", "想法类型", "select", item?.draftType ?? "prose", [["prose", "正文想法"], ["setting", "设定想法"]])
7446
7459
  + `<div class="draft-binding-field" data-draft-binding-field="prose">${field("volumeId", "绑定分卷", "select", item?.volumeId ?? "", volumeOptions)}</div>`
7447
7460
  + `<div class="draft-binding-field" data-draft-binding-field="setting">${field("settingModule", "绑定设定模块", "select", item?.settingModule ?? "", settingModuleOptions)}</div>`
7448
- + field("title", "标题", "text", item?.title ?? "")
7449
7461
  + field("content", "内容", "markdown", item?.content ?? "", {
7450
7462
  placeholder: "记录尚未定稿的片段、方向或设定想法……",
7451
7463
  attachmentModule: "drafts",
7452
7464
  readOnly: viewOnly
7453
- }) + management;
7465
+ });
7454
7466
  openDialog(item ? viewOnly ? "查看想法" : "编辑想法" : "新建想法", fields, async (form) => {
7455
7467
  if (viewOnly) return;
7456
7468
  const title = String(form.get("title") ?? "").trim();
@@ -7475,7 +7487,9 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
7475
7487
  hideCancel: viewOnly,
7476
7488
  editor: true,
7477
7489
  errorPrefix: "想法保存失败:",
7478
- meta: "这里记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。"
7490
+ meta: "这里记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。",
7491
+ titleInput: { value: item?.title ?? "", readOnly: viewOnly, placeholder: "输入想法标题", label: "想法标题" },
7492
+ dangerAction: item && !viewOnly ? { label: "删除想法", onClick: () => deleteDraft(item) } : null
7479
7493
  });
7480
7494
  const draftTypeSelect = $("#dialog-fields").querySelector('select[name="draftType"]');
7481
7495
  const syncDraftBindingFields = () => {
@@ -7495,9 +7509,6 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
7495
7509
  else control.readOnly = true;
7496
7510
  });
7497
7511
  }
7498
- $("#dialog-fields").querySelector("[data-dialog-draft-delete]")?.addEventListener("click", () => {
7499
- void deleteDraft(item);
7500
- });
7501
7512
  }
7502
7513
 
7503
7514
  async function renderDrafts(page = moduleListPages.drafts) {
@@ -7979,7 +7990,7 @@ async function deleteTimelineEvent(item, page = moduleListPages.timeline) {
7979
7990
  try {
7980
7991
  await api(`/api/timeline/${encodeURIComponent(item.id)}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
7981
7992
  await renderTimeline(page);
7982
- toast(`已删除时间事件“${item.name}”`);
7993
+ deleteToast(`已删除时间事件“${item.name}”`);
7983
7994
  } catch (error) {
7984
7995
  toast(error.message, "error");
7985
7996
  }
@@ -8039,6 +8050,7 @@ async function renderTimeline(page = moduleListPages.timeline) {
8039
8050
  $("#module-content").querySelectorAll("[data-timeline-track-tab]").forEach((button) => button.addEventListener("click", async () => {
8040
8051
  const nextTrackId = button.dataset.timelineTrackTab ?? "";
8041
8052
  if (String(nextTrackId) === String(timelineActiveTrackId)) return;
8053
+ dismissDeleteToasts();
8042
8054
  timelineActiveTrackId = nextTrackId;
8043
8055
  await renderTimeline(1);
8044
8056
  }));
@@ -10462,7 +10474,11 @@ function applyAiModels(models) {
10462
10474
  select.innerHTML = state.models.length
10463
10475
  ? state.models.map((model) => `<option value="${esc(model.id)}">${esc(modelOptionLabel(model))}</option>`).join("")
10464
10476
  : '<option value="">请先配置模型</option>';
10477
+ if (state.aiConversationModelId && state.models.some((model) => model.id === state.aiConversationModelId)) {
10478
+ select.value = state.aiConversationModelId;
10479
+ }
10465
10480
  setAiContextMeter(null);
10481
+ syncAiTaskOptions();
10466
10482
  }
10467
10483
 
10468
10484
  async function ensureAiModelsLoaded() {
@@ -10830,10 +10846,18 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
10830
10846
  const submit = $("#dialog-submit");
10831
10847
  const submitStatus = $("#dialog-submit-status");
10832
10848
  const submitStatusMessage = $("#dialog-submit-status-message");
10849
+ const titleInput = $("#dialog-title-input");
10833
10850
  const submitLabel = options.submitLabel ?? "保存";
10834
10851
  let submitting = false;
10835
10852
  let disabledStates = [];
10836
10853
  $("#dialog-title").textContent = title;
10854
+ $("#dialog-title").classList.toggle("hidden", Boolean(options.titleInput));
10855
+ titleInput.classList.toggle("hidden", !options.titleInput);
10856
+ titleInput.name = options.titleInput ? "title" : "";
10857
+ titleInput.value = options.titleInput ? String(options.titleInput.value ?? "") : "";
10858
+ titleInput.placeholder = options.titleInput?.placeholder ?? "";
10859
+ titleInput.readOnly = Boolean(options.titleInput?.readOnly);
10860
+ titleInput.setAttribute("aria-label", options.titleInput?.label ?? "记录标题");
10837
10861
  $("#dialog-eyebrow").textContent = eyebrow;
10838
10862
  $("#dialog-meta").textContent = options.meta ?? "";
10839
10863
  $("#dialog-meta").classList.toggle("hidden", !options.meta);
@@ -10844,6 +10868,17 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
10844
10868
  form.classList.remove("is-submitting");
10845
10869
  form.removeAttribute("aria-busy");
10846
10870
  $("#dynamic-form .dialog-actions [value='cancel']").classList.toggle("hidden", Boolean(options.hideCancel));
10871
+ const dialogActions = form.querySelector(".dialog-actions");
10872
+ dialogActions?.querySelector("[data-dialog-danger-action]")?.remove();
10873
+ if (options.dangerAction) {
10874
+ const dangerButton = document.createElement("button");
10875
+ dangerButton.className = "danger-button dialog-danger-action";
10876
+ dangerButton.type = "button";
10877
+ dangerButton.dataset.dialogDangerAction = "";
10878
+ dangerButton.textContent = options.dangerAction.label;
10879
+ dangerButton.addEventListener("click", () => { void options.dangerAction.onClick(); });
10880
+ dialogActions?.insertBefore(dangerButton, dialogActions.querySelector("[value='cancel']"));
10881
+ }
10847
10882
  dialog.classList.toggle("wide-dialog", Boolean(options.wide));
10848
10883
  dialog.classList.toggle("trace-dialog", Boolean(options.trace));
10849
10884
  dialog.classList.toggle("large-dialog", Boolean(options.large));
@@ -10970,7 +11005,7 @@ function bindWorkCoverControls(work) {
10970
11005
  Object.assign(work, updated);
10971
11006
  refreshWorkCoverField(work);
10972
11007
  renderShelf();
10973
- toast("封面已移除");
11008
+ deleteToast("封面已移除");
10974
11009
  } catch (error) {
10975
11010
  toast(error.message, "error");
10976
11011
  }
@@ -11157,7 +11192,7 @@ async function deleteWork(work) {
11157
11192
  state.chapter = null;
11158
11193
  }
11159
11194
  await loadWorks();
11160
- toast(`作品“${work.title}”已移入回收站`);
11195
+ deleteToast(`作品“${work.title}”已移入回收站`);
11161
11196
  } catch (error) {
11162
11197
  if (deletingCurrentWork && state.dirty) scheduleChapterAutoSave();
11163
11198
  toast(error.message, "error");
@@ -11227,7 +11262,7 @@ async function deleteVolume(item) {
11227
11262
  state.collapsedVolumeIds.delete(item.id);
11228
11263
  state.work = await api(`/api/works/${state.work.id}`);
11229
11264
  renderTree();
11230
- toast(`分卷“${item.title}”已移入回收站`);
11265
+ deleteToast(`分卷“${item.title}”已移入回收站`);
11231
11266
  } catch (error) {
11232
11267
  toast(error.message, "error");
11233
11268
  openVolumeDialog(item);
@@ -11920,7 +11955,7 @@ function knowledgeSectionEditorHtml(section = null) {
11920
11955
  <div><span class="eyebrow">${label} Markdown 设定</span><input id="knowledge-section-title" class="character-section-editor-title-input" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="新建设定" aria-label="设定标题" required></div>
11921
11956
  <button class="entity-editor-back" type="button" data-knowledge-section-edit-close>返回设定列表</button>
11922
11957
  </header>
11923
- <section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}${label} Markdown 设定">
11958
+ <section class="character-markdown-editor knowledge-markdown-editor" aria-label="${section ? "编辑" : "新建"}${label} Markdown 设定">
11924
11959
  <div id="knowledge-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
11925
11960
  <div class="character-markdown-editor-footer">
11926
11961
  <div class="character-markdown-editor-actions"><button type="button" data-knowledge-section-edit-cancel>取消</button><button type="button" class="primary-button" data-knowledge-section-edit-save>${section ? "保存设定" : "添加设定"}</button></div>
@@ -12196,7 +12231,7 @@ function renderCharacterMarkdownSections() {
12196
12231
  characterEditorSections = await api(`/api/characters/${characterEditorItem.id}/sections`);
12197
12232
  renderCharacterMarkdownSections();
12198
12233
  await Promise.all([renderCharacters(), loadAiReferences()]);
12199
- toast("人物 Markdown 章节已删除");
12234
+ deleteToast("人物 Markdown 章节已删除");
12200
12235
  } catch (error) {
12201
12236
  button.disabled = false;
12202
12237
  toast(error.message, "error");
@@ -12904,7 +12939,7 @@ async function openRelationshipDialog(item, options = {}) {
12904
12939
  try {
12905
12940
  await api(`/api/relationships/${item.id}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
12906
12941
  await Promise.all([refreshRelationshipSurfaces(options.characterId ?? null), loadAiReferences()]);
12907
- toast(`已删除人物关系“${relationshipName}”`);
12942
+ deleteToast(`已删除人物关系“${relationshipName}”`);
12908
12943
  } catch (error) {
12909
12944
  reopenDialog();
12910
12945
  toast(error.message, "error");
@@ -13429,12 +13464,13 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
13429
13464
  "user",
13430
13465
  instruction,
13431
13466
  citations,
13432
- {},
13467
+ { modelId },
13433
13468
  { signal: request.signal }
13434
13469
  );
13435
13470
  assertAiRequestCurrent(request);
13436
13471
  updateAiConversationSummaryFromMessage(persistedUserMessage);
13437
13472
  requestHolder.snapshot = aiRequestManager.bind(request, { userMessageId: persistedUserMessage.id });
13473
+ state.aiConversationModelId = modelId;
13438
13474
  state.aiPromptSent = true;
13439
13475
  syncAiTaskOptions();
13440
13476
  renderAiRoleplayCharacterSelect();
@@ -13472,7 +13508,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
13472
13508
  const request = assertAiRequestCurrent(requestHolder.snapshot);
13473
13509
  suggestion = await api(`/api/works/${encodeURIComponent(request.workId)}/suggestions`, {
13474
13510
  method: "POST",
13475
- body: { taskType, instruction, scope, modelId, citations },
13511
+ body: { taskType, instruction, scope, modelId, citations, conversationId: requestHolder.snapshot.conversationId },
13476
13512
  signal: request.signal
13477
13513
  });
13478
13514
  assertAiRequestCurrent(request);
@@ -13482,7 +13518,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
13482
13518
  if (suggestionFailed) setAiAssistantStatus("error");
13483
13519
  setAiContextMeter(suggestion.contextUsage, false);
13484
13520
  assistantContent = suggestion.content;
13485
- assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
13521
+ assistantMetadata = { modelId, modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent, processDurationMs: suggestion.processDurationMs };
13486
13522
  }
13487
13523
  try {
13488
13524
  const request = assertAiRequestCurrent(requestHolder.snapshot);
@@ -13702,6 +13738,10 @@ async function streamChat(requestHolder, body, idempotencyKey) {
13702
13738
  }
13703
13739
  requestHolder.snapshot = aiRequestManager.bind(requestHolder.snapshot, { userMessageId: persistedUserMessage.id });
13704
13740
  updateAiConversationSummaryFromMessage(persistedUserMessage);
13741
+ const lockedModelId = typeof persistedUserMessage.metadata?.modelId === "string"
13742
+ ? persistedUserMessage.metadata.modelId
13743
+ : typeof body.modelId === "string" ? body.modelId : null;
13744
+ state.aiConversationModelId = lockedModelId;
13705
13745
  state.aiPromptSent = true;
13706
13746
  syncAiTaskOptions();
13707
13747
  renderAiRoleplayCharacterSelect();
@@ -13772,10 +13812,12 @@ async function streamChat(requestHolder, body, idempotencyKey) {
13772
13812
  message.querySelector(".message-heading > span").textContent = "助手";
13773
13813
  toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : toolCalls;
13774
13814
  processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : processSteps;
13775
- const processDurationMs = elapsedProcessTime();
13815
+ const processDurationMs = Number.isFinite(payload.processDurationMs) && payload.processDurationMs >= 0
13816
+ ? payload.processDurationMs
13817
+ : elapsedProcessTime();
13776
13818
  generatedMetadata = { modelDisplayName: payload.model?.displayName, outputTokens: payload.outputTokens, cacheHitPercent: payload.cacheHitPercent, toolCalls, processSteps, processDurationMs };
13777
13819
  renderAiProcessSteps(message, processSteps, true, processDurationMs);
13778
- meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent);
13820
+ meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent, "", processDurationMs);
13779
13821
  attachAssistantCopyAction(message, streamedText);
13780
13822
  scrollAiFeedToBottom();
13781
13823
  } else if (eventName === "request_status") {
@@ -13893,11 +13935,16 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
13893
13935
  }
13894
13936
  message.append(references);
13895
13937
  }
13896
- if (role === "assistant") {
13897
- const processSteps = Array.isArray(metadata?.processSteps) && metadata.processSteps.length
13938
+ const processSteps = role === "assistant"
13939
+ ? (Array.isArray(metadata?.processSteps) && metadata.processSteps.length
13898
13940
  ? metadata.processSteps
13899
- : (Array.isArray(metadata?.toolCalls) ? metadata.toolCalls : []).map((toolCall) => aiToolProcessStep(toolCall));
13900
- renderAiProcessSteps(message, processSteps, true, resolveAiProcessDuration(metadata, processSteps, createdAt));
13941
+ : (Array.isArray(metadata?.toolCalls) ? metadata.toolCalls : []).map((toolCall) => aiToolProcessStep(toolCall)))
13942
+ : [];
13943
+ const processDurationMs = role === "assistant"
13944
+ ? resolveAiProcessDuration(metadata, processSteps, createdAt)
13945
+ : null;
13946
+ if (role === "assistant") {
13947
+ renderAiProcessSteps(message, processSteps, true, processDurationMs);
13901
13948
  }
13902
13949
  if (role === "assistant" && !text.startsWith("调用失败:")) {
13903
13950
  const selectedModel = state.models.find((model) => model.id === $("#ai-model").value) ?? state.models[0];
@@ -13907,7 +13954,7 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
13907
13954
  meta.className = "message-meta";
13908
13955
  meta.textContent = isInterrupted
13909
13956
  ? formatAiStreamInterruptionMeta(interruptionCode, text.length)
13910
- : formatAiMessageMeta(modelDisplayName, outputTokens, metadata?.cacheHitPercent);
13957
+ : formatAiMessageMeta(modelDisplayName, outputTokens, metadata?.cacheHitPercent, "", processDurationMs);
13911
13958
  message.append(meta);
13912
13959
  attachAssistantCopyAction(message, text);
13913
13960
  }
@@ -13922,7 +13969,7 @@ function appendSuggestion(suggestion, createdAt = null, messageId = null) {
13922
13969
  const applicable = suggestion.action !== "note";
13923
13970
  const guard = suggestion.guard;
13924
13971
  const guardHtml = guard ? `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${guard.issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<p>${esc(guard.failure || "无法完成检查,请谨慎采纳")}</p>` : guard.issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>` : "";
13925
- message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, suggestion.cacheHitPercent, `基于 v${suggestion.chapterVersion ?? "-"}`))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
13972
+ message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, suggestion.cacheHitPercent, `基于 v${suggestion.chapterVersion ?? "-"}`, suggestion.processDurationMs))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
13926
13973
  attachMessageHeading(message, "助手建议", createdAt ?? undefined);
13927
13974
  attachAssistantCopyAction(message, suggestion.content);
13928
13975
  attachMessageIdentity(message, messageId);
@@ -14234,7 +14281,7 @@ function renderWorkRecycleBin(result) {
14234
14281
  });
14235
14282
  await loadWorkRecycleBin();
14236
14283
  dialog.showModal();
14237
- toast(`已彻底删除作品“${work.title}”`);
14284
+ deleteToast(`已彻底删除作品“${work.title}”`);
14238
14285
  } catch (error) {
14239
14286
  dialog.showModal();
14240
14287
  toast(error.message, "error");
@@ -14340,7 +14387,7 @@ function renderChapterRecycleBin(result) {
14340
14387
  renderTree();
14341
14388
  await loadChapterRecycleBin();
14342
14389
  dialog.showModal();
14343
- toast(`已彻底删除分卷“${volume.title}”`);
14390
+ deleteToast(`已彻底删除分卷“${volume.title}”`);
14344
14391
  } catch (error) {
14345
14392
  dialog.showModal();
14346
14393
  toast(error.message, "error");
@@ -14396,7 +14443,7 @@ function renderChapterRecycleBin(result) {
14396
14443
  renderTree();
14397
14444
  await loadChapterRecycleBin();
14398
14445
  dialog.showModal();
14399
- toast(`已彻底删除章节“${chapter.title}”`);
14446
+ deleteToast(`已彻底删除章节“${chapter.title}”`);
14400
14447
  } catch (error) {
14401
14448
  dialog.showModal();
14402
14449
  toast(error.message, "error");
@@ -14901,7 +14948,7 @@ $("#avatar-remove-button").addEventListener("click", async () => {
14901
14948
  const updated = await api("/api/auth/avatar", { method: "DELETE" });
14902
14949
  applyAuthenticatedUser({ user: updated, csrfToken: state.csrfToken });
14903
14950
  renderProfileAvatar();
14904
- toast("头像已移除");
14951
+ deleteToast("头像已移除");
14905
14952
  } catch (error) {
14906
14953
  toast(error.message, "error");
14907
14954
  } finally {
@@ -15446,7 +15493,13 @@ $("#ai-prompt").addEventListener("focus", () => {
15446
15493
  $("#ai-model").addEventListener("focus", () => {
15447
15494
  ensureAiModelsLoaded().catch((error) => toast(`模型加载失败:${error.message}`, "error"));
15448
15495
  });
15449
- $("#ai-model").addEventListener("change", () => setAiContextMeter(null));
15496
+ $("#ai-model").addEventListener("change", (event) => {
15497
+ if (state.aiPromptSent) {
15498
+ event.currentTarget.value = state.aiConversationModelId ?? event.currentTarget.value;
15499
+ return toast("当前对话已经开始,请新建对话后再切换模型", "error");
15500
+ }
15501
+ setAiContextMeter(null);
15502
+ });
15450
15503
  $("#ai-roleplay-character").addEventListener("focus", async () => {
15451
15504
  try {
15452
15505
  await ensureAiReferencesLoaded();
@@ -15954,6 +16007,10 @@ document.addEventListener("visibilitychange", () => {
15954
16007
  if (state.user?.role === "admin" && !systemRestartDetected) void refreshS3BackupEvents();
15955
16008
  void refreshSystemHealth();
15956
16009
  });
16010
+ document.addEventListener("click", (event) => {
16011
+ if (event.target instanceof Element && event.target.closest('[role="tab"]')) dismissDeleteToasts();
16012
+ }, true);
16013
+ window.addEventListener("pagehide", dismissDeleteToasts);
15957
16014
  window.addEventListener("beforeunload", (event) => {
15958
16015
  if (hasUnsavedEditorChanges()) event.preventDefault();
15959
16016
  });
@@ -10,12 +10,12 @@
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=20260813-scrollbar-stable-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260814-vditor-ui-fixes-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
17
17
  <div class="auth-card">
18
- <div class="auth-brand"><span class="brand-mark">叙</span><div><strong>叙界</strong><small>小说 AI 创作工作台</small></div></div>
18
+ <div class="auth-brand"><img class="brand-mark" src="/icon.svg?v=20260712" alt=""><div><strong>叙界</strong><small>小说 AI 创作工作台</small></div></div>
19
19
  <span class="eyebrow">账户与作品空间</span>
20
20
  <h1 id="auth-title">登录后继续创作</h1>
21
21
  <p id="auth-description">你的作品、协作权限和每一次修改都会绑定到账户。</p>
@@ -65,7 +65,7 @@
65
65
  <div id="app" class="app-shell">
66
66
  <header class="topbar">
67
67
  <button id="home-button" class="brand-block" type="button" aria-label="返回书架">
68
- <span class="brand-mark">叙</span>
68
+ <img class="brand-mark" src="/icon.svg?v=20260712" alt="">
69
69
  <div>
70
70
  <strong>叙界</strong>
71
71
  <small>小说 AI 创作工作台</small>
@@ -512,7 +512,7 @@
512
512
  <dialog id="form-dialog" class="dialog">
513
513
  <form id="dynamic-form" method="dialog">
514
514
  <div class="dialog-header">
515
- <div><span id="dialog-eyebrow" class="eyebrow">新增</span><h2 id="dialog-title">创建记录</h2><p id="dialog-meta" class="dialog-header-meta hidden"></p></div>
515
+ <div><span id="dialog-eyebrow" class="eyebrow">新增</span><h2 id="dialog-title">创建记录</h2><input id="dialog-title-input" class="dialog-title-input hidden" type="text" autocomplete="off" aria-label="记录标题"><p id="dialog-meta" class="dialog-header-meta hidden"></p></div>
516
516
  <button class="dialog-close" value="cancel" aria-label="关闭" type="submit">×</button>
517
517
  </div>
518
518
  <div id="dialog-fields" class="dialog-fields"></div>
@@ -1156,6 +1156,6 @@
1156
1156
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1157
1157
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1158
1158
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1159
- <script type="module" src="/app.js?v=20260813-outline-board-page-v1"></script>
1159
+ <script type="module" src="/app.js?v=20260814-vditor-ui-fixes-v1"></script>
1160
1160
  </body>
1161
1161
  </html>