@musnows/scriverse 0.8.8 → 0.9.1

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.
@@ -121,6 +121,10 @@ import {
121
121
  resizeCropRect
122
122
  } from "/avatar-crop.js?v=20260725-avatar-crop";
123
123
 
124
+ const DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS = 300;
125
+ const MIN_AI_ANALYSIS_TIMEOUT_SECONDS = 30;
126
+ const MAX_AI_ANALYSIS_TIMEOUT_SECONDS = 3_600;
127
+
124
128
  const defaultPageSizes = Object.freeze({
125
129
  drafts: 30,
126
130
  settings: 30,
@@ -3441,15 +3445,18 @@ async function createNewAiConversation(taskType = "chat") {
3441
3445
  function favoriteCharactersFirst(characters) {
3442
3446
  const items = Array.isArray(characters) ? characters : [];
3443
3447
  return [
3444
- ...items.filter((character) => character.isFavorite === true),
3445
- ...items.filter((character) => character.isFavorite !== true)
3448
+ ...items.filter((character) => character.isPinned === true && character.isFavorite === true),
3449
+ ...items.filter((character) => character.isPinned === true && character.isFavorite !== true),
3450
+ ...items.filter((character) => character.isPinned !== true && character.isFavorite === true),
3451
+ ...items.filter((character) => character.isPinned !== true && character.isFavorite !== true)
3446
3452
  ];
3447
3453
  }
3448
3454
 
3449
3455
  function roleplayCharacterOptionLabel(character) {
3450
- const favoriteLabel = character?.isFavorite === true ? "[已收藏] " : "";
3456
+ const pinLabel = character?.isPinned === true ? "[置顶]" : "";
3457
+ const favoriteLabel = character?.isFavorite === true ? "[收藏]" : "";
3451
3458
  const deathLabel = character?.isDead ? "(已死亡)" : "";
3452
- return `${favoriteLabel}${String(character?.name ?? "")}${deathLabel}`;
3459
+ return `${pinLabel}${favoriteLabel}${pinLabel || favoriteLabel ? " " : ""}${String(character?.name ?? "")}${deathLabel}`;
3453
3460
  }
3454
3461
 
3455
3462
  function renderAiRoleplayCharacterSelect() {
@@ -4105,6 +4112,31 @@ function aiPromptTextBeforeCursor() {
4105
4112
  return promptTextFromNode(fragment).replace(/\n$/u, "");
4106
4113
  }
4107
4114
 
4115
+ function aiPromptTextFromRange(range, prompt) {
4116
+ const beforeCursor = range.cloneRange();
4117
+ beforeCursor.selectNodeContents(prompt);
4118
+ beforeCursor.setEnd(range.endContainer, range.endOffset);
4119
+ const fragment = document.createElement("div");
4120
+ fragment.append(beforeCursor.cloneContents());
4121
+ return promptTextFromNode(fragment).replace(/\n$/u, "");
4122
+ }
4123
+
4124
+ function aiPromptTextBoundary(prompt, offset) {
4125
+ const walker = document.createTreeWalker(prompt, NodeFilter.SHOW_TEXT);
4126
+ let remaining = Math.max(0, Number(offset) || 0);
4127
+ let lastTextNode = null;
4128
+ while (walker.nextNode()) {
4129
+ const textNode = walker.currentNode;
4130
+ if (textNode.parentElement?.closest("[data-ai-reference-key]")) continue;
4131
+ lastTextNode = textNode;
4132
+ const length = textNode.textContent?.length ?? 0;
4133
+ if (remaining <= length) return { node: textNode, offset: remaining };
4134
+ remaining -= length;
4135
+ }
4136
+ if (lastTextNode) return { node: lastTextNode, offset: lastTextNode.textContent?.length ?? 0 };
4137
+ return { node: prompt, offset: prompt.childNodes.length };
4138
+ }
4139
+
4108
4140
  function hideAiMentionMenu() {
4109
4141
  aiMentionMatch = null;
4110
4142
  aiMentionRange = null;
@@ -4193,12 +4225,14 @@ function selectAiMention(button) {
4193
4225
  id: button.dataset.aiReferenceId,
4194
4226
  name: button.dataset.aiReferenceName
4195
4227
  };
4196
- const range = aiMentionRange.cloneRange();
4197
- const textNode = range.startContainer;
4198
- const localText = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent?.slice(0, range.startOffset) ?? "" : "";
4199
- const localMention = findAiMention(localText);
4228
+ const cursorText = aiPromptTextFromRange(aiMentionRange, prompt);
4229
+ const localMention = findAiMention(cursorText);
4200
4230
  if (!localMention) return hideAiMentionMenu();
4201
- range.setStart(textNode, localMention.start);
4231
+ const range = document.createRange();
4232
+ const startBoundary = aiPromptTextBoundary(prompt, localMention.start);
4233
+ const endBoundary = aiPromptTextBoundary(prompt, cursorText.length);
4234
+ range.setStart(startBoundary.node, startBoundary.offset);
4235
+ range.setEnd(endBoundary.node, endBoundary.offset);
4202
4236
  range.deleteContents();
4203
4237
  const spacer = document.createTextNode(" ");
4204
4238
  range.insertNode(spacer);
@@ -5143,11 +5177,17 @@ function showAuth(setupRequired, registrationOpen = false, setupTokenRequired =
5143
5177
  function applyAuthenticatedUser(session) {
5144
5178
  state.user = session.user;
5145
5179
  state.csrfToken = session.csrfToken;
5180
+ const isSystemAdmin = session.user.isSystemAdmin === true;
5181
+ const accountButton = $("#account-button");
5146
5182
  $("#account-name").textContent = session.user.displayName;
5147
5183
  renderUserAvatar($("#account-avatar"), session.user);
5184
+ accountButton.setAttribute("aria-label", isSystemAdmin
5185
+ ? `账户:${session.user.displayName},系统管理员`
5186
+ : `账户:${session.user.displayName}`);
5187
+ $("#account-admin-mark").classList.toggle("hidden", !isSystemAdmin);
5148
5188
  $("#account-menu-display-name").textContent = session.user.displayName;
5149
5189
  $("#account-menu-username").textContent = `@${session.user.username}`;
5150
- $("#account-menu-role").textContent = session.user.role === "admin" ? "系统管理员" : "普通用户";
5190
+ $("#account-menu-role").textContent = isSystemAdmin ? "系统管理员" : "普通用户";
5151
5191
  $("#auth-view").classList.add("hidden");
5152
5192
  document.documentElement.classList.remove("login-route");
5153
5193
  if (!session.csrfToken) document.body.classList.remove("auth-pending");
@@ -8677,6 +8717,18 @@ function characterFavoriteIconMarkup() {
8677
8717
  return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2L3 9.6l6.2-.9L12 3Z"></path></svg>';
8678
8718
  }
8679
8719
 
8720
+ function pushPinIconMarkup() {
8721
+ return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 9V4h1V2H7v2h1v5c0 1.66-1.34 3-3 3v2h5.97v6l1.03 1 1.03-1v-6H20v-2c-2.21 0-4-1.79-4-4Z"></path></svg>';
8722
+ }
8723
+
8724
+ function characterPinButton(item) {
8725
+ const isPinned = item.isPinned === true;
8726
+ const canPin = canEditModule("characters");
8727
+ const action = isPinned ? "取消置顶" : "置顶";
8728
+ const title = canPin ? action : "当前账户没有角色模块写入权限";
8729
+ return `<button class="character-pin-button${isPinned ? " is-pinned" : ""}" type="button" data-character-pin="${esc(item.id)}" aria-label="${action}角色“${esc(item.name)}”" aria-pressed="${isPinned}" title="${title}" ${canPin ? "" : "disabled"}>${pushPinIconMarkup()}</button>`;
8730
+ }
8731
+
8680
8732
  function characterFavoriteButton(item) {
8681
8733
  const isFavorite = item.isFavorite === true;
8682
8734
  const canFavorite = canEditModule("characters");
@@ -8703,6 +8755,17 @@ function recordFavoriteButton(type, item, { cardControl = false } = {}) {
8703
8755
  return `<button class="record-favorite-button${cardControl ? " is-card-control" : ""}${isFavorite ? " is-favorite" : ""}" type="button" data-record-favorite="${esc(item.id)}" data-record-favorite-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isFavorite}" title="${title}" ${canFavorite ? "" : "disabled"}>${characterFavoriteIconMarkup()}</button>`;
8704
8756
  }
8705
8757
 
8758
+ function recordPinButton(type, item, { cardControl = false } = {}) {
8759
+ const config = recordFavoriteConfigs[type];
8760
+ if (!config) return "";
8761
+ const isPinned = item.isPinned === true;
8762
+ const canPin = canEditModule(config.module);
8763
+ const action = isPinned ? "取消置顶" : "置顶";
8764
+ const title = canPin ? action : `当前账户没有${config.label}模块写入权限`;
8765
+ const name = String(item[config.nameField] ?? "");
8766
+ return `<button class="record-pin-button${cardControl ? " is-card-control" : ""}${isPinned ? " is-pinned" : ""}" type="button" data-record-pin="${esc(item.id)}" data-record-pin-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isPinned}" title="${title}" ${canPin ? "" : "disabled"}>${pushPinIconMarkup()}</button>`;
8767
+ }
8768
+
8706
8769
  async function toggleRecordFavorite(type, item, render) {
8707
8770
  const config = recordFavoriteConfigs[type];
8708
8771
  if (!config) return;
@@ -8719,6 +8782,22 @@ async function toggleRecordFavorite(type, item, render) {
8719
8782
  toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
8720
8783
  }
8721
8784
 
8785
+ async function toggleRecordPin(type, item, render) {
8786
+ const config = recordFavoriteConfigs[type];
8787
+ if (!config) return;
8788
+ const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/pin`, {
8789
+ method: "PATCH",
8790
+ body: { isPinned: item.isPinned !== true }
8791
+ });
8792
+ await render();
8793
+ const pinButton = [...$("#module-content").querySelectorAll("[data-record-pin]")].find((button) => (
8794
+ button.dataset.recordPinType === type && button.dataset.recordPin === String(updated.id)
8795
+ ));
8796
+ pinButton?.focus({ preventScroll: true });
8797
+ const name = String(updated[config.nameField] ?? "");
8798
+ toast(updated.isPinned ? `已置顶${config.label}“${name}”` : `已取消置顶${config.label}“${name}”`);
8799
+ }
8800
+
8722
8801
  function bindRecordFavoriteButtons(type, items, render) {
8723
8802
  const config = recordFavoriteConfigs[type];
8724
8803
  if (!config) return;
@@ -8735,6 +8814,22 @@ function bindRecordFavoriteButtons(type, items, render) {
8735
8814
  }));
8736
8815
  }
8737
8816
 
8817
+ function bindRecordPinButtons(type, items, render) {
8818
+ const config = recordFavoriteConfigs[type];
8819
+ if (!config) return;
8820
+ $("#module-content").querySelectorAll(`[data-record-pin-type="${type}"]`).forEach((button) => button.addEventListener("click", async () => {
8821
+ const item = items.find((candidate) => candidate.id === button.dataset.recordPin);
8822
+ if (!item || !canEditModule(config.module)) return;
8823
+ button.disabled = true;
8824
+ try {
8825
+ await toggleRecordPin(type, item, render);
8826
+ } catch (error) {
8827
+ button.disabled = false;
8828
+ toast(`${config.label}置顶状态更新失败:${error.message}`, "error");
8829
+ }
8830
+ }));
8831
+ }
8832
+
8738
8833
  function syncEntityDetailFavoriteButton(button, type, item) {
8739
8834
  const config = recordFavoriteConfigs[type];
8740
8835
  const visible = Boolean(config && item);
@@ -8759,6 +8854,30 @@ function syncEntityDetailFavoriteButton(button, type, item) {
8759
8854
  button.disabled = !canFavorite;
8760
8855
  }
8761
8856
 
8857
+ function syncEntityDetailPinButton(button, type, item) {
8858
+ const config = recordFavoriteConfigs[type];
8859
+ const visible = Boolean(config && item);
8860
+ button.classList.toggle("hidden", !visible);
8861
+ button.innerHTML = pushPinIconMarkup();
8862
+ if (!visible) {
8863
+ button.disabled = true;
8864
+ button.classList.remove("is-pinned");
8865
+ button.setAttribute("aria-pressed", "false");
8866
+ button.removeAttribute("aria-label");
8867
+ button.removeAttribute("title");
8868
+ return;
8869
+ }
8870
+ const isPinned = item.isPinned === true;
8871
+ const canPin = canEditModule(config.module);
8872
+ const action = isPinned ? "取消置顶" : "置顶";
8873
+ const name = String(item[config.nameField] ?? "");
8874
+ button.classList.toggle("is-pinned", isPinned);
8875
+ button.setAttribute("aria-pressed", String(isPinned));
8876
+ button.setAttribute("aria-label", `${action}${config.label}“${name}”`);
8877
+ button.title = canPin ? action : `当前账户没有${config.label}模块写入权限`;
8878
+ button.disabled = !canPin;
8879
+ }
8880
+
8762
8881
  function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
8763
8882
  syncEntityDetailFavoriteButton(button, type, getItem());
8764
8883
  button.onclick = async () => {
@@ -8783,6 +8902,30 @@ function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
8783
8902
  };
8784
8903
  }
8785
8904
 
8905
+ function bindEntityDetailPinButton(button, type, getItem, onUpdated) {
8906
+ syncEntityDetailPinButton(button, type, getItem());
8907
+ button.onclick = async () => {
8908
+ const config = recordFavoriteConfigs[type];
8909
+ const item = getItem();
8910
+ if (!config || !item || !canEditModule(config.module)) return;
8911
+ button.disabled = true;
8912
+ try {
8913
+ const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/pin`, {
8914
+ method: "PATCH",
8915
+ body: { isPinned: item.isPinned !== true }
8916
+ });
8917
+ onUpdated(updated);
8918
+ syncEntityDetailPinButton(button, type, getItem() ?? updated);
8919
+ button.focus({ preventScroll: true });
8920
+ const name = String(updated[config.nameField] ?? "");
8921
+ toast(updated.isPinned ? `已置顶${config.label}“${name}”` : `已取消置顶${config.label}“${name}”`);
8922
+ } catch (error) {
8923
+ syncEntityDetailPinButton(button, type, getItem());
8924
+ toast(`${config.label}置顶状态更新失败:${error instanceof Error ? error.message : "未知错误"}`, "error");
8925
+ }
8926
+ };
8927
+ }
8928
+
8786
8929
  function recordCardEditButton(attribute, id, label) {
8787
8930
  return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
8788
8931
  }
@@ -9121,7 +9264,7 @@ function settingRecordActions(item) {
9121
9264
  const recordAction = canEditModule("settings")
9122
9265
  ? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
9123
9266
  : recordHistoryButton("setting", item.id, item.title);
9124
- return `${recordFavoriteButton("setting", item)}${recordAction}`;
9267
+ return `${recordPinButton("setting", item)}${recordFavoriteButton("setting", item)}${recordAction}`;
9125
9268
  }
9126
9269
 
9127
9270
  function draftTypeLabel(draftType) {
@@ -9235,7 +9378,11 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
9235
9378
  }
9236
9379
  if (item && viewOnly) {
9237
9380
  const headerActions = $("#dialog-context-actions");
9238
- headerActions.innerHTML = `<button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
9381
+ headerActions.innerHTML = `<button id="draft-dialog-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button><button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
9382
+ bindEntityDetailPinButton($("#draft-dialog-pin"), "draft", () => draftDialogItem, (updated) => {
9383
+ draftDialogItem = updated;
9384
+ void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
9385
+ });
9239
9386
  bindEntityDetailFavoriteButton($("#draft-dialog-favorite"), "draft", () => draftDialogItem, (updated) => {
9240
9387
  draftDialogItem = updated;
9241
9388
  void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
@@ -9278,7 +9425,7 @@ async function renderDrafts(page = moduleListPages.drafts) {
9278
9425
  <details class="character-filter-dropdown"><summary><span>按绑定位置筛选</span><strong>${selectedBindingKeys.size ? `已选 ${selectedBindingKeys.size} 项` : "全部位置"}</strong></summary><div id="draft-binding-filter" class="character-filter-options">${bindingOptions.map(([value, label]) => `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedBindingKeys.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`).join("")}</div></details>
9279
9426
  <div class="character-filter-toolbar-actions">${hasDraftFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${drafts.length} 条想法</span>` : ""}<button id="clear-draft-filters" class="ghost-button" type="button" ${hasDraftFilters ? "" : "disabled"}>重置筛选</button></div>
9280
9427
  </section>`;
9281
- const actions = (item) => `${recordFavoriteButton("draft", item)}${canEditModule("drafts")
9428
+ const actions = (item) => `${recordPinButton("draft", item)}${recordFavoriteButton("draft", item)}${canEditModule("drafts")
9282
9429
  ? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
9283
9430
  : recordHistoryButton("draft", item.id, item.title)}`;
9284
9431
  const cards = `<div class="card-grid">${pageResult.items.map((item) => `
@@ -9340,6 +9487,10 @@ async function renderDrafts(page = moduleListPages.drafts) {
9340
9487
  moduleListPages.drafts = 1;
9341
9488
  await renderDrafts(1);
9342
9489
  });
9490
+ bindRecordPinButtons("draft", pageResult.items, async () => {
9491
+ moduleListPages.drafts = 1;
9492
+ await renderDrafts(1);
9493
+ });
9343
9494
  bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
9344
9495
  }
9345
9496
 
@@ -9407,6 +9558,10 @@ async function renderSettings(page = moduleListPages.settings) {
9407
9558
  moduleListPages.settings = 1;
9408
9559
  await renderSettings(1);
9409
9560
  });
9561
+ bindRecordPinButtons("setting", pageResult.items, async () => {
9562
+ moduleListPages.settings = 1;
9563
+ await renderSettings(1);
9564
+ });
9410
9565
  bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
9411
9566
  };
9412
9567
 
@@ -9458,6 +9613,21 @@ async function toggleCharacterFavorite(item) {
9458
9613
  toast(updated.isFavorite ? `已收藏角色“${updated.name}”` : `已取消收藏角色“${updated.name}”`);
9459
9614
  }
9460
9615
 
9616
+ async function toggleCharacterPin(item) {
9617
+ const updated = await api(`/api/characters/${encodeURIComponent(item.id)}/pin`, {
9618
+ method: "PATCH",
9619
+ body: { isPinned: item.isPinned !== true }
9620
+ });
9621
+ if (loadedAiReferencesWorkId === state.work?.id) {
9622
+ state.characters = upsertEntityCollection(state.characters, updated);
9623
+ renderAiRoleplayCharacterSelect();
9624
+ }
9625
+ characterListPage = 1;
9626
+ await renderCharacters(1);
9627
+ $("#module-content").querySelector(`[data-character-pin="${CSS.escape(String(updated.id))}"]`)?.focus({ preventScroll: true });
9628
+ toast(updated.isPinned ? `已置顶角色“${updated.name}”` : `已取消置顶角色“${updated.name}”`);
9629
+ }
9630
+
9461
9631
  async function renderCharacters(page = characterListPage) {
9462
9632
  const hasCharacterFilters = characterFilters.raceIds.length > 0
9463
9633
  || characterFilters.organizationIds.length > 0
@@ -9480,14 +9650,14 @@ async function renderCharacters(page = characterListPage) {
9480
9650
  [state.races, state.organizations] = [races, organizations];
9481
9651
  mountModuleCount(characterPage.total);
9482
9652
  const layout = readModuleLayout();
9483
- const characterActions = (item) => `${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
9653
+ const characterActions = (item) => `${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
9484
9654
  const characterLockBadge = (item) => item.lockedFields.length
9485
9655
  ? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
9486
9656
  : "";
9487
9657
  const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
9488
9658
  const details = normalizeCharacterDetails(item.attributes?.details);
9489
9659
  return `
9490
- <article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
9660
+ <article class="record-card character-card preview-record-card has-card-edit has-pin-control" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
9491
9661
  <div class="character-card-heading">${characterAvatarHtml(item)}<h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
9492
9662
  ${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
9493
9663
  <div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
@@ -9564,6 +9734,17 @@ async function renderCharacters(page = characterListPage) {
9564
9734
  toast(`角色收藏状态更新失败:${error.message}`, "error");
9565
9735
  }
9566
9736
  }));
9737
+ $("#module-content").querySelectorAll("[data-character-pin]").forEach((button) => button.addEventListener("click", async () => {
9738
+ const item = pageCharacters.find((candidate) => candidate.id === button.dataset.characterPin);
9739
+ if (!item || !canEditModule("characters")) return;
9740
+ button.disabled = true;
9741
+ try {
9742
+ await toggleCharacterPin(item);
9743
+ } catch (error) {
9744
+ button.disabled = false;
9745
+ toast(`角色置顶状态更新失败:${error.message}`, "error");
9746
+ }
9747
+ }));
9567
9748
  const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
9568
9749
  $("#character-race-filter").addEventListener("change", async () => {
9569
9750
  characterFiltersPanelOpen = true;
@@ -9718,14 +9899,14 @@ async function renderOrganizations(page = moduleListPages.organizations) {
9718
9899
  moduleListPages.organizations = pageResult.page;
9719
9900
  const layout = readModuleLayout();
9720
9901
  const canEditOrganizations = canEditModule("organizations");
9721
- const organizationActions = (item, { cardControl = false } = {}) => `${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
9902
+ const organizationActions = (item, { cardControl = false } = {}) => `${recordPinButton("organization", item, { cardControl })}${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
9722
9903
  ? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
9723
9904
  : recordHistoryButton("organization", item.id, item.name)}`;
9724
9905
  const organizationCardActions = (item) => canEditOrganizations
9725
9906
  ? organizationActions(item, { cardControl: true })
9726
9907
  : `<div class="card-actions">${organizationActions(item)}</div>`;
9727
9908
  const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
9728
- <article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
9909
+ <article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control has-pin-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
9729
9910
  <h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
9730
9911
  <div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
9731
9912
  <p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
@@ -9754,6 +9935,10 @@ async function renderOrganizations(page = moduleListPages.organizations) {
9754
9935
  moduleListPages.organizations = 1;
9755
9936
  await renderOrganizations(1);
9756
9937
  });
9938
+ bindRecordPinButtons("organization", pageResult.items, async () => {
9939
+ moduleListPages.organizations = 1;
9940
+ await renderOrganizations(1);
9941
+ });
9757
9942
  bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
9758
9943
  }
9759
9944
 
@@ -11375,7 +11560,7 @@ function openTaskDetailDialog(task, trace) {
11375
11560
  <p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
11376
11561
  <p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
11377
11562
  <div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
11378
- <div><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
11563
+ <div class="task-detail-failures"><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
11379
11564
  <div><strong>结果摘要</strong>${resultPreview}</div>
11380
11565
  ${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>` : ""}
11381
11566
  </section>
@@ -11414,7 +11599,7 @@ function renderProviderCards(providers, models, protocolOptions) {
11414
11599
  : "";
11415
11600
  return `
11416
11601
  <article class="record-card provider-card ${provider.status === "disabled" ? "is-disabled" : ""}"><div class="provider-card-meta"><small>平台级 · ${esc(providerProtocolLabel(provider.protocol, protocolOptions))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><span class="provider-status-badge ${providerStatusClass}">${esc(providerStatusLabel(provider.status))}</span></div><h3>${esc(provider.name)}</h3>
11417
- ${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n最大输出参数:${esc(provider.maxTokensParameter ?? "max_tokens")}\n思考类型:${esc(provider.thinkingType ?? "enabled")}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}\n每日 Token 额度:${provider.dailyTokenQuota === null || provider.dailyTokenQuota === undefined ? "未限制" : Number(provider.dailyTokenQuota).toLocaleString("zh-CN")} · 每月 Token 额度:${provider.monthlyTokenQuota === null || provider.monthlyTokenQuota === undefined ? "未限制" : Number(provider.monthlyTokenQuota).toLocaleString("zh-CN")}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
11602
+ ${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n最大输出参数:${esc(provider.maxTokensParameter ?? "max_tokens")}\n思考类型:${esc(provider.thinkingType ?? "enabled")}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}\n分析请求超时:${Number(provider.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS).toLocaleString("zh-CN")} 秒\n每日 Token 额度:${provider.dailyTokenQuota === null || provider.dailyTokenQuota === undefined ? "未限制" : Number(provider.dailyTokenQuota).toLocaleString("zh-CN")} · 每月 Token 额度:${provider.monthlyTokenQuota === null || provider.monthlyTokenQuota === undefined ? "未限制" : Number(provider.monthlyTokenQuota).toLocaleString("zh-CN")}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
11418
11603
  <div class="provider-models">${providerModels.map((model) => {
11419
11604
  const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
11420
11605
  const modelStatus = !model.enabled
@@ -13466,6 +13651,10 @@ function openSettingEditor(item = null, { readOnly = false } = {}) {
13466
13651
  const editButton = $("#setting-editor-edit");
13467
13652
  editButton.classList.toggle("hidden", !readOnly || !canEditModule("settings"));
13468
13653
  editButton.onclick = () => openSettingEditor(settingEditorItem);
13654
+ bindEntityDetailPinButton($("#setting-editor-pin"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
13655
+ settingEditorItem = updated;
13656
+ state.settings = upsertEntityCollection(state.settings, updated);
13657
+ });
13469
13658
  bindEntityDetailFavoriteButton($("#setting-editor-favorite"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
13470
13659
  settingEditorItem = updated;
13471
13660
  state.settings = upsertEntityCollection(state.settings, updated);
@@ -14702,6 +14891,13 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
14702
14891
  const editButton = $("#character-editor-edit");
14703
14892
  editButton.classList.toggle("hidden", !readOnly || !canEditModule("characters"));
14704
14893
  editButton.onclick = () => void openCharacterEditor(characterEditorItem);
14894
+ bindEntityDetailPinButton($("#character-editor-pin"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
14895
+ characterEditorItem = updated;
14896
+ if (loadedAiReferencesWorkId === state.work?.id) {
14897
+ state.characters = upsertEntityCollection(state.characters, updated);
14898
+ renderAiRoleplayCharacterSelect();
14899
+ }
14900
+ });
14705
14901
  bindEntityDetailFavoriteButton($("#character-editor-favorite"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
14706
14902
  characterEditorItem = updated;
14707
14903
  if (loadedAiReferencesWorkId === state.work?.id) {
@@ -14895,6 +15091,10 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
14895
15091
  editButton.textContent = `编辑${label}`;
14896
15092
  editButton.classList.toggle("hidden", !readOnly || !canEditModule(module));
14897
15093
  editButton.onclick = () => void openKnowledgeEditor(kind, knowledgeEditorItem);
15094
+ bindEntityDetailPinButton($("#knowledge-editor-pin"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
15095
+ knowledgeEditorItem = updated;
15096
+ state.organizations = upsertEntityCollection(state.organizations, updated);
15097
+ });
14898
15098
  bindEntityDetailFavoriteButton($("#knowledge-editor-favorite"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
14899
15099
  knowledgeEditorItem = updated;
14900
15100
  state.organizations = upsertEntityCollection(state.organizations, updated);
@@ -15810,6 +16010,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15810
16010
  const monthlyTokenQuota = item?.monthlyTokenQuota ?? null;
15811
16011
  const providerTokenQuotaFields = `<div class="form-field provider-token-quota-fields" data-provider-token-quota-fields><span>供应商 Token 额度</span><small>按服务器部署时区统计该供应商跨所有小说的输入与输出 Token;与单个小说额度独立。额度必须设置为大于 0;低于每日 10,000 或每月 1,000,000 时仅提示风险。</small><div class="provider-token-quota-row"><label class="checkbox-field provider-token-quota-toggle"><input name="dailyTokenQuotaEnabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}><span>启用每日额度</span></label><label class="provider-token-quota-input">每日额度<input name="dailyTokenQuota" type="number" min="1" max="2000000000" step="1" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="供应商每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label></div><div class="provider-token-quota-row"><label class="checkbox-field provider-token-quota-toggle"><input name="monthlyTokenQuotaEnabled" type="checkbox" ${monthlyTokenQuota === null ? "" : "checked"}><span>启用每月额度</span></label><label class="provider-token-quota-input">每月额度<input name="monthlyTokenQuota" type="number" min="1" max="2000000000" step="1" value="${esc(String(monthlyTokenQuota ?? 10000))}" aria-label="供应商每月 Token 额度" ${monthlyTokenQuota === null ? "disabled" : ""}></label></div></div>`;
15812
16012
  const thinkingTypeField = `<div class="form-field provider-thinking-type-field" data-provider-thinking-type-field><span>思考类型(开启时)</span><label class="checkbox-field"><input name="useAdaptiveThinking" type="checkbox" ${useAdaptiveThinking ? "checked" : ""} aria-describedby="provider-thinking-type-hint"><span>使用 adaptive(关闭时发送 enabled)</span></label><small id="provider-thinking-type-hint">关闭模型思考时仍发送 disabled;请只在供应商支持 adaptive 时开启。</small></div>`;
16013
+ const analysisTimeoutField = `<div class="form-field provider-analysis-timeout-field" data-provider-analysis-timeout-field><span>分析请求超时</span><label>单次请求(秒)<input name="analysisTimeoutSeconds" type="number" min="${MIN_AI_ANALYSIS_TIMEOUT_SECONDS}" max="${MAX_AI_ANALYSIS_TIMEOUT_SECONDS}" step="1" value="${esc(String(item?.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS))}" aria-describedby="provider-analysis-timeout-hint"></label><small id="provider-analysis-timeout-hint">用于全书分析和关系分析的单次非流式请求;默认 300 秒,可设置 30–3600 秒。</small></div>`;
15813
16014
  openDialog(
15814
16015
  item ? "编辑 AI 供应商" : "新建 AI 供应商",
15815
16016
  field("name", "显示名称", "text", item?.name)
@@ -15820,6 +16021,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15820
16021
  + thinkingTypeField
15821
16022
  + field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10)
15822
16023
  + field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10)
16024
+ + analysisTimeoutField
15823
16025
  + providerTokenQuotaFields
15824
16026
  + field("note", "用途备注", "textarea", item?.note)
15825
16027
  + field("enabled", item ? "启用供应商" : "立即启用", "checkbox", item ? item.status === "enabled" : true),
@@ -15832,11 +16034,17 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15832
16034
  thinkingType: form.get("useAdaptiveThinking") === "on" ? "adaptive" : "enabled",
15833
16035
  concurrencyLimit: Number(form.get("concurrencyLimit")),
15834
16036
  rpmLimit: Number(form.get("rpmLimit")),
16037
+ analysisTimeoutSeconds: Number(form.get("analysisTimeoutSeconds")),
15835
16038
  dailyTokenQuota: form.get("dailyTokenQuotaEnabled") === "on" ? Number(form.get("dailyTokenQuota")) : null,
15836
16039
  monthlyTokenQuota: form.get("monthlyTokenQuotaEnabled") === "on" ? Number(form.get("monthlyTokenQuota")) : null,
15837
16040
  note: form.get("note"),
15838
16041
  status: form.get("enabled") === "on" ? "enabled" : "disabled"
15839
16042
  };
16043
+ if (
16044
+ !Number.isInteger(body.analysisTimeoutSeconds)
16045
+ || body.analysisTimeoutSeconds < MIN_AI_ANALYSIS_TIMEOUT_SECONDS
16046
+ || body.analysisTimeoutSeconds > MAX_AI_ANALYSIS_TIMEOUT_SECONDS
16047
+ ) throw new Error("分析请求超时必须设置为 30–3600 秒的整数");
15840
16048
  if (!item || String(form.get("apiKey") ?? "").trim()) body.apiKey = form.get("apiKey");
15841
16049
  for (const [period, label] of [["daily", "每日"], ["monthly", "每月"]]) {
15842
16050
  const value = body[`${period}TokenQuota`];
@@ -15849,7 +16057,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15849
16057
  await loadModels();
15850
16058
  if (item) toast(connectivityConfigurationSavedToast("provider"));
15851
16059
  },
15852
- item ? "协议、限流与凭据" : "供应商协议、限流与凭据", {
16060
+ item ? "协议、超时、限流与凭据" : "供应商协议、超时、限流与凭据", {
15853
16061
  dangerAction: item ? { label: "删除供应商", onClick: () => deletePlatformProvider(item) } : null
15854
16062
  }
15855
16063
  );
@@ -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&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&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&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v3&feature=character-card-title-fit-v2">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -82,7 +82,7 @@
82
82
  <div id="presence-list" class="presence-list" aria-live="polite"></div>
83
83
  </section>
84
84
  </div>
85
- <button id="account-button" class="account-button" type="button" aria-haspopup="menu" aria-expanded="false"><span id="account-avatar" class="user-avatar account-avatar" aria-hidden="true"><span class="user-avatar-fallback">作</span></span><span id="account-name">账户</span></button>
85
+ <button id="account-button" class="account-button" type="button" aria-label="账户" aria-haspopup="menu" aria-expanded="false"><span class="account-avatar-frame"><span id="account-avatar" class="user-avatar account-avatar" aria-hidden="true"><span class="user-avatar-fallback">作</span></span><span id="account-admin-mark" class="account-admin-mark hidden" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M8 1.8 13 3.7v3.7c0 3.1-2 5.6-5 6.8-3-1.2-5-3.7-5-6.8V3.7L8 1.8Z"/><path d="m5.5 7.8 1.6 1.6 3.5-3.5"/></svg></span></span><span class="account-identity"><span id="account-name">账户</span></span></button>
86
86
  <div id="account-menu" class="account-menu hidden" role="menu">
87
87
  <strong id="account-menu-name" class="account-menu-name"><span id="account-menu-display-name">账户</span><span id="account-menu-username" class="account-menu-username">@account</span></strong><small id="account-menu-role">普通用户</small>
88
88
  <button id="onboarding-menu-button" type="button" role="menuitem">功能导览</button>
@@ -658,6 +658,7 @@
658
658
  </div>
659
659
  </div>
660
660
  <div class="entity-editor-mode-actions">
661
+ <button id="setting-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
661
662
  <button id="setting-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
662
663
  <button id="setting-editor-edit" class="ghost-button hidden" type="button">编辑设定</button>
663
664
  <button id="setting-editor-confirm" class="ghost-button hidden" type="button">确认候选</button>
@@ -685,6 +686,7 @@
685
686
  <div class="character-editor-title-row"><h1 id="character-editor-title">新建角色</h1><span id="character-editor-version" class="character-version-badge">新档案</span><span id="character-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span></div>
686
687
  </div>
687
688
  <div class="character-editor-header-actions">
689
+ <button id="character-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
688
690
  <button id="character-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
689
691
  <button id="character-editor-edit" class="primary-button hidden" type="button">编辑角色</button>
690
692
  <button id="character-history-button" class="ghost-button" type="button" aria-controls="character-history-panel" aria-expanded="false">版本历史</button>
@@ -721,6 +723,7 @@
721
723
  </div>
722
724
  <div class="character-editor-header-actions">
723
725
  <span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
726
+ <button id="knowledge-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
724
727
  <button id="knowledge-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
725
728
  <button id="knowledge-editor-edit" class="primary-button hidden" type="button">编辑档案</button>
726
729
  <button id="knowledge-editor-history" class="ghost-button hidden" type="button">版本历史</button>
@@ -1258,6 +1261,6 @@
1258
1261
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1259
1262
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1260
1263
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1261
- <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-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-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&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2"></script>
1264
+ <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-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-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&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1"></script>
1262
1265
  </body>
1263
1266
  </html>