@musnows/scriverse 0.6.4 → 0.6.6

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.
@@ -1,7 +1,7 @@
1
1
  import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260728-galaxy-edge-stars-v3";
2
2
  import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
3
3
  import { renderMarkdown } from "/markdown.js?v=20260731-no-external-images-v1";
4
- import { findAiMention, listAiMentionOptions, mergeAiReferenceScope } from "/ai-mentions.js?v=20260801-context-lock-v2";
4
+ import { findAiMention, listAiMentionOptions, mergeAiReferenceScope } from "/ai-mentions.js?v=20260801-context-setting-mention-v1";
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
7
  import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
@@ -16,7 +16,7 @@ import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-resu
16
16
  import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
17
17
  import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
18
18
  import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
19
- import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260725-unified-permissions";
19
+ import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260801-entity-lifecycle-v1";
20
20
  import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260731-drafts-to-ideas-v1";
21
21
  import {
22
22
  chapterVersionSourceLabel,
@@ -48,6 +48,12 @@ import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global
48
48
  import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260728-hybrid-search-v1";
49
49
  import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
50
50
  import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
51
+ import {
52
+ prepareTimelineEvents,
53
+ resolveTimelineActiveTrackId,
54
+ normalizeTimelineSortDirection,
55
+ timelineTrackColorIndex
56
+ } from "/timeline-view.js?v=20260801-timeline-sort-actions-v1";
51
57
  import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
52
58
  import { createModuleRequestCache } from "/module-request-cache.js?v=20260730-module-request-cache-v1";
53
59
  import { systemStatusPresentation } from "/system-status.js?v=20260801-system-health-v1";
@@ -177,6 +183,8 @@ const chapterBatchSelectedIds = new Set();
177
183
  let chapterMovePending = false;
178
184
 
179
185
  let timelineMultiSelectEnabled = false;
186
+ let timelineActiveTrackId = null;
187
+ let timelineSortDirection = "asc";
180
188
  let taskProgressRefreshTimer = null;
181
189
  let taskAutoRunEditing = false;
182
190
  let taskAutoRunEditingWorkId = null;
@@ -1285,7 +1293,7 @@ function aiReferenceKey(reference) {
1285
1293
  }
1286
1294
 
1287
1295
  function aiReferenceKindLabel(reference) {
1288
- return ({ character: "角色", setting: "设定", chapter: "章节" })[reference.kind] ?? "引用";
1296
+ return ({ character: "角色", setting: "设定", chapter: "章节", "context-settings": "能力" })[reference.kind] ?? "引用";
1289
1297
  }
1290
1298
 
1291
1299
  function createAiReferenceChip(reference) {
@@ -1872,15 +1880,12 @@ function syncAiTaskOptions() {
1872
1880
  $("#ai-scope").setAttribute("aria-hidden", String(roleplaySelected));
1873
1881
  $("#ai-roleplay-character").classList.toggle("hidden", !roleplaySelected);
1874
1882
  $("#ai-roleplay-character").setAttribute("aria-hidden", String(!roleplaySelected));
1875
- $(".ai-include-setting-info").classList.toggle("hidden", roleplaySelected);
1876
- $(".ai-include-setting-info").setAttribute("aria-hidden", String(roleplaySelected));
1877
1883
  $("#ai-task").disabled = state.aiPromptSent;
1878
1884
  $("#ai-task").title = state.aiPromptSent ? "对话开始后不能切换任务类型" : "";
1879
1885
  $("#ai-scope").disabled = roleplaySelected || state.aiPromptSent;
1880
1886
  $("#ai-scope").title = state.aiPromptSent
1881
1887
  ? "对话开始后不能切换上下文引用"
1882
1888
  : roleplaySelected ? "角色扮演模式只使用角色自身的记忆" : "";
1883
- syncAiIncludeSettingInfoControl();
1884
1889
  }
1885
1890
 
1886
1891
  function applyAiConversationTaskType(taskType) {
@@ -1899,7 +1904,6 @@ function applyAiConversationContextScope(scope) {
1899
1904
  : normalizedScope.type === "chapter" && normalizedScope.includeBookSummary ? "chapter-summary"
1900
1905
  : normalizedScope.type === "chapter" ? "chapter"
1901
1906
  : "none";
1902
- $("#ai-include-setting-info").checked = normalizedScope.includeSettingInfo !== false;
1903
1907
  syncAiTaskOptions();
1904
1908
  }
1905
1909
 
@@ -2046,10 +2050,11 @@ function updateAiMentionMenu() {
2046
2050
  ...chapter,
2047
2051
  volumeTitle: volume.title
2048
2052
  }))) ?? [];
2049
- const options = listAiMentionOptions(state.characters, state.settings, chapters, match.query);
2053
+ const options = listAiMentionOptions(state.characters, state.settings, chapters, match.query)
2054
+ .filter((item) => item.kind !== "context-settings" || $("#ai-task").value !== "roleplay");
2050
2055
  menu.innerHTML = options.length
2051
2056
  ? options.map((item) => `<button class="ai-mention-option" type="button" role="option" data-ai-reference-kind="${esc(item.kind)}" data-ai-reference-id="${esc(item.id)}" data-ai-reference-name="${esc(item.name)}"><small>${esc(item.kindLabel)}</small><strong>${esc(item.name)}</strong></button>`).join("")
2052
- : '<p class="ai-mention-empty">没有匹配的角色、设定或章节</p>';
2057
+ : '<p class="ai-mention-empty">没有匹配的角色、设定、章节或上下文能力</p>';
2053
2058
  menu.classList.remove("hidden");
2054
2059
  }
2055
2060
 
@@ -2271,7 +2276,7 @@ function clearChapterLineSelection() {
2271
2276
  }
2272
2277
 
2273
2278
  const typographyStorageKey = "ai-novel-typography-v1";
2274
- const typographyDefaults = Object.freeze({ cjkFont: "system", latinFont: "system", fontSize: 17, uiFontSize: 16, aiFontSize: 14, density: "balanced" });
2279
+ const typographyDefaults = Object.freeze({ cjkFont: "system", latinFont: "system", fontSize: 17, uiFontSize: 14, aiFontSize: 14, density: "balanced" });
2275
2280
  const cjkFontStacks = {
2276
2281
  system: '"PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "Heiti SC"',
2277
2282
  pingfang: '"PingFang SC", "Heiti SC", "Microsoft YaHei", "Noto Sans CJK SC"',
@@ -4124,6 +4129,9 @@ function resetWorkScopedUiCaches() {
4124
4129
  Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
4125
4130
  relationshipFilters.fromCharacterIds = [];
4126
4131
  relationshipFilters.toCharacterIds = [];
4132
+ timelineActiveTrackId = null;
4133
+ timelineSortDirection = "asc";
4134
+ timelineMultiSelectEnabled = false;
4127
4135
  taskListPage = 1;
4128
4136
  state.collapsedVolumeIds.clear();
4129
4137
  loadedVolumeChapterIds.clear();
@@ -5363,6 +5371,10 @@ function renderSettingRows(records) {
5363
5371
  }).join("")}</div>`;
5364
5372
  }
5365
5373
 
5374
+ function entityLifecycleBadge(active, label) {
5375
+ return active ? `<span class="entity-lifecycle-badge">${esc(label)}</span>` : "";
5376
+ }
5377
+
5366
5378
  async function renderSettings(page = moduleListPages.settings) {
5367
5379
  const records = await moduleApiAllPages("settings", `/api/works/${state.work.id}/settings`);
5368
5380
  state.settings = records;
@@ -5408,7 +5420,7 @@ async function renderCharacters(page = characterListPage) {
5408
5420
  const details = normalizeCharacterDetails(item.attributes?.details);
5409
5421
  return `
5410
5422
  <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)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
5411
- <div class="character-card-heading"><h3>${esc(item.name)}</h3>${characterLockBadge(item)}</div>
5423
+ <div class="character-card-heading"><h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
5412
5424
  ${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
5413
5425
  ${item.aliases.length ? `<div class="character-aliases"><b>别名</b>${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>` : ""}
5414
5426
  ${item.code ? `<div class="character-code"><b>编号</b><span class="pill">${esc(item.code)}</span></div>` : ""}
@@ -5430,7 +5442,7 @@ async function renderCharacters(page = characterListPage) {
5430
5442
  const line = meta ? `${meta} · ${preview}` : preview;
5431
5443
  return `
5432
5444
  <article class="record-card module-row character-row character-card preview-record-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">
5433
- <div class="character-card-heading"><h3>${esc(item.name)}</h3>${characterLockBadge(item)}</div>
5445
+ <div class="character-card-heading"><h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
5434
5446
  <p class="module-row-preview" title="${esc(line)}">${esc(line)}</p>
5435
5447
  <div class="card-actions">${characterActions(item)}</div>
5436
5448
  </article>`;
@@ -5508,7 +5520,7 @@ function renderRaceCollection(total, descendantsLoading = false) {
5508
5520
  ? raceActions(item)
5509
5521
  : `<div class="card-actions">${raceActions(item)}</div>`;
5510
5522
  const renderRaceNode = (item) => `<details class="race-tree-node"${state.collapsedRaceIds.has(item.id) ? "" : " open"} data-race-node="${esc(item.id)}">
5511
- <summary><span>${esc(item.name)}</span><small>${directChildCount(item)} 个直接子种族</small></summary>
5523
+ <summary><span>${esc(item.name)}${entityLifecycleBadge(item.isExtinct, "已灭绝")}</span><small>${directChildCount(item)} 个直接子种族</small></summary>
5512
5524
  <div class="race-tree-branch">
5513
5525
  <article class="record-card race-card preview-record-card${canEditRaces ? " has-card-edit" : ""}" data-open-race="${esc(item.id)}" role="button" tabindex="0" aria-label="查看种族 ${esc(item.name)}"><small>${item.memberIds.length} 位直接角色 · ${item.settingsCount ?? item.settings?.length ?? 0} 条自身设定</small>
5514
5526
  <div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
@@ -5526,7 +5538,7 @@ function renderRaceCollection(total, descendantsLoading = false) {
5526
5538
  return `
5527
5539
  <article class="record-card module-row race-card preview-record-card" data-open-race="${esc(item.id)}" role="button" tabindex="0" aria-label="查看种族 ${esc(item.name)}">
5528
5540
  <small>${esc(meta)}</small>
5529
- <h3>${esc(item.name)}<span class="module-row-path">${esc(racePathLabel(item))}</span></h3>
5541
+ <h3>${esc(item.name)}${entityLifecycleBadge(item.isExtinct, "已灭绝")}<span class="module-row-path">${esc(racePathLabel(item))}</span></h3>
5530
5542
  <p class="module-row-preview" title="${esc(preview)}">${esc(preview)}${item.members.length ? ` · ${esc(item.members.map((member) => member.name).join("、"))}` : ""}</p>
5531
5543
  <div class="card-actions">${raceActions(item)}</div>
5532
5544
  </article>`;
@@ -5613,7 +5625,7 @@ async function renderOrganizations(page = moduleListPages.organizations) {
5613
5625
  : `<div class="card-actions">${organizationActions(item)}</div>`;
5614
5626
  const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
5615
5627
  <article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit" : ""}" 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>
5616
- <h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
5628
+ <h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
5617
5629
  <div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
5618
5630
  <p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
5619
5631
  ${organizationCardActions(item)}
@@ -5624,7 +5636,7 @@ async function renderOrganizations(page = moduleListPages.organizations) {
5624
5636
  return `
5625
5637
  <article class="record-card module-row organization-card" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}">
5626
5638
  <small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
5627
- <h3>${esc(item.name)}</h3>
5639
+ <h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3>
5628
5640
  <p class="module-row-preview" title="${esc(`${preview} · 成员:${members}`)}">${esc(preview)} · ${esc(members)}</p>
5629
5641
  <div class="card-actions">${organizationActions(item)}</div>
5630
5642
  </article>`;
@@ -5667,24 +5679,63 @@ async function renderTimeline(page = moduleListPages.timeline) {
5667
5679
  moduleApiAllPages("timeline", `/api/works/${state.work.id}/timeline`),
5668
5680
  moduleApiAllPages("timeline", `/api/works/${state.work.id}/timeline-tracks`)
5669
5681
  ]);
5670
- mountModuleCount(events.length);
5671
- const pageResult = paginateModuleItems(events, page, "timeline");
5682
+ state.timelineTracks = tracks;
5683
+ const orderedTracks = [...tracks].sort((left, right) => {
5684
+ const orderDelta = Number(left.sortOrder ?? 0) - Number(right.sortOrder ?? 0);
5685
+ if (orderDelta !== 0) return orderDelta;
5686
+ return String(left.id).localeCompare(String(right.id));
5687
+ });
5688
+ const tabTracks = [
5689
+ ...orderedTracks,
5690
+ { id: "", name: "未分组", description: "尚未归入独立时间轴的时间节点。" }
5691
+ ];
5692
+ timelineActiveTrackId = resolveTimelineActiveTrackId(timelineActiveTrackId, tracks);
5693
+ timelineSortDirection = normalizeTimelineSortDirection(timelineSortDirection);
5694
+ const activeTrack = tabTracks.find((track) => String(track.id ?? "") === String(timelineActiveTrackId)) ?? tabTracks[tabTracks.length - 1];
5695
+ const filteredEvents = prepareTimelineEvents(events, { trackIds: [timelineActiveTrackId] }, { direction: timelineSortDirection });
5696
+ mountModuleCount(filteredEvents.length);
5697
+ const pageResult = paginateModuleItems(filteredEvents, page, "timeline");
5672
5698
  moduleListPages.timeline = pageResult.page;
5673
5699
  timelineMultiSelectEnabled = false;
5674
5700
  $("#timeline-tools")?.remove();
5701
+ $("#module-header-actions").querySelector('[data-module-header-action="timeline-filter-toggle"]')?.remove();
5675
5702
  $("#module-header-actions").insertAdjacentHTML("beforeend", `<div id="timeline-tools" class="timeline-tools" data-module-header-action="timeline-tools" role="group" aria-label="时间轴操作"><button id="create-timeline-track" class="ghost-button" type="button">新建独立时间轴</button><button id="timeline-multi-select-toggle" class="ghost-button" type="button" aria-pressed="false">多选</button>${events.length > 1 ? '<button id="merge-events" class="ghost-button" type="button" hidden>合并所选事件</button>' : ""}</div>`);
5676
- state.timelineTracks = tracks;
5677
- const lanes = [...tracks, { id: "", name: "未分组时间轴", description: "尚未归入独立大事件的时间节点。", sortOrder: Number.MAX_SAFE_INTEGER }];
5678
- const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(timelineStatusLabel(item.status))}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
5679
- $("#module-content").innerHTML = `<div class="timeline-kanban" data-testid="timeline-kanban">${lanes.map((track) => {
5680
- const laneEvents = pageResult.items.filter((item) => (item.trackId ?? "") === track.id);
5681
- return `<section class="timeline-lane" data-track-id="${esc(track.id)}"><header><div><small>${laneEvents.length} 个节点</small><h3>${esc(track.name)}</h3></div>${track.id ? `<div class="timeline-track-actions"><button class="timeline-track-menu" data-edit-timeline-track="${esc(track.id)}" type="button">编辑</button><button class="timeline-track-menu" data-entity-history="timeline-track" data-entity-id="${esc(track.id)}" data-entity-title="${esc(track.name)}" type="button">历史</button></div>` : ""}</header><p class="timeline-track-description">${esc(track.description || "暂无说明")}</p><div class="timeline-lane-events">${laneEvents.map(eventCard).join("") || '<div class="timeline-lane-empty">还没有时间节点</div>'}</div><button class="timeline-add-event" data-add-event-track="${esc(track.id)}" type="button">添加事件</button></section>`;
5682
- }).join("")}</div>${renderModulePagination(pageResult, "timeline", "时间线事件")}`;
5703
+ const tabsMarkup = `<div class="timeline-track-tabs" role="tablist" aria-label="时间轴轨道">${tabTracks.map((track) => {
5704
+ const trackKey = String(track.id ?? "");
5705
+ const selected = trackKey === String(timelineActiveTrackId);
5706
+ const colorIndex = timelineTrackColorIndex(track.id, tracks);
5707
+ return `<button type="button" class="timeline-track-tab" role="tab" id="timeline-track-tab-${esc(trackKey || "ungrouped")}" data-timeline-track-tab="${esc(trackKey)}" data-track-color-index="${colorIndex}" aria-selected="${selected}" aria-controls="timeline-track-panel" tabindex="${selected ? "0" : "-1"}"><span class="timeline-track-swatch" aria-hidden="true"></span><span>${esc(track.name)}</span></button>`;
5708
+ }).join("")}</div>`;
5709
+ const trackManageActions = activeTrack.id
5710
+ ? `<button class="timeline-track-menu" data-edit-timeline-track="${esc(activeTrack.id)}" type="button">编辑</button><button class="timeline-track-menu" data-entity-history="timeline-track" data-entity-id="${esc(activeTrack.id)}" data-entity-title="${esc(activeTrack.name)}" type="button">历史</button>`
5711
+ : "";
5712
+ const panelMarkup = `<section id="timeline-track-panel" class="timeline-track-panel" data-track-color-index="${timelineTrackColorIndex(activeTrack.id, tracks)}" role="tabpanel" aria-labelledby="timeline-track-tab-${esc(String(activeTrack.id || "ungrouped"))}"><div class="timeline-track-panel-copy"><strong>${esc(activeTrack.name)}</strong><p>${esc(activeTrack.description || "暂无说明")}</p></div><div class="timeline-track-panel-actions"><div class="timeline-track-actions" role="group" aria-label="当前轨道操作">${trackManageActions}<button class="timeline-track-menu" data-add-event-track="${esc(String(activeTrack.id ?? ""))}" type="button">添加事件</button><button class="timeline-track-menu" data-timeline-sort="asc" type="button" aria-pressed="${timelineSortDirection === "asc"}">正序</button><button class="timeline-track-menu" data-timeline-sort="desc" type="button" aria-pressed="${timelineSortDirection === "desc"}">倒序</button></div></div></section>`;
5713
+ const eventItem = (item) => {
5714
+ const trackKey = item.trackId ?? "";
5715
+ const colorIndex = timelineTrackColorIndex(trackKey, tracks);
5716
+ return `<article class="timeline-item" data-track-color-index="${colorIndex}" data-event-id="${esc(item.id)}"><div class="timeline-card-meta"><input class="timeline-select" type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(timelineStatusLabel(item.status))}</small></div><h3>${esc(item.name)}</h3><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span class="timeline-item-location">地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}" type="button">编辑与排序</button><button data-split-event="${esc(item.id)}" type="button">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}" type="button">版本历史</button></div></article>`;
5717
+ };
5718
+ const axisMarkup = pageResult.items.length
5719
+ ? `<div class="timeline-axis" data-testid="timeline-axis"><div class="timeline-list">${pageResult.items.map(eventItem).join("")}</div></div>`
5720
+ : `<div class="timeline-axis" data-testid="timeline-axis"><div class="empty-state"><b>当前轨道还没有时间节点</b>可以添加事件,或切换到其他轨道查看。</div></div>`;
5721
+ $("#module-content").innerHTML = `${tabsMarkup}${panelMarkup}${axisMarkup}${renderModulePagination(pageResult, "timeline", "时间线事件")}`;
5683
5722
  bindModulePagination("timeline", renderTimeline);
5684
5723
  $("#create-timeline-track").addEventListener("click", () => openTimelineTrackDialog());
5685
5724
  $("#timeline-multi-select-toggle").addEventListener("click", () => setTimelineMultiSelectMode(!timelineMultiSelectEnabled));
5686
5725
  $("#module-content").querySelectorAll("[data-event-select]").forEach((input) => input.addEventListener("change", updateTimelineMultiSelectControls));
5687
5726
  setTimelineMultiSelectMode(false);
5727
+ $("#module-content").querySelectorAll("[data-timeline-track-tab]").forEach((button) => button.addEventListener("click", async () => {
5728
+ const nextTrackId = button.dataset.timelineTrackTab ?? "";
5729
+ if (String(nextTrackId) === String(timelineActiveTrackId)) return;
5730
+ timelineActiveTrackId = nextTrackId;
5731
+ await renderTimeline(1);
5732
+ }));
5733
+ $("#module-content").querySelectorAll("[data-timeline-sort]").forEach((button) => button.addEventListener("click", async () => {
5734
+ const nextDirection = normalizeTimelineSortDirection(button.dataset.timelineSort);
5735
+ if (nextDirection === timelineSortDirection) return;
5736
+ timelineSortDirection = nextDirection;
5737
+ await renderTimeline(1);
5738
+ }));
5688
5739
  $("#module-content").querySelectorAll("[data-edit-timeline-track]").forEach((button) => button.addEventListener("click", () => openTimelineTrackDialog(tracks.find((track) => track.id === button.dataset.editTimelineTrack))));
5689
5740
  $("#module-content").querySelectorAll("[data-add-event-track]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(null, button.dataset.addEventTrack || null)));
5690
5741
  $("#module-content").querySelectorAll("[data-edit-event]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(events.find((item) => item.id === button.dataset.editEvent))));
@@ -7216,6 +7267,7 @@ async function renderBookAiSettings() {
7216
7267
  title: "本书 Token 用量",
7217
7268
  description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
7218
7269
  })}</section><section class="config-section"><div class="config-section-header"><div><h2>每日 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然日可使用的输入与输出 Token 总量。额度最低为 10,000;达到额度后,新的 AI 请求会等到后端时区的次日零点重置后再执行。</p></div></div><div class="config-inline-save"><label><input id="daily-token-quota-enabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}>启用每日额度</label><label class="daily-token-quota-field">每日额度<input id="daily-token-quota" type="number" min="10000" max="2000000000" step="1000" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="本书每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label><button id="save-daily-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="daily-token-quota-status" class="usage-measurement-note" role="status">${esc(quotaStatusText)}</p></section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>对话 context 使用独立预算。达到该百分比阈值时先提醒;继续发送会对较早消息执行 compact,压缩上下文占用,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>Agent 工具调用上限</h2><p>限制单次回答里 Agent 可调用工具的次数,并用「全局倍数」给整次回答加一道不会因 Compact 重置的熔断阀,防止工具死循环空耗 Token。调用上限 5–48(默认 12);全局倍数 1–6(默认 3,全局上限 = 调用上限 × 倍数)。<a class="config-doc-link" href="https://scriverse.top/docs/global-tool-call-limit.html" target="_blank" rel="noopener noreferrer">了解原理与推荐设置</a></p></div></div><div class="config-inline-save"><label class="agent-tool-call-limit-field">调用上限<input id="agent-tool-call-limit" type="number" min="5" max="48" value="${esc(String(settings.agentToolCallLimit ?? 12))}" aria-label="Agent 工具调用上限"></label><div class="agent-tool-call-global-multiplier-field"><span id="agent-tool-call-global-multiplier-label">全局倍数</span><div class="settings-layout-toggle agent-tool-call-global-multiplier-toggle" role="group" aria-labelledby="agent-tool-call-global-multiplier-label">${[1, 2, 3, 4, 5, 6].map((value) => `<button type="button" data-global-multiplier="${value}" aria-pressed="${Number(settings.agentToolCallGlobalMultiplier ?? 3) === value}">${value}</button>`).join("")}</div><input id="agent-tool-call-global-multiplier" type="hidden" value="${esc(String(Math.min(6, Math.max(1, Number(settings.agentToolCallGlobalMultiplier ?? 3) || 3))))}" aria-label="Agent 工具调用全局倍数"></div><button id="save-agent-tool-call-limit" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。已开始的对话会锁定创建时的工具集,修改后仅对新对话生效,避免打断 prompt cache。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名、拼音或短关键词混合检索设定、人物、组织、时间线、关系、大纲和伏笔;非语义问答。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults, settings)}`;
7270
+ $("#daily-token-quota-status").closest(".config-section").insertAdjacentHTML("afterend", `<section class="config-section"><div class="config-section-header"><div><h2>设定上下文注入</h2><p>开启后,本书的普通 AI 请求会自动注入锁定设定、组织、种族与相关约束;即使本轮同时使用“@注入上下文设定”,也只会注入一次。</p></div></div><div class="config-inline-save"><label><input id="always-include-setting-info" type="checkbox" ${settings.alwaysIncludeSettingInfo ? "checked" : ""}>是否注入设定</label><button id="save-always-include-setting-info" class="ghost-button config-save-button" type="button">保存</button></div></section>`);
7219
7271
  bindUsageCalendarInteractions(host);
7220
7272
  scrollUsageCalendarsToLatest(host);
7221
7273
  host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
@@ -7287,6 +7339,20 @@ async function renderBookAiSettings() {
7287
7339
  button.disabled = false;
7288
7340
  }
7289
7341
  });
7342
+ $("#save-always-include-setting-info").addEventListener("click", async () => {
7343
+ const button = $("#save-always-include-setting-info");
7344
+ button.disabled = true;
7345
+ try {
7346
+ const alwaysIncludeSettingInfo = $("#always-include-setting-info").checked;
7347
+ await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { alwaysIncludeSettingInfo } });
7348
+ toast(alwaysIncludeSettingInfo ? "已开启全局设定上下文注入" : "已关闭全局设定上下文注入");
7349
+ setAiContextMeter(null);
7350
+ } catch (error) {
7351
+ toast(error.message, "error");
7352
+ } finally {
7353
+ button.disabled = false;
7354
+ }
7355
+ });
7290
7356
  $("#save-work-system-prompt").addEventListener("click", async () => {
7291
7357
  const button = $("#save-work-system-prompt");
7292
7358
  button.disabled = true;
@@ -7472,22 +7538,6 @@ async function ensureAiModelsLoaded() {
7472
7538
  }
7473
7539
  }
7474
7540
 
7475
- function syncAiIncludeSettingInfoControl() {
7476
- const scopeType = $("#ai-scope").value;
7477
- const checkbox = $("#ai-include-setting-info");
7478
- const proseScopes = new Set(["chapter", "chapter-summary", "volume", "book"]);
7479
- const enabled = proseScopes.has(scopeType);
7480
- const roleplaySelected = $("#ai-task").value === "roleplay";
7481
- checkbox.disabled = roleplaySelected || state.aiPromptSent || !enabled;
7482
- checkbox.title = state.aiPromptSent
7483
- ? "对话开始后不能切换上下文选项"
7484
- : enabled
7485
- ? "在正文上下文中注入锁定设定、组织/种族简表等"
7486
- : scopeType === "settings-catalog"
7487
- ? "设定库范围会直接注入设定目录,无需此选项"
7488
- : "仅在选择正文类上下文范围时可用";
7489
- }
7490
-
7491
7541
  function currentAiRequestScope() {
7492
7542
  if (!state.work) return null;
7493
7543
  const selectedTaskType = $("#ai-task").value;
@@ -7495,6 +7545,7 @@ function currentAiRequestScope() {
7495
7545
  const taskType = roleplaySelected ? "chat" : selectedTaskType;
7496
7546
  if (state.aiPromptSent) {
7497
7547
  const conversationScope = JSON.parse(JSON.stringify(state.aiContextScope ?? { type: "none" }));
7548
+ conversationScope.includeSettingInfo = false;
7498
7549
  const scope = mergeAiReferenceScope(conversationScope, state.aiReferences);
7499
7550
  return { taskType, scope, conversationScope, selection: typeof conversationScope.selection === "string" ? conversationScope.selection : "" };
7500
7551
  }
@@ -7511,10 +7562,7 @@ function currentAiRequestScope() {
7511
7562
  : scopeType === "settings-catalog" ? { type: "settings-catalog" }
7512
7563
  : { type: "chapter", chapterId: state.chapter?.id };
7513
7564
  if (includeBookSummary) conversationScope.includeBookSummary = true;
7514
- const proseScopes = new Set(["chapter", "chapter-summary", "volume", "book"]);
7515
- if (proseScopes.has(scopeType) || taskType === "polish") {
7516
- conversationScope.includeSettingInfo = $("#ai-include-setting-info").checked;
7517
- }
7565
+ conversationScope.includeSettingInfo = false;
7518
7566
  const scope = mergeAiReferenceScope(conversationScope, state.aiReferences);
7519
7567
  return { taskType, scope, conversationScope, selection };
7520
7568
  }
@@ -8947,6 +8995,7 @@ function renderCharacterEditorFields(item) {
8947
8995
  $("#character-editor-fields").innerHTML = [
8948
8996
  characterEditorSection("basic", "基础资料", "用于检索、去重和建立人物在作品中的基本归属。",
8949
8997
  field("name", "标准名", "text", item?.name) +
8998
+ field("isDead", "标记为已死亡", "checkbox", item?.isDead ?? false) +
8950
8999
  field("aliases", "别名", "item-list", item?.aliases ?? []) +
8951
9000
  (!canReadModule("races")
8952
9001
  ? '<div class="character-editor-empty-field"><b>种族</b><span>当前账户没有种族模块读取权限,原有绑定不会被修改。</span></div>'
@@ -8999,6 +9048,7 @@ function collectCharacterBody(form) {
8999
9048
  delete profile.sections;
9000
9049
  const body = {
9001
9050
  name: String(form.get("name") ?? "").trim(),
9051
+ isDead: form.has("isDead"),
9002
9052
  code: String(form.get("code") ?? "").trim(),
9003
9053
  aliases: form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean),
9004
9054
  attributes: {
@@ -9207,7 +9257,10 @@ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
9207
9257
  ? [["basic", "基础资料", "名称、层级与简介"], ["settings", "共同设定", "Markdown 设定章节"], ["members", "种族成员", "角色归属"]]
9208
9258
  : [["basic", "基础资料", "名称与简介"], ["settings", "组织设定", "Markdown 设定章节"], ["members", "组织成员", "角色归属"]];
9209
9259
  $("#knowledge-editor-nav").innerHTML = tabs.map(([key, tabTitle, description], index) => `<button type="button" role="tab" data-knowledge-editor-tab="${key}" aria-selected="${index === 0}" tabindex="${index === 0 ? "0" : "-1"}">${tabTitle}<small>${description}</small></button>`).join("");
9210
- const basicFields = field("name", `${label}名称`, "text", item?.name, []) + (isRace ? field("parentRaceId", "父种族", "select", item?.parentRaceId ?? "", parentOptions) : "") + field("description", `${label}简介`, "textarea", item?.description, []);
9260
+ const basicFields = field("name", `${label}名称`, "text", item?.name, [])
9261
+ + field(isRace ? "isExtinct" : "isDissolved", isRace ? "标记为已灭绝" : "标记为已解散", "checkbox", isRace ? item?.isExtinct ?? false : item?.isDissolved ?? false)
9262
+ + (isRace ? field("parentRaceId", "父种族", "select", item?.parentRaceId ?? "", parentOptions) : "")
9263
+ + field("description", `${label}简介`, "textarea", item?.description, []);
9211
9264
  const memberField = memberOptions.length
9212
9265
  ? field("memberIds", isRace ? "属于该种族的角色(可多选)" : "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions)
9213
9266
  : `<div class="character-editor-empty-field"><strong>${isRace ? "种族成员" : "组织成员"}</strong><span>当前还没有可绑定的角色。</span></div>`;
@@ -9313,8 +9366,8 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
9313
9366
  if (untitled >= 0) throw new Error(`请填写第 ${untitled + 1} 条 Markdown 设定的标题`);
9314
9367
  const settingsMarkdown = settingsSections.map((section) => section.contentMarkdown).join("\n\n");
9315
9368
  const body = isRace
9316
- ? { name, parentRaceId: data.get("parentRaceId") || null, description: data.get("description"), settingsMarkdown, settingsSections }
9317
- : { name, description: data.get("description"), settingsMarkdown, settingsSections };
9369
+ ? { name, isExtinct: data.has("isExtinct"), parentRaceId: data.get("parentRaceId") || null, description: data.get("description"), settingsMarkdown, settingsSections }
9370
+ : { name, isDissolved: data.has("isDissolved"), description: data.get("description"), settingsMarkdown, settingsSections };
9318
9371
  if (canReadModule("characters")) body.memberIds = data.getAll("memberIds").map(String);
9319
9372
  const wasEditing = Boolean(knowledgeEditorItem);
9320
9373
  if (!(await confirmConcurrentSave())) return;
@@ -9343,7 +9396,7 @@ async function openOrganizationDialog(item, options) {
9343
9396
  }
9344
9397
 
9345
9398
  function openTimelineTrackDialog(item) {
9346
- openDialog(item ? "编辑独立时间轴" : "新建独立时间轴", field("name", "时间轴名称", "text", item?.name) + field("description", "时间轴简介", "textarea", item?.description) + field("sortOrder", "看板排序", "number", item?.sortOrder ?? state.timelineTracks.length), async (form) => {
9399
+ openDialog(item ? "编辑独立时间轴" : "新建独立时间轴", field("name", "时间轴名称", "text", item?.name) + field("description", "时间轴简介", "textarea", item?.description) + field("sortOrder", "轨道排序", "number", item?.sortOrder ?? state.timelineTracks.length), async (form) => {
9347
9400
  await api(item ? `/api/timeline-tracks/${item.id}` : `/api/works/${state.work.id}/timeline-tracks`, { method: item ? "PATCH" : "POST", body: { name: form.get("name"), description: form.get("description"), sortOrder: Number(form.get("sortOrder")) } });
9348
9401
  await renderTimeline();
9349
9402
  }, item ? "大事件分组" : "多线叙事");
@@ -11618,11 +11671,8 @@ $("#ai-scope").addEventListener("change", (event) => {
11618
11671
  return toast("当前对话已经开始,请新建对话后再切换上下文引用", "error");
11619
11672
  }
11620
11673
  event.currentTarget.title = "";
11621
- syncAiIncludeSettingInfoControl();
11622
11674
  setAiContextMeter(null);
11623
11675
  });
11624
- $("#ai-include-setting-info").addEventListener("change", () => setAiContextMeter(null));
11625
- syncAiIncludeSettingInfoControl();
11626
11676
  $("#ai-mention-menu").addEventListener("click", (event) => {
11627
11677
  const button = event.target.closest("[data-ai-reference-id]");
11628
11678
  if (button) selectAiMention(button);
@@ -1,5 +1,6 @@
1
1
  const fieldLabels = Object.freeze({
2
2
  name: "标准名",
3
+ isDead: "死亡标识",
3
4
  code: "编号",
4
5
  aliases: "别名",
5
6
  raceId: "种族",
@@ -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=20260801-ai-roleplay-chat-merge-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260801-ui-font-scale-shelf-v3">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -321,17 +321,13 @@
321
321
  <select id="ai-roleplay-character" class="ai-roleplay-character hidden" aria-label="角色扮演角色卡">
322
322
  <option value="">选择角色卡</option>
323
323
  </select>
324
- <label class="ai-include-setting-info" for="ai-include-setting-info">
325
- <input id="ai-include-setting-info" type="checkbox" checked>
326
- <span>注入设定信息</span>
327
- </label>
328
324
  </div>
329
325
  <div class="field-label prompt-model-field">
330
326
  <select id="ai-model" aria-label="实际使用模型"></select>
331
327
  </div>
332
328
  <div class="prompt-composer">
333
329
  <div id="ai-prompt" class="ai-prompt" contenteditable="true" role="textbox" aria-multiline="true" aria-keyshortcuts="Enter" title="Enter 发送,Shift+Enter 换行" data-placeholder="告诉 AI 你想讨论或修改什么……"></div>
334
- <div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定或章节"></div>
330
+ <div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定、章节或上下文能力"></div>
335
331
  <div class="prompt-composer-actions">
336
332
  <button id="ai-context-meter" class="ai-context-meter is-empty" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-context-popover" aria-live="polite" aria-label="当前上下文用量"><b>—</b></button>
337
333
  <section id="ai-context-popover" class="ai-context-popover hidden" role="dialog" aria-labelledby="ai-context-popover-title" aria-describedby="ai-context-popover-description">
@@ -668,9 +664,9 @@
668
664
  </label>
669
665
  <label>界面字号
670
666
  <select id="appearance-ui-font-size" name="uiFontSize">
671
- <option value="14">14 px(小)</option>
667
+ <option value="14">14 px(标准)</option>
672
668
  <option value="15">15 px</option>
673
- <option value="16">16 px(标准)</option>
669
+ <option value="16">16 px</option>
674
670
  <option value="17">17 px</option>
675
671
  <option value="18">18 px(大)</option>
676
672
  </select>
@@ -708,7 +704,7 @@
708
704
  <strong>字体预览 Typography 0123</strong>
709
705
  <span>中文默认使用黑体,English letters always use monospace.</span>
710
706
  <span>第二段用于预览正文的行距与段落密度。</span>
711
- <span id="font-size-preview">界面 16 px · Agent 对话 14 px</span>
707
+ <span id="font-size-preview">界面 14 px · Agent 对话 14 px</span>
712
708
  </div>
713
709
  </div>
714
710
  <div class="dialog-actions">
@@ -955,6 +951,6 @@
955
951
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
956
952
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
957
953
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
958
- <script type="module" src="/app.js?v=20260801-ai-roleplay-chat-merge-v1"></script>
954
+ <script type="module" src="/app.js?v=20260801-timeline-lifecycle-v1"></script>
959
955
  </body>
960
956
  </html>