@musnows/scriverse 0.5.8 → 0.5.9

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.
@@ -33,16 +33,16 @@ import {
33
33
  taskScopeLabel,
34
34
  timelineStatusLabel,
35
35
  characterStateFieldLabel
36
- } from "/display-labels.js?v=20260726-anthropic-messages-v2";
36
+ } from "/display-labels.js?v=20260728-hybrid-search-v1";
37
37
  import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260727-ai-usage";
38
38
  import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
39
39
  import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
40
- import { eligibleRaceParents, orderRaceFilterOptions, paginateRaceForest, racePathLabel } from "/race-hierarchy.js?v=20260727-race-tree-pagination-v1";
40
+ import { buildRaceForest, eligibleRaceParents, orderRaceFilterOptions, racePathLabel } from "/race-hierarchy.js?v=20260729-race-tree-all-v1";
41
41
  import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
42
42
  import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
43
43
  import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
44
44
  import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
45
- import { resolveGlobalSearchTarget } from "/global-search.js?v=20260726-search-result-details";
45
+ import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260728-hybrid-search-v1";
46
46
  import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
47
47
  import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
48
48
  import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
@@ -210,8 +210,12 @@ function renderAnalysisTaskStatus(item) {
210
210
  const status = normalizedAnalysisTaskStatus(item.status);
211
211
  const statusChanged = taskStatusSnapshots.get(taskId) !== status;
212
212
  taskStatusSnapshots.set(taskId, status);
213
- const label = analysisTaskStatusLabel(item.status);
214
- return `<span class="task-status-badge is-${status}${statusChanged ? " is-state-change" : ""}" aria-label="任务状态:${esc(label)}">
213
+ const waitingToRetry = status === "pending" && Boolean(item.nextAttemptAt);
214
+ const label = waitingToRetry ? "等待重试" : analysisTaskStatusLabel(item.status);
215
+ const retryTitle = waitingToRetry
216
+ ? ` title="第 ${esc(String(item.attemptCount ?? 0))} 次尝试失败,将于 ${esc(formatDateTime(item.nextAttemptAt))} 重试"`
217
+ : "";
218
+ return `<span class="task-status-badge is-${status}${statusChanged ? " is-state-change" : ""}" aria-label="任务状态:${esc(label)}"${retryTitle}>
215
219
  <span class="task-status-indicator" aria-hidden="true"></span>
216
220
  <span>${esc(label)}</span>
217
221
  </span>`;
@@ -362,6 +366,10 @@ let aiReferencesLoadWorkId = null;
362
366
  let aiConversationsLoadPromise = null;
363
367
  let aiConversationsLoadWorkId = null;
364
368
  let workScopedUiGeneration = 0;
369
+ let raceHierarchyLoadPromise = null;
370
+ let raceHierarchyLoadWorkId = null;
371
+ let loadedRaceHierarchyWorkId = null;
372
+ let raceListRequestId = 0;
365
373
  let importHistoryRecords = [];
366
374
  let importHistoryNextPage = null;
367
375
  let importHistoryRequestId = 0;
@@ -862,7 +870,6 @@ let characterListPage = 1;
862
870
  let taskListPage = 1;
863
871
  const moduleListPages = {
864
872
  settings: 1,
865
- races: 1,
866
873
  organizations: 1,
867
874
  timeline: 1,
868
875
  outlinePlans: 1,
@@ -1364,7 +1371,7 @@ const AI_TOOL_DESCRIPTIONS = {
1364
1371
  story_index: "分页读取当前作品的卷章目录和章节概要。",
1365
1372
  read_chapters: "读取指定章节的概要、正文或两者。",
1366
1373
  grep: "查询正文关键字所在的完整段落及章节信息。",
1367
- search_story_entities: "按实体名或关键词子串匹配设定、人物、组织等结构化记录;非语义检索。",
1374
+ search_story_entities: "按实体名、拼音或短关键词混合检索设定、人物、组织等结构化记录;非语义问答。",
1368
1375
  read_character_sections: "读取指定人物 Markdown 档案章节的摘要或原文。"
1369
1376
  };
1370
1377
 
@@ -2290,6 +2297,22 @@ function toast(message, type = "info") {
2290
2297
  }, 3600);
2291
2298
  }
2292
2299
 
2300
+ function persistentToast(message, type = "info") {
2301
+ const region = $("#toast-region");
2302
+ const element = document.createElement("div");
2303
+ element.className = `toast ${type}`;
2304
+ element.setAttribute("role", "status");
2305
+ element.textContent = message;
2306
+ region.append(element);
2307
+ raiseToastRegion();
2308
+ return () => {
2309
+ element.remove();
2310
+ if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
2311
+ region.hidePopover();
2312
+ }
2313
+ };
2314
+ }
2315
+
2293
2316
  function confirmToast(message, { title = "请再次确认", confirmLabel = "确认", cancelLabel = "取消" } = {}) {
2294
2317
  const region = $("#toast-region");
2295
2318
  const element = document.createElement("section");
@@ -2887,7 +2910,6 @@ async function openPlatformUiSettingsDialog() {
2887
2910
  const pageSizes = normalizePageSizes(settings.pageSizes);
2888
2911
  $("#page-size-settings").value = String(pageSizes.settings);
2889
2912
  $("#page-size-characters").value = String(pageSizes.characters);
2890
- $("#page-size-races").value = String(pageSizes.races);
2891
2913
  $("#page-size-organizations").value = String(pageSizes.organizations);
2892
2914
  $("#page-size-timeline").value = String(pageSizes.timeline);
2893
2915
  $("#page-size-outlines").value = String(pageSizes.outlines);
@@ -3010,24 +3032,40 @@ async function openSearchDialog() {
3010
3032
  }
3011
3033
  $("#search-dialog .eyebrow").textContent = `当前作品 · 《${state.work.title}》`;
3012
3034
  $("#search-query").value = "";
3035
+ $("#search-type").value = "";
3013
3036
  $("#search-results").innerHTML = '<p class="search-results-empty">输入关键词后开始检索。</p>';
3014
3037
  $("#search-dialog").showModal();
3015
3038
  queueMicrotask(() => $("#search-query").focus());
3016
3039
  }
3017
3040
 
3018
- function renderSearchResults(results) {
3041
+ function highlightedSearchText(value, query) {
3042
+ return splitGlobalSearchHighlight(value, query)
3043
+ .map((segment) => segment.match ? `<mark>${esc(segment.text)}</mark>` : esc(segment.text))
3044
+ .join("");
3045
+ }
3046
+
3047
+ function renderSearchResults(results, query) {
3019
3048
  if (!results.length) {
3020
3049
  $("#search-results").innerHTML = '<p class="search-results-status">未找到相关内容。</p>';
3021
3050
  return;
3022
3051
  }
3023
- $("#search-results").innerHTML = results.map((item) => `
3024
- <button type="button" class="search-result" data-search-type="${esc(item.type)}" data-search-id="${esc(item.id)}">
3025
- <div class="search-result-meta"><span>${esc(searchResultTypeLabel(item.type))}</span><strong>${esc(item.title)}</strong></div>
3026
- <p>${esc(item.snippet || "无摘要")}</p>
3027
- </button>`).join("");
3028
- $("#search-results").querySelectorAll(".search-result").forEach((button) => {
3052
+ const matchKindLabel = { metadata: "资料命中", exact: "精确命中", phonetic: "拼音命中" };
3053
+ $("#search-results").innerHTML = `<p class="search-results-summary">找到 ${results.length} 条结果,按综合相关度排序。</p>${results.map((item) => {
3054
+ const matchKinds = Array.isArray(item.matchKinds) ? item.matchKinds : [];
3055
+ const lineRange = Number.isInteger(item.startLine)
3056
+ ? `<span class="search-result-chip">${item.startLine === item.endLine ? `第 ${item.startLine} 行` : `第 ${item.startLine}-${item.endLine} 行`}</span>`
3057
+ : "";
3058
+ const subtitle = item.subtitle ? `<small>${esc(item.subtitle)}</small>` : "";
3059
+ return `
3060
+ <button type="button" class="search-result">
3061
+ <div class="search-result-meta"><span>${esc(searchResultTypeLabel(item.type))}</span><strong>${highlightedSearchText(item.title, query)}</strong></div>
3062
+ <p>${highlightedSearchText(item.snippet || "无摘要", query)}</p>
3063
+ <div class="search-result-details">${subtitle}${lineRange}${matchKinds.map((kind) => `<span class="search-result-chip search-result-chip-${esc(kind)}">${esc(matchKindLabel[kind] ?? kind)}</span>`).join("")}</div>
3064
+ </button>`;
3065
+ }).join("")}`;
3066
+ $("#search-results").querySelectorAll(".search-result").forEach((button, index) => {
3029
3067
  button.addEventListener("click", () => {
3030
- openSearchResult({ type: button.dataset.searchType, id: button.dataset.searchId })
3068
+ openSearchResult(results[index])
3031
3069
  .catch((error) => toast(error.message, "error"));
3032
3070
  });
3033
3071
  });
@@ -3041,8 +3079,50 @@ async function runWorkSearch() {
3041
3079
  return;
3042
3080
  }
3043
3081
  $("#search-results").innerHTML = '<p class="search-results-status">正在检索……</p>';
3044
- const results = await api(`/api/works/${encodeURIComponent(state.work.id)}/search?q=${encodeURIComponent(query)}`);
3045
- renderSearchResults(results);
3082
+ const parameters = new URLSearchParams({ q: query });
3083
+ const type = $("#search-type").value;
3084
+ if (type) parameters.set("type", type);
3085
+ const results = await api(`/api/works/${encodeURIComponent(state.work.id)}/search?${parameters}`);
3086
+ renderSearchResults(results, query);
3087
+ }
3088
+
3089
+ function revealChapterSearchLines(startLine, endLine) {
3090
+ const start = Math.max(0, Number(startLine) - 1);
3091
+ const end = Math.max(start, Number(endLine ?? startLine) - 1);
3092
+ const input = $("#chapter-content");
3093
+ const selection = selectedChapterLinePayload(start, end);
3094
+ chapterLineSelection = { start: selection.safeStart, end: selection.safeEnd };
3095
+ input.focus({ preventScroll: true });
3096
+ input.setSelectionRange(selection.startOffset, selection.startOffset + selection.text.length);
3097
+ scheduleChapterLineNumbers();
3098
+ requestAnimationFrame(() => {
3099
+ paintChapterLineSelection(selection.safeStart, selection.safeEnd);
3100
+ const row = $("#chapter-line-numbers-inner").querySelector(`[data-line-index="${selection.safeStart}"]`);
3101
+ if (row) input.scrollTop = Math.max(0, row.offsetTop - input.clientHeight / 3);
3102
+ syncChapterLineNumberScroll();
3103
+ });
3104
+ }
3105
+
3106
+ function openSearchEntityPreview(result, item) {
3107
+ const rowsByType = {
3108
+ "timeline-track": [["时间轴名称", item.name], ["简介", item.description], ["排序", item.sortOrder]],
3109
+ "timeline-event": [["事件名称", item.name], ["时间", item.timeLabel], ["类型", item.eventType], ["地点", item.location], ["说明", item.description], ["状态", item.status]],
3110
+ relationship: [["关系", result.title], ["大类", relationshipCategoryLabel(item.category)], ["子类", item.subtype], ["关键词", Array.isArray(item.keywords) ? item.keywords.join("、") : ""], ["当前状态", item.currentStatus], ["置信度", typeof item.confidence === "number" ? `${Math.round(item.confidence * 100)}%` : ""]],
3111
+ "chapter-outline": [["章节", item.chapterTitle], ["本章目标", item.goal], ["核心冲突", item.conflict], ["关键转折", item.turningPoint], ["补充说明", item.notes], ["规划状态", outlineStatusLabel(item.status)]],
3112
+ foreshadow: [["伏笔名称", item.title], ["内容与作用", item.description], ["重要程度", levelLabel(item.importance)], ["状态", foreshadowStatusLabel(item.status)], ["回收结论", item.resolutionNote]]
3113
+ };
3114
+ const rows = rowsByType[result.type] ?? [];
3115
+ const content = rows
3116
+ .filter(([, value]) => value !== undefined && value !== null && String(value).trim())
3117
+ .map(([label, value]) => `<div><dt>${esc(label)}</dt><dd>${esc(value)}</dd></div>`)
3118
+ .join("");
3119
+ openDialog(
3120
+ result.title || searchResultTypeLabel(result.type),
3121
+ `<dl class="search-result-preview">${content || "<div><dt>详情</dt><dd>暂无补充信息</dd></div>"}</dl>`,
3122
+ async () => undefined,
3123
+ searchResultTypeLabel(result.type),
3124
+ { submitLabel: "关闭", hideCancel: true, wide: true }
3125
+ );
3046
3126
  }
3047
3127
 
3048
3128
  async function openSearchResult(result) {
@@ -3055,6 +3135,10 @@ async function openSearchResult(result) {
3055
3135
  if (inSettings) await returnFromSettings();
3056
3136
  if (target.kind === "chapter") {
3057
3137
  await selectChapter(target.id);
3138
+ if (state.chapter?.id === target.id && target.startLine) {
3139
+ revealChapterSearchLines(target.startLine, target.endLine);
3140
+ toast(target.startLine === target.endLine ? `已定位到第 ${target.startLine} 行` : `已定位到第 ${target.startLine}-${target.endLine} 行`);
3141
+ }
3058
3142
  return;
3059
3143
  }
3060
3144
  await showModule(target.module);
@@ -3064,6 +3148,10 @@ async function openSearchResult(result) {
3064
3148
  if (target.entity === "character") await openCharacterEditor(item, { readOnly: true });
3065
3149
  if (target.entity === "race") await openRaceDialog(item, { readOnly: true });
3066
3150
  if (target.entity === "organization") await openOrganizationDialog(item, { readOnly: true });
3151
+ if (target.entity === "review") openReviewDetailDialog(item);
3152
+ if (["timeline-track", "timeline-event", "relationship", "chapter-outline", "foreshadow"].includes(target.entity)) {
3153
+ openSearchEntityPreview(result, item);
3154
+ }
3067
3155
  }
3068
3156
 
3069
3157
  async function showSettingsHub() {
@@ -3192,9 +3280,14 @@ function resetWorkScopedUiCaches() {
3192
3280
  aiReferencesLoadWorkId = null;
3193
3281
  aiConversationsLoadPromise = null;
3194
3282
  aiConversationsLoadWorkId = null;
3283
+ raceHierarchyLoadPromise = null;
3284
+ raceHierarchyLoadWorkId = null;
3285
+ loadedRaceHierarchyWorkId = null;
3286
+ raceListRequestId += 1;
3195
3287
  state.models = [];
3196
3288
  state.characters = [];
3197
3289
  state.settings = [];
3290
+ state.races = [];
3198
3291
  characterListPage = 1;
3199
3292
  Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
3200
3293
  relationshipFilters.fromCharacterIds = [];
@@ -4125,7 +4218,7 @@ async function renderCharacters(page = characterListPage) {
4125
4218
  const pageSize = pageSizeFor("characters");
4126
4219
  const [characterSource, races, organizations] = await Promise.all([
4127
4220
  hasCharacterFilters ? apiAllPages(`/api/works/${state.work.id}/characters`) : apiPage(`/api/works/${state.work.id}/characters`, page, pageSize),
4128
- canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
4221
+ canReadModule("races") ? api(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
4129
4222
  canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
4130
4223
  ]);
4131
4224
  const characterPage = hasCharacterFilters
@@ -4228,15 +4321,16 @@ async function renderCharacters(page = characterListPage) {
4228
4321
  $("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(pageCharacters.find((item) => item.id === button.dataset.editCharacter))));
4229
4322
  }
4230
4323
 
4231
- async function renderRaces(page = moduleListPages.races) {
4232
- state.races = await apiAllPages(`/api/works/${state.work.id}/races`);
4233
- mountModuleCount(state.races.length);
4324
+ function renderRaceCollection(total, descendantsLoading = false) {
4325
+ mountModuleCount(total);
4234
4326
  const layout = readModuleLayout();
4235
- const pageResult = layout === "rows"
4236
- ? paginateModuleItems(state.races, page, "races")
4237
- : paginateRaceForest(state.races, page, pageSizeFor("races"));
4238
- moduleListPages.races = pageResult.page;
4327
+ const raceItems = layout === "rows"
4328
+ ? [...state.races].sort((left, right) => String(left.name).localeCompare(String(right.name), "zh-CN"))
4329
+ : buildRaceForest(state.races);
4239
4330
  const canEditRaces = canEditModule("races");
4331
+ const directChildCount = (item) => Number.isInteger(Number(item.childCount))
4332
+ ? Number(item.childCount)
4333
+ : Array.isArray(item.children) ? item.children.length : 0;
4240
4334
  const raceActions = (item) => canEditRaces
4241
4335
  ? recordCardEditButton("edit-race", item.id, `种族“${item.name}”`)
4242
4336
  : recordHistoryButton("race", item.id, item.name);
@@ -4244,7 +4338,7 @@ async function renderRaces(page = moduleListPages.races) {
4244
4338
  ? raceActions(item)
4245
4339
  : `<div class="card-actions">${raceActions(item)}</div>`;
4246
4340
  const renderRaceNode = (item) => `<details class="race-tree-node"${state.collapsedRaceIds.has(item.id) ? "" : " open"} data-race-node="${esc(item.id)}">
4247
- <summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
4341
+ <summary><span>${esc(item.name)}</span><small>${directChildCount(item)} 个直接子种族</small></summary>
4248
4342
  <div class="race-tree-branch">
4249
4343
  <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>
4250
4344
  <div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
@@ -4256,9 +4350,9 @@ async function renderRaces(page = moduleListPages.races) {
4256
4350
  ${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
4257
4351
  </div>
4258
4352
  </details>`;
4259
- const raceRows = () => `<div class="module-row-list">${pageResult.items.map((item) => {
4353
+ const raceRows = () => `<div class="module-row-list">${raceItems.map((item) => {
4260
4354
  const preview = moduleRowPreview(item.description || "尚未填写种族简介");
4261
- const meta = `${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
4355
+ const meta = `${directChildCount(item)} 个直接子种族 · ${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
4262
4356
  return `
4263
4357
  <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)}">
4264
4358
  <small>${esc(meta)}</small>
@@ -4270,15 +4364,65 @@ async function renderRaces(page = moduleListPages.races) {
4270
4364
  if (state.races.length) mountModuleLayoutToggle(layout, "种族列表样式");
4271
4365
  if (state.races.length && layout !== "rows") mountRaceTreeExpandToggle();
4272
4366
  $("#module-content").innerHTML = state.races.length
4273
- ? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${pageResult.items.map(renderRaceNode).join("")}</section>`}${renderModulePagination(pageResult, "races", "种族列表")}`
4367
+ ? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级" aria-busy="${descendantsLoading}">${raceItems.map(renderRaceNode).join("")}</section>`}`
4274
4368
  : emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
4275
- bindModuleLayoutToggle(() => renderRaces(pageResult.page));
4276
- bindModulePagination("races", renderRaces);
4369
+ bindModuleLayoutToggle(() => renderRaceCollection(total, descendantsLoading));
4277
4370
  bindRaceTreeExpandToggle();
4278
4371
  bindRaceTreeNodeToggles();
4279
4372
  const openRace = async (id, readOnly) => openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly });
4280
4373
  $("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => { void openRace(button.dataset.editRace, false); }));
4281
- bindEntityHistoryButtons(async () => { await renderRaces(pageResult.page); await loadAiReferences(); });
4374
+ bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
4375
+ }
4376
+
4377
+ async function ensureCompleteRaceList() {
4378
+ const workId = state.work?.id;
4379
+ if (!workId) return false;
4380
+ const generation = workScopedUiGeneration;
4381
+ if (loadedRaceHierarchyWorkId === workId) return true;
4382
+ if (raceHierarchyLoadPromise && raceHierarchyLoadWorkId === workId) {
4383
+ await raceHierarchyLoadPromise.catch(() => {});
4384
+ }
4385
+ if (state.work?.id !== workId || generation !== workScopedUiGeneration) return false;
4386
+ if (loadedRaceHierarchyWorkId === workId) return true;
4387
+ const races = await api(`/api/works/${workId}/races`);
4388
+ if (state.work?.id !== workId || generation !== workScopedUiGeneration) return false;
4389
+ state.races = races;
4390
+ loadedRaceHierarchyWorkId = workId;
4391
+ return true;
4392
+ }
4393
+
4394
+ async function renderRaces() {
4395
+ const workId = state.work.id;
4396
+ const generation = workScopedUiGeneration;
4397
+ const requestId = ++raceListRequestId;
4398
+ const roots = await api(`/api/works/${workId}/races?scope=roots`);
4399
+ if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
4400
+ state.races = roots.items;
4401
+ loadedRaceHierarchyWorkId = roots.items.length === roots.total ? workId : null;
4402
+ renderRaceCollection(roots.total, loadedRaceHierarchyWorkId !== workId);
4403
+ if (loadedRaceHierarchyWorkId === workId) return;
4404
+
4405
+ const dismissLoadingToast = persistentToast("正在加载子种族……");
4406
+ const loadPromise = api(`/api/works/${workId}/races?scope=descendants`).then((descendants) => {
4407
+ if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
4408
+ state.races = [...roots.items, ...descendants];
4409
+ loadedRaceHierarchyWorkId = workId;
4410
+ if (state.module === "races") renderRaceCollection(roots.total);
4411
+ });
4412
+ raceHierarchyLoadPromise = loadPromise;
4413
+ raceHierarchyLoadWorkId = workId;
4414
+ void loadPromise.catch((error) => {
4415
+ if (state.work?.id === workId && generation === workScopedUiGeneration && requestId === raceListRequestId) {
4416
+ renderRaceCollection(roots.total);
4417
+ toast(`父种族已显示,但子种族载入失败:${error.message}`, "error");
4418
+ }
4419
+ }).finally(() => {
4420
+ if (raceHierarchyLoadPromise === loadPromise) {
4421
+ raceHierarchyLoadPromise = null;
4422
+ raceHierarchyLoadWorkId = null;
4423
+ }
4424
+ dismissLoadingToast();
4425
+ });
4282
4426
  }
4283
4427
 
4284
4428
  async function renderOrganizations(page = moduleListPages.organizations) {
@@ -4619,7 +4763,7 @@ async function renderTasks(page = taskListPage) {
4619
4763
  apiPage(`/api/works/${state.work.id}/tasks`, page, pageSize),
4620
4764
  canReadModule("ai-settings")
4621
4765
  ? api(`/api/works/${state.work.id}/ai-settings`)
4622
- : Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
4766
+ : Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunDailyTaskLimit: 0, autoRunFailureThreshold: 3, autoRunPaused: false })
4623
4767
  ]);
4624
4768
  if (!taskPage.items.length && page > 1) return renderTasks(page - 1);
4625
4769
  taskListPage = taskPage.page;
@@ -4631,6 +4775,12 @@ async function renderTasks(page = taskListPage) {
4631
4775
  const runningCount = Number(taskPage.stats?.runningCount ?? 0);
4632
4776
  const activeTaskCount = pendingCount + runningCount;
4633
4777
  const runningProgress = runningCount ? analysisTaskProgressValue(taskPage.stats?.runningProgress) : 0;
4778
+ const autoRunPaused = Boolean(settings.autoRunEnabled && settings.autoRunPaused);
4779
+ const autoRunActive = Boolean(settings.autoRunEnabled && !autoRunPaused);
4780
+ const queueProgressLabel = runningCount
4781
+ ? "运行中任务平均进度"
4782
+ : autoRunPaused ? "自动执行已暂停" : autoRunActive ? "等待任务开始" : "自动执行已关闭";
4783
+ const queueProgressClass = runningCount ? "is-running" : autoRunPaused ? "is-paused" : "is-waiting";
4634
4784
  const visibleTaskIds = new Set(tasks.map((item) => String(item.id)));
4635
4785
  for (const taskId of taskStatusSnapshots.keys()) {
4636
4786
  if (!visibleTaskIds.has(taskId)) taskStatusSnapshots.delete(taskId);
@@ -4647,27 +4797,30 @@ async function renderTasks(page = taskListPage) {
4647
4797
  <div class="task-auto-run-copy">
4648
4798
  <strong id="task-auto-run-title">自动执行待分析任务</strong>
4649
4799
  <small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
4650
- <small>每轮最多启动「每轮任务上限」个,同时运行数量不超过「同时运行上限」;剩余任务需点击“开始下一轮”。</small>
4800
+ <small>开启后会持续执行直到队列清空;临时错误自动退避重试,连续失败达到阈值后暂停。</small>
4651
4801
  </div>
4652
4802
  <div class="task-auto-run-controls">
4653
- <label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""}><span>自动执行待分析任务</span></label>
4803
+ <label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""}><span>持续自动执行</span></label>
4654
4804
  <label>同时运行上限<input id="task-auto-run-concurrency" type="number" min="1" max="8" value="${esc(String(settings.autoRunConcurrency ?? 2))}"></label>
4655
- <label>每轮任务上限<input id="task-auto-run-batch-limit" type="number" min="1" max="200" value="${esc(String(settings.autoRunBatchLimit ?? 20))}"></label>
4805
+ <label>每日任务上限<input id="task-auto-run-daily-limit" type="number" min="0" max="10000" value="${esc(String(settings.autoRunDailyTaskLimit ?? 0))}" aria-describedby="task-auto-run-daily-help"></label>
4806
+ <label>连续失败暂停阈值<input id="task-auto-run-failure-threshold" type="number" min="1" max="10" value="${esc(String(settings.autoRunFailureThreshold ?? 3))}"></label>
4656
4807
  <button id="task-auto-run-save" class="primary-button" type="button">保存并生效</button>
4657
- <button id="task-auto-run-continue" class="ghost-button" type="button" ${settings.autoRunEnabled ? "" : "disabled"}>开始下一轮</button>
4808
+ <button id="task-auto-run-toggle" class="ghost-button" type="button" ${settings.autoRunEnabled ? "" : "disabled"}>${autoRunPaused ? "恢复自动执行" : "暂停自动执行"}</button>
4658
4809
  </div>
4659
- <p class="task-auto-run-meta">待执行队列 ${pendingCount} · 正在运行 ${runningCount} 个</p>
4810
+ <p id="task-auto-run-daily-help" class="task-auto-run-help">每日任务上限填 0 表示不限制;达到上限后会在下一个 UTC 自然日自动恢复。</p>
4811
+ <p class="task-auto-run-meta">待执行队列 ${pendingCount} 个 · 正在运行 ${runningCount} 个 · ${autoRunPaused ? "已暂停" : autoRunActive ? "持续执行中" : "未开启"}</p>
4812
+ ${autoRunPaused ? `<p class="task-auto-run-alert" role="status"><strong>自动执行已暂停</strong><span>${esc(settings.autoRunPauseReason || "需要人工确认后恢复")}</span>${settings.autoRunResumeAt ? `<small>预计 ${esc(formatDateTime(settings.autoRunResumeAt))} 自动恢复</small>` : ""}</p>` : ""}
4660
4813
  <div class="task-auto-run-progress ${activeTaskCount ? "" : "hidden"}" aria-live="polite">
4661
- <div class="task-auto-run-progress-ring ${runningCount ? "is-running" : "is-waiting"}" role="progressbar" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${runningProgress}">
4814
+ <div class="task-auto-run-progress-ring ${queueProgressClass}" role="progressbar" aria-label="${queueProgressLabel}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${runningProgress}">
4662
4815
  <svg viewBox="0 0 120 120" aria-hidden="true" focusable="false">
4663
4816
  <circle class="task-auto-run-progress-ring-track" cx="60" cy="60" r="52"></circle>
4664
4817
  <circle class="task-auto-run-progress-ring-value" cx="60" cy="60" r="52" pathLength="100" stroke-dasharray="${runningProgress} 100"></circle>
4665
4818
  </svg>
4666
- <div class="task-auto-run-progress-ring-label"><strong>${runningProgress}%</strong><span>${runningCount ? "运行中平均进度" : "等待任务开始"}</span></div>
4819
+ <div class="task-auto-run-progress-ring-label"><strong>${runningProgress}%</strong><span>${queueProgressLabel}</span></div>
4667
4820
  </div>
4668
4821
  <div class="task-auto-run-progress-bar-layout">
4669
- <div class="task-auto-run-progress-label"><span>${runningCount ? "运行中任务平均进度" : "等待任务开始"}</span><strong>${runningProgress}%</strong></div>
4670
- <progress class="task-auto-run-progress-bar ${runningCount ? "is-running" : "is-waiting"}" max="100" value="${runningProgress}" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}">${runningProgress}%</progress>
4822
+ <div class="task-auto-run-progress-label"><span>${queueProgressLabel}</span><strong>${runningProgress}%</strong></div>
4823
+ <progress class="task-auto-run-progress-bar ${queueProgressClass}" max="100" value="${runningProgress}" aria-label="${queueProgressLabel}">${runningProgress}%</progress>
4671
4824
  </div>
4672
4825
  </div>
4673
4826
  </section>
@@ -4680,7 +4833,7 @@ async function renderTasks(page = taskListPage) {
4680
4833
  <td class="task-progress-cell">${renderAnalysisTaskProgress(item)}</td>
4681
4834
  <td class="task-row-actions">
4682
4835
  <button class="ghost-button" type="button" data-task-detail="${esc(item.id)}">详情</button>
4683
- ${item.status === "pending" ? `<button class="ghost-button" type="button" data-run-task="${esc(item.id)}">运行</button>` : ""}
4836
+ ${item.status === "pending" && !item.nextAttemptAt ? `<button class="ghost-button" type="button" data-run-task="${esc(item.id)}">运行</button>` : ""}
4684
4837
  ${item.status === "pending" || item.status === "running" ? `<button class="ghost-button" type="button" data-cancel-task="${esc(item.id)}">取消</button>` : ""}
4685
4838
  </td>
4686
4839
  </tr>`).join("")}</tbody></table>${pagination}` : emptyModule("还没有 AI 分析记录", "点击“开始 AI 分析”,可分析指定章节或整部作品。")}`;
@@ -4700,26 +4853,34 @@ async function renderTasks(page = taskListPage) {
4700
4853
  body: {
4701
4854
  autoRunEnabled: $("#task-auto-run-enabled").checked,
4702
4855
  autoRunConcurrency: Number($("#task-auto-run-concurrency").value),
4703
- autoRunBatchLimit: Number($("#task-auto-run-batch-limit").value)
4856
+ autoRunDailyTaskLimit: Number($("#task-auto-run-daily-limit").value),
4857
+ autoRunFailureThreshold: Number($("#task-auto-run-failure-threshold").value)
4704
4858
  }
4705
4859
  });
4706
4860
  toast(updated.autoRunEnabled
4707
- ? `自动执行已开启:同时最多 ${updated.autoRunConcurrency} 个,每轮最多 ${updated.autoRunBatchLimit} 个`
4861
+ ? `持续自动执行已开启:同时最多 ${updated.autoRunConcurrency} 个`
4708
4862
  : "自动执行已关闭");
4709
4863
  await renderTasks();
4864
+ window.setTimeout(() => $("#task-auto-run-save")?.focus(), 0);
4710
4865
  } catch (error) {
4711
4866
  toast(error.message, "error");
4712
4867
  button.disabled = false;
4713
4868
  }
4714
4869
  });
4715
- $("#task-auto-run-continue")?.addEventListener("click", async () => {
4716
- const button = $("#task-auto-run-continue");
4870
+ $("#task-auto-run-toggle")?.addEventListener("click", async () => {
4871
+ const button = $("#task-auto-run-toggle");
4717
4872
  button.disabled = true;
4718
4873
  try {
4719
- const result = await api(`/api/works/${state.work.id}/tasks/auto-run`, { method: "POST", body: {} });
4720
- toast(`已开始下一轮,队列中还有 ${result.pendingCount} 个待执行任务`);
4874
+ if (autoRunPaused) {
4875
+ await api(`/api/works/${state.work.id}/tasks/auto-run`, { method: "POST", body: {} });
4876
+ toast("自动执行已恢复");
4877
+ } else {
4878
+ await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { autoRunEnabled: false } });
4879
+ toast("已暂停启动新的分析任务,正在运行的任务会继续完成");
4880
+ }
4721
4881
  await refreshBackgroundTaskCenter({ announce: false });
4722
4882
  await renderTasks();
4883
+ window.setTimeout(() => $(autoRunPaused ? "#task-auto-run-toggle" : "#task-auto-run-enabled")?.focus(), 0);
4723
4884
  } catch (error) {
4724
4885
  toast(error.message, "error");
4725
4886
  button.disabled = false;
@@ -4776,7 +4937,7 @@ async function renderTasks(page = taskListPage) {
4776
4937
  button.disabled = false;
4777
4938
  }
4778
4939
  }));
4779
- scheduleTaskProgressRefresh(state.work.id, runningCount);
4940
+ scheduleTaskProgressRefresh(state.work.id, runningCount > 0 || (pendingCount > 0 && autoRunActive) ? 1 : 0);
4780
4941
  }
4781
4942
 
4782
4943
  async function rerunAnalysisTask(taskId, button, { closeDetail = false } = {}) {
@@ -5703,7 +5864,7 @@ async function renderBookAiSettings() {
5703
5864
  host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
5704
5865
  title: "本书 Token 用量",
5705
5866
  description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
5706
- })}</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>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。</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)}`;
5867
+ })}</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>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。</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)}`;
5707
5868
  scrollUsageCalendarsToLatest(host);
5708
5869
  host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
5709
5870
  "beforebegin",
@@ -7307,7 +7468,7 @@ async function showCharacterHistory() {
7307
7468
  async function openCharacterEditor(item = null, { readOnly = false } = {}) {
7308
7469
  entityEditorReadOnly = readOnly;
7309
7470
  [state.races, state.organizations, state.characters] = await Promise.all([
7310
- canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
7471
+ canReadModule("races") ? api(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
7311
7472
  canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([]),
7312
7473
  canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
7313
7474
  ]);
@@ -7440,6 +7601,7 @@ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
7440
7601
  async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
7441
7602
  entityEditorReadOnly = readOnly;
7442
7603
  await discardPendingMarkdownAttachments();
7604
+ if (kind === "race" && !(await ensureCompleteRaceList())) return;
7443
7605
  state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
7444
7606
  const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
7445
7607
  const isRace = kind === "race";
@@ -9209,7 +9371,6 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
9209
9371
  pageSizes: {
9210
9372
  settings: Number($("#page-size-settings").value),
9211
9373
  characters: Number($("#page-size-characters").value),
9212
- races: Number($("#page-size-races").value),
9213
9374
  organizations: Number($("#page-size-organizations").value),
9214
9375
  timeline: Number($("#page-size-timeline").value),
9215
9376
  outlines: Number($("#page-size-outlines").value),
@@ -81,7 +81,19 @@ export function occurrenceRoleLabel(value) {
81
81
  }
82
82
 
83
83
  export function searchResultTypeLabel(value) {
84
- return enumLabel({ chapter: "章节", setting: "设定", character: "角色", race: "种族", organization: "组织" }, value, "其他资料");
84
+ return enumLabel({
85
+ chapter: "章节",
86
+ setting: "设定",
87
+ character: "角色",
88
+ race: "种族",
89
+ organization: "组织",
90
+ "timeline-track": "独立时间轴",
91
+ "timeline-event": "时间线事件",
92
+ relationship: "人物关系",
93
+ "chapter-outline": "章节大纲",
94
+ foreshadow: "伏笔",
95
+ review: "审核项"
96
+ }, value, "其他资料");
85
97
  }
86
98
 
87
99
  export function characterStateFieldLabel(value) {
@@ -1,12 +1,13 @@
1
1
  export type GlobalSearchTarget =
2
- | { kind: "chapter"; type: "chapter"; id: string; module: "editor" }
2
+ | { kind: "chapter"; type: "chapter"; id: string; module: "editor"; startLine?: number; endLine?: number }
3
3
  | {
4
4
  kind: "entity";
5
- type: "setting" | "character" | "race" | "organization";
5
+ type: "setting" | "character" | "race" | "organization" | "timeline-track" | "timeline-event" | "relationship" | "chapter-outline" | "foreshadow" | "review";
6
6
  id: string;
7
- module: "settings" | "characters" | "races" | "organizations";
8
- entity: "setting" | "character" | "race" | "organization";
7
+ module: "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews";
8
+ entity: "setting" | "character" | "race" | "organization" | "timeline-track" | "timeline-event" | "relationship" | "chapter-outline" | "foreshadow" | "review";
9
9
  apiPath: string;
10
10
  };
11
11
 
12
- export function resolveGlobalSearchTarget(result?: { type?: unknown; id?: unknown }): GlobalSearchTarget | null;
12
+ export function splitGlobalSearchHighlight(value: unknown, query: unknown): Array<{ text: string; match: boolean }>;
13
+ export function resolveGlobalSearchTarget(result?: { type?: unknown; id?: unknown; startLine?: unknown; endLine?: unknown }): GlobalSearchTarget | null;