@musnows/scriverse 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +355 -70
- package/dist/ai.js.map +1 -1
- package/dist/app.js +48 -37
- package/dist/app.js.map +1 -1
- package/dist/cli-core.js +161 -20
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +72 -0
- package/dist/database.js.map +1 -1
- package/dist/hybrid-search.js +106 -0
- package/dist/hybrid-search.js.map +1 -0
- package/dist/public/app.js +421 -64
- package/dist/public/display-labels.js +13 -1
- package/dist/public/global-search.d.ts +6 -5
- package/dist/public/global-search.js +44 -3
- package/dist/public/index.html +20 -7
- package/dist/public/race-hierarchy.js +0 -35
- package/dist/public/styles.css +36 -10
- package/dist/public/vditor-line-number-layout.d.ts +14 -0
- package/dist/public/vditor-line-number-layout.js +51 -0
- package/dist/relationship-search.js +21 -2
- package/dist/relationship-search.js.map +1 -1
- package/dist/store.js +137 -5
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +27 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { renderMarkdown } from "/markdown.js?v=20260725-ordered-list";
|
|
|
4
4
|
import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
|
|
5
5
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
6
|
import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
7
|
+
import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
|
|
7
8
|
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
|
|
8
9
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
9
10
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
@@ -33,16 +34,16 @@ import {
|
|
|
33
34
|
taskScopeLabel,
|
|
34
35
|
timelineStatusLabel,
|
|
35
36
|
characterStateFieldLabel
|
|
36
|
-
} from "/display-labels.js?v=
|
|
37
|
+
} from "/display-labels.js?v=20260728-hybrid-search-v1";
|
|
37
38
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260727-ai-usage";
|
|
38
39
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
39
40
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
40
|
-
import { eligibleRaceParents, orderRaceFilterOptions,
|
|
41
|
+
import { buildRaceForest, eligibleRaceParents, orderRaceFilterOptions, racePathLabel } from "/race-hierarchy.js?v=20260729-race-tree-all-v1";
|
|
41
42
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
42
43
|
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
|
|
43
44
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
44
45
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
45
|
-
import { resolveGlobalSearchTarget } from "/global-search.js?v=
|
|
46
|
+
import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260728-hybrid-search-v1";
|
|
46
47
|
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
47
48
|
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
48
49
|
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
|
|
@@ -141,6 +142,8 @@ let chapterMovePending = false;
|
|
|
141
142
|
|
|
142
143
|
let timelineMultiSelectEnabled = false;
|
|
143
144
|
let taskProgressRefreshTimer = null;
|
|
145
|
+
let taskAutoRunEditing = false;
|
|
146
|
+
let taskAutoRunEditingWorkId = null;
|
|
144
147
|
let relationshipSearchIndexRefreshTimer = null;
|
|
145
148
|
let backgroundTaskCenterTimer = null;
|
|
146
149
|
let backgroundTaskCenterRequest = 0;
|
|
@@ -210,8 +213,12 @@ function renderAnalysisTaskStatus(item) {
|
|
|
210
213
|
const status = normalizedAnalysisTaskStatus(item.status);
|
|
211
214
|
const statusChanged = taskStatusSnapshots.get(taskId) !== status;
|
|
212
215
|
taskStatusSnapshots.set(taskId, status);
|
|
213
|
-
const
|
|
214
|
-
|
|
216
|
+
const waitingToRetry = status === "pending" && Boolean(item.nextAttemptAt);
|
|
217
|
+
const label = waitingToRetry ? "等待重试" : analysisTaskStatusLabel(item.status);
|
|
218
|
+
const retryTitle = waitingToRetry
|
|
219
|
+
? ` title="第 ${esc(String(item.attemptCount ?? 0))} 次尝试失败,将于 ${esc(formatDateTime(item.nextAttemptAt))} 重试"`
|
|
220
|
+
: "";
|
|
221
|
+
return `<span class="task-status-badge is-${status}${statusChanged ? " is-state-change" : ""}" aria-label="任务状态:${esc(label)}"${retryTitle}>
|
|
215
222
|
<span class="task-status-indicator" aria-hidden="true"></span>
|
|
216
223
|
<span>${esc(label)}</span>
|
|
217
224
|
</span>`;
|
|
@@ -362,6 +369,10 @@ let aiReferencesLoadWorkId = null;
|
|
|
362
369
|
let aiConversationsLoadPromise = null;
|
|
363
370
|
let aiConversationsLoadWorkId = null;
|
|
364
371
|
let workScopedUiGeneration = 0;
|
|
372
|
+
let raceHierarchyLoadPromise = null;
|
|
373
|
+
let raceHierarchyLoadWorkId = null;
|
|
374
|
+
let loadedRaceHierarchyWorkId = null;
|
|
375
|
+
let raceListRequestId = 0;
|
|
365
376
|
let importHistoryRecords = [];
|
|
366
377
|
let importHistoryNextPage = null;
|
|
367
378
|
let importHistoryRequestId = 0;
|
|
@@ -862,7 +873,6 @@ let characterListPage = 1;
|
|
|
862
873
|
let taskListPage = 1;
|
|
863
874
|
const moduleListPages = {
|
|
864
875
|
settings: 1,
|
|
865
|
-
races: 1,
|
|
866
876
|
organizations: 1,
|
|
867
877
|
timeline: 1,
|
|
868
878
|
outlinePlans: 1,
|
|
@@ -1364,7 +1374,7 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
1364
1374
|
story_index: "分页读取当前作品的卷章目录和章节概要。",
|
|
1365
1375
|
read_chapters: "读取指定章节的概要、正文或两者。",
|
|
1366
1376
|
grep: "查询正文关键字所在的完整段落及章节信息。",
|
|
1367
|
-
search_story_entities: "
|
|
1377
|
+
search_story_entities: "按实体名、拼音或短关键词混合检索设定、人物、组织等结构化记录;非语义问答。",
|
|
1368
1378
|
read_character_sections: "读取指定人物 Markdown 档案章节的摘要或原文。"
|
|
1369
1379
|
};
|
|
1370
1380
|
|
|
@@ -2290,6 +2300,22 @@ function toast(message, type = "info") {
|
|
|
2290
2300
|
}, 3600);
|
|
2291
2301
|
}
|
|
2292
2302
|
|
|
2303
|
+
function persistentToast(message, type = "info") {
|
|
2304
|
+
const region = $("#toast-region");
|
|
2305
|
+
const element = document.createElement("div");
|
|
2306
|
+
element.className = `toast ${type}`;
|
|
2307
|
+
element.setAttribute("role", "status");
|
|
2308
|
+
element.textContent = message;
|
|
2309
|
+
region.append(element);
|
|
2310
|
+
raiseToastRegion();
|
|
2311
|
+
return () => {
|
|
2312
|
+
element.remove();
|
|
2313
|
+
if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
|
|
2314
|
+
region.hidePopover();
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2293
2319
|
function confirmToast(message, { title = "请再次确认", confirmLabel = "确认", cancelLabel = "取消" } = {}) {
|
|
2294
2320
|
const region = $("#toast-region");
|
|
2295
2321
|
const element = document.createElement("section");
|
|
@@ -2887,7 +2913,6 @@ async function openPlatformUiSettingsDialog() {
|
|
|
2887
2913
|
const pageSizes = normalizePageSizes(settings.pageSizes);
|
|
2888
2914
|
$("#page-size-settings").value = String(pageSizes.settings);
|
|
2889
2915
|
$("#page-size-characters").value = String(pageSizes.characters);
|
|
2890
|
-
$("#page-size-races").value = String(pageSizes.races);
|
|
2891
2916
|
$("#page-size-organizations").value = String(pageSizes.organizations);
|
|
2892
2917
|
$("#page-size-timeline").value = String(pageSizes.timeline);
|
|
2893
2918
|
$("#page-size-outlines").value = String(pageSizes.outlines);
|
|
@@ -3010,24 +3035,40 @@ async function openSearchDialog() {
|
|
|
3010
3035
|
}
|
|
3011
3036
|
$("#search-dialog .eyebrow").textContent = `当前作品 · 《${state.work.title}》`;
|
|
3012
3037
|
$("#search-query").value = "";
|
|
3038
|
+
$("#search-type").value = "";
|
|
3013
3039
|
$("#search-results").innerHTML = '<p class="search-results-empty">输入关键词后开始检索。</p>';
|
|
3014
3040
|
$("#search-dialog").showModal();
|
|
3015
3041
|
queueMicrotask(() => $("#search-query").focus());
|
|
3016
3042
|
}
|
|
3017
3043
|
|
|
3018
|
-
function
|
|
3044
|
+
function highlightedSearchText(value, query) {
|
|
3045
|
+
return splitGlobalSearchHighlight(value, query)
|
|
3046
|
+
.map((segment) => segment.match ? `<mark>${esc(segment.text)}</mark>` : esc(segment.text))
|
|
3047
|
+
.join("");
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
function renderSearchResults(results, query) {
|
|
3019
3051
|
if (!results.length) {
|
|
3020
3052
|
$("#search-results").innerHTML = '<p class="search-results-status">未找到相关内容。</p>';
|
|
3021
3053
|
return;
|
|
3022
3054
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3055
|
+
const matchKindLabel = { metadata: "资料命中", exact: "精确命中", phonetic: "拼音命中" };
|
|
3056
|
+
$("#search-results").innerHTML = `<p class="search-results-summary">找到 ${results.length} 条结果,按综合相关度排序。</p>${results.map((item) => {
|
|
3057
|
+
const matchKinds = Array.isArray(item.matchKinds) ? item.matchKinds : [];
|
|
3058
|
+
const lineRange = Number.isInteger(item.startLine)
|
|
3059
|
+
? `<span class="search-result-chip">${item.startLine === item.endLine ? `第 ${item.startLine} 行` : `第 ${item.startLine}-${item.endLine} 行`}</span>`
|
|
3060
|
+
: "";
|
|
3061
|
+
const subtitle = item.subtitle ? `<small>${esc(item.subtitle)}</small>` : "";
|
|
3062
|
+
return `
|
|
3063
|
+
<button type="button" class="search-result">
|
|
3064
|
+
<div class="search-result-meta"><span>${esc(searchResultTypeLabel(item.type))}</span><strong>${highlightedSearchText(item.title, query)}</strong></div>
|
|
3065
|
+
<p>${highlightedSearchText(item.snippet || "无摘要", query)}</p>
|
|
3066
|
+
<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>
|
|
3067
|
+
</button>`;
|
|
3068
|
+
}).join("")}`;
|
|
3069
|
+
$("#search-results").querySelectorAll(".search-result").forEach((button, index) => {
|
|
3029
3070
|
button.addEventListener("click", () => {
|
|
3030
|
-
openSearchResult(
|
|
3071
|
+
openSearchResult(results[index])
|
|
3031
3072
|
.catch((error) => toast(error.message, "error"));
|
|
3032
3073
|
});
|
|
3033
3074
|
});
|
|
@@ -3041,8 +3082,50 @@ async function runWorkSearch() {
|
|
|
3041
3082
|
return;
|
|
3042
3083
|
}
|
|
3043
3084
|
$("#search-results").innerHTML = '<p class="search-results-status">正在检索……</p>';
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3085
|
+
const parameters = new URLSearchParams({ q: query });
|
|
3086
|
+
const type = $("#search-type").value;
|
|
3087
|
+
if (type) parameters.set("type", type);
|
|
3088
|
+
const results = await api(`/api/works/${encodeURIComponent(state.work.id)}/search?${parameters}`);
|
|
3089
|
+
renderSearchResults(results, query);
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
function revealChapterSearchLines(startLine, endLine) {
|
|
3093
|
+
const start = Math.max(0, Number(startLine) - 1);
|
|
3094
|
+
const end = Math.max(start, Number(endLine ?? startLine) - 1);
|
|
3095
|
+
const input = $("#chapter-content");
|
|
3096
|
+
const selection = selectedChapterLinePayload(start, end);
|
|
3097
|
+
chapterLineSelection = { start: selection.safeStart, end: selection.safeEnd };
|
|
3098
|
+
input.focus({ preventScroll: true });
|
|
3099
|
+
input.setSelectionRange(selection.startOffset, selection.startOffset + selection.text.length);
|
|
3100
|
+
scheduleChapterLineNumbers();
|
|
3101
|
+
requestAnimationFrame(() => {
|
|
3102
|
+
paintChapterLineSelection(selection.safeStart, selection.safeEnd);
|
|
3103
|
+
const row = $("#chapter-line-numbers-inner").querySelector(`[data-line-index="${selection.safeStart}"]`);
|
|
3104
|
+
if (row) input.scrollTop = Math.max(0, row.offsetTop - input.clientHeight / 3);
|
|
3105
|
+
syncChapterLineNumberScroll();
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
function openSearchEntityPreview(result, item) {
|
|
3110
|
+
const rowsByType = {
|
|
3111
|
+
"timeline-track": [["时间轴名称", item.name], ["简介", item.description], ["排序", item.sortOrder]],
|
|
3112
|
+
"timeline-event": [["事件名称", item.name], ["时间", item.timeLabel], ["类型", item.eventType], ["地点", item.location], ["说明", item.description], ["状态", item.status]],
|
|
3113
|
+
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)}%` : ""]],
|
|
3114
|
+
"chapter-outline": [["章节", item.chapterTitle], ["本章目标", item.goal], ["核心冲突", item.conflict], ["关键转折", item.turningPoint], ["补充说明", item.notes], ["规划状态", outlineStatusLabel(item.status)]],
|
|
3115
|
+
foreshadow: [["伏笔名称", item.title], ["内容与作用", item.description], ["重要程度", levelLabel(item.importance)], ["状态", foreshadowStatusLabel(item.status)], ["回收结论", item.resolutionNote]]
|
|
3116
|
+
};
|
|
3117
|
+
const rows = rowsByType[result.type] ?? [];
|
|
3118
|
+
const content = rows
|
|
3119
|
+
.filter(([, value]) => value !== undefined && value !== null && String(value).trim())
|
|
3120
|
+
.map(([label, value]) => `<div><dt>${esc(label)}</dt><dd>${esc(value)}</dd></div>`)
|
|
3121
|
+
.join("");
|
|
3122
|
+
openDialog(
|
|
3123
|
+
result.title || searchResultTypeLabel(result.type),
|
|
3124
|
+
`<dl class="search-result-preview">${content || "<div><dt>详情</dt><dd>暂无补充信息</dd></div>"}</dl>`,
|
|
3125
|
+
async () => undefined,
|
|
3126
|
+
searchResultTypeLabel(result.type),
|
|
3127
|
+
{ submitLabel: "关闭", hideCancel: true, wide: true }
|
|
3128
|
+
);
|
|
3046
3129
|
}
|
|
3047
3130
|
|
|
3048
3131
|
async function openSearchResult(result) {
|
|
@@ -3055,6 +3138,10 @@ async function openSearchResult(result) {
|
|
|
3055
3138
|
if (inSettings) await returnFromSettings();
|
|
3056
3139
|
if (target.kind === "chapter") {
|
|
3057
3140
|
await selectChapter(target.id);
|
|
3141
|
+
if (state.chapter?.id === target.id && target.startLine) {
|
|
3142
|
+
revealChapterSearchLines(target.startLine, target.endLine);
|
|
3143
|
+
toast(target.startLine === target.endLine ? `已定位到第 ${target.startLine} 行` : `已定位到第 ${target.startLine}-${target.endLine} 行`);
|
|
3144
|
+
}
|
|
3058
3145
|
return;
|
|
3059
3146
|
}
|
|
3060
3147
|
await showModule(target.module);
|
|
@@ -3064,6 +3151,10 @@ async function openSearchResult(result) {
|
|
|
3064
3151
|
if (target.entity === "character") await openCharacterEditor(item, { readOnly: true });
|
|
3065
3152
|
if (target.entity === "race") await openRaceDialog(item, { readOnly: true });
|
|
3066
3153
|
if (target.entity === "organization") await openOrganizationDialog(item, { readOnly: true });
|
|
3154
|
+
if (target.entity === "review") openReviewDetailDialog(item);
|
|
3155
|
+
if (["timeline-track", "timeline-event", "relationship", "chapter-outline", "foreshadow"].includes(target.entity)) {
|
|
3156
|
+
openSearchEntityPreview(result, item);
|
|
3157
|
+
}
|
|
3067
3158
|
}
|
|
3068
3159
|
|
|
3069
3160
|
async function showSettingsHub() {
|
|
@@ -3192,9 +3283,14 @@ function resetWorkScopedUiCaches() {
|
|
|
3192
3283
|
aiReferencesLoadWorkId = null;
|
|
3193
3284
|
aiConversationsLoadPromise = null;
|
|
3194
3285
|
aiConversationsLoadWorkId = null;
|
|
3286
|
+
raceHierarchyLoadPromise = null;
|
|
3287
|
+
raceHierarchyLoadWorkId = null;
|
|
3288
|
+
loadedRaceHierarchyWorkId = null;
|
|
3289
|
+
raceListRequestId += 1;
|
|
3195
3290
|
state.models = [];
|
|
3196
3291
|
state.characters = [];
|
|
3197
3292
|
state.settings = [];
|
|
3293
|
+
state.races = [];
|
|
3198
3294
|
characterListPage = 1;
|
|
3199
3295
|
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
3200
3296
|
relationshipFilters.fromCharacterIds = [];
|
|
@@ -3656,6 +3752,10 @@ async function showModule(module) {
|
|
|
3656
3752
|
}
|
|
3657
3753
|
module = fallback;
|
|
3658
3754
|
}
|
|
3755
|
+
if (module !== "tasks") {
|
|
3756
|
+
taskAutoRunEditing = false;
|
|
3757
|
+
taskAutoRunEditingWorkId = null;
|
|
3758
|
+
}
|
|
3659
3759
|
if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
|
|
3660
3760
|
if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
|
|
3661
3761
|
state.module = module;
|
|
@@ -4125,7 +4225,7 @@ async function renderCharacters(page = characterListPage) {
|
|
|
4125
4225
|
const pageSize = pageSizeFor("characters");
|
|
4126
4226
|
const [characterSource, races, organizations] = await Promise.all([
|
|
4127
4227
|
hasCharacterFilters ? apiAllPages(`/api/works/${state.work.id}/characters`) : apiPage(`/api/works/${state.work.id}/characters`, page, pageSize),
|
|
4128
|
-
canReadModule("races") ?
|
|
4228
|
+
canReadModule("races") ? api(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
4129
4229
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
4130
4230
|
]);
|
|
4131
4231
|
const characterPage = hasCharacterFilters
|
|
@@ -4228,15 +4328,16 @@ async function renderCharacters(page = characterListPage) {
|
|
|
4228
4328
|
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(pageCharacters.find((item) => item.id === button.dataset.editCharacter))));
|
|
4229
4329
|
}
|
|
4230
4330
|
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
mountModuleCount(state.races.length);
|
|
4331
|
+
function renderRaceCollection(total, descendantsLoading = false) {
|
|
4332
|
+
mountModuleCount(total);
|
|
4234
4333
|
const layout = readModuleLayout();
|
|
4235
|
-
const
|
|
4236
|
-
?
|
|
4237
|
-
:
|
|
4238
|
-
moduleListPages.races = pageResult.page;
|
|
4334
|
+
const raceItems = layout === "rows"
|
|
4335
|
+
? [...state.races].sort((left, right) => String(left.name).localeCompare(String(right.name), "zh-CN"))
|
|
4336
|
+
: buildRaceForest(state.races);
|
|
4239
4337
|
const canEditRaces = canEditModule("races");
|
|
4338
|
+
const directChildCount = (item) => Number.isInteger(Number(item.childCount))
|
|
4339
|
+
? Number(item.childCount)
|
|
4340
|
+
: Array.isArray(item.children) ? item.children.length : 0;
|
|
4240
4341
|
const raceActions = (item) => canEditRaces
|
|
4241
4342
|
? recordCardEditButton("edit-race", item.id, `种族“${item.name}”`)
|
|
4242
4343
|
: recordHistoryButton("race", item.id, item.name);
|
|
@@ -4244,7 +4345,7 @@ async function renderRaces(page = moduleListPages.races) {
|
|
|
4244
4345
|
? raceActions(item)
|
|
4245
4346
|
: `<div class="card-actions">${raceActions(item)}</div>`;
|
|
4246
4347
|
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
|
|
4348
|
+
<summary><span>${esc(item.name)}</span><small>${directChildCount(item)} 个直接子种族</small></summary>
|
|
4248
4349
|
<div class="race-tree-branch">
|
|
4249
4350
|
<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
4351
|
<div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
|
|
@@ -4256,9 +4357,9 @@ async function renderRaces(page = moduleListPages.races) {
|
|
|
4256
4357
|
${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
|
|
4257
4358
|
</div>
|
|
4258
4359
|
</details>`;
|
|
4259
|
-
const raceRows = () => `<div class="module-row-list">${
|
|
4360
|
+
const raceRows = () => `<div class="module-row-list">${raceItems.map((item) => {
|
|
4260
4361
|
const preview = moduleRowPreview(item.description || "尚未填写种族简介");
|
|
4261
|
-
const meta = `${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
|
|
4362
|
+
const meta = `${directChildCount(item)} 个直接子种族 · ${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
|
|
4262
4363
|
return `
|
|
4263
4364
|
<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
4365
|
<small>${esc(meta)}</small>
|
|
@@ -4270,15 +4371,65 @@ async function renderRaces(page = moduleListPages.races) {
|
|
|
4270
4371
|
if (state.races.length) mountModuleLayoutToggle(layout, "种族列表样式");
|
|
4271
4372
|
if (state.races.length && layout !== "rows") mountRaceTreeExpandToggle();
|
|
4272
4373
|
$("#module-content").innerHTML = state.races.length
|
|
4273
|
-
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${
|
|
4374
|
+
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级" aria-busy="${descendantsLoading}">${raceItems.map(renderRaceNode).join("")}</section>`}`
|
|
4274
4375
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
4275
|
-
bindModuleLayoutToggle(() =>
|
|
4276
|
-
bindModulePagination("races", renderRaces);
|
|
4376
|
+
bindModuleLayoutToggle(() => renderRaceCollection(total, descendantsLoading));
|
|
4277
4377
|
bindRaceTreeExpandToggle();
|
|
4278
4378
|
bindRaceTreeNodeToggles();
|
|
4279
4379
|
const openRace = async (id, readOnly) => openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly });
|
|
4280
4380
|
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => { void openRace(button.dataset.editRace, false); }));
|
|
4281
|
-
bindEntityHistoryButtons(async () => { await renderRaces(
|
|
4381
|
+
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
async function ensureCompleteRaceList() {
|
|
4385
|
+
const workId = state.work?.id;
|
|
4386
|
+
if (!workId) return false;
|
|
4387
|
+
const generation = workScopedUiGeneration;
|
|
4388
|
+
if (loadedRaceHierarchyWorkId === workId) return true;
|
|
4389
|
+
if (raceHierarchyLoadPromise && raceHierarchyLoadWorkId === workId) {
|
|
4390
|
+
await raceHierarchyLoadPromise.catch(() => {});
|
|
4391
|
+
}
|
|
4392
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return false;
|
|
4393
|
+
if (loadedRaceHierarchyWorkId === workId) return true;
|
|
4394
|
+
const races = await api(`/api/works/${workId}/races`);
|
|
4395
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return false;
|
|
4396
|
+
state.races = races;
|
|
4397
|
+
loadedRaceHierarchyWorkId = workId;
|
|
4398
|
+
return true;
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
async function renderRaces() {
|
|
4402
|
+
const workId = state.work.id;
|
|
4403
|
+
const generation = workScopedUiGeneration;
|
|
4404
|
+
const requestId = ++raceListRequestId;
|
|
4405
|
+
const roots = await api(`/api/works/${workId}/races?scope=roots`);
|
|
4406
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
|
|
4407
|
+
state.races = roots.items;
|
|
4408
|
+
loadedRaceHierarchyWorkId = roots.items.length === roots.total ? workId : null;
|
|
4409
|
+
renderRaceCollection(roots.total, loadedRaceHierarchyWorkId !== workId);
|
|
4410
|
+
if (loadedRaceHierarchyWorkId === workId) return;
|
|
4411
|
+
|
|
4412
|
+
const dismissLoadingToast = persistentToast("正在加载子种族……");
|
|
4413
|
+
const loadPromise = api(`/api/works/${workId}/races?scope=descendants`).then((descendants) => {
|
|
4414
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
|
|
4415
|
+
state.races = [...roots.items, ...descendants];
|
|
4416
|
+
loadedRaceHierarchyWorkId = workId;
|
|
4417
|
+
if (state.module === "races") renderRaceCollection(roots.total);
|
|
4418
|
+
});
|
|
4419
|
+
raceHierarchyLoadPromise = loadPromise;
|
|
4420
|
+
raceHierarchyLoadWorkId = workId;
|
|
4421
|
+
void loadPromise.catch((error) => {
|
|
4422
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration && requestId === raceListRequestId) {
|
|
4423
|
+
renderRaceCollection(roots.total);
|
|
4424
|
+
toast(`父种族已显示,但子种族载入失败:${error.message}`, "error");
|
|
4425
|
+
}
|
|
4426
|
+
}).finally(() => {
|
|
4427
|
+
if (raceHierarchyLoadPromise === loadPromise) {
|
|
4428
|
+
raceHierarchyLoadPromise = null;
|
|
4429
|
+
raceHierarchyLoadWorkId = null;
|
|
4430
|
+
}
|
|
4431
|
+
dismissLoadingToast();
|
|
4432
|
+
});
|
|
4282
4433
|
}
|
|
4283
4434
|
|
|
4284
4435
|
async function renderOrganizations(page = moduleListPages.organizations) {
|
|
@@ -4619,7 +4770,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4619
4770
|
apiPage(`/api/works/${state.work.id}/tasks`, page, pageSize),
|
|
4620
4771
|
canReadModule("ai-settings")
|
|
4621
4772
|
? api(`/api/works/${state.work.id}/ai-settings`)
|
|
4622
|
-
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2,
|
|
4773
|
+
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunDailyTaskLimit: 0, autoRunFailureThreshold: 3, autoRunPaused: false })
|
|
4623
4774
|
]);
|
|
4624
4775
|
if (!taskPage.items.length && page > 1) return renderTasks(page - 1);
|
|
4625
4776
|
taskListPage = taskPage.page;
|
|
@@ -4627,10 +4778,18 @@ async function renderTasks(page = taskListPage) {
|
|
|
4627
4778
|
const taskTotal = Number(taskPage.total ?? taskPage.stats?.total ?? tasks.length);
|
|
4628
4779
|
mountModuleCount(taskTotal);
|
|
4629
4780
|
const canConfigureAutoRun = canEditModule("tasks") && canEditModule("ai-settings");
|
|
4781
|
+
const autoRunEditing = canConfigureAutoRun && taskAutoRunEditing && taskAutoRunEditingWorkId === state.work.id;
|
|
4630
4782
|
const pendingCount = Number(taskPage.stats?.pendingCount ?? 0);
|
|
4631
4783
|
const runningCount = Number(taskPage.stats?.runningCount ?? 0);
|
|
4632
4784
|
const activeTaskCount = pendingCount + runningCount;
|
|
4633
4785
|
const runningProgress = runningCount ? analysisTaskProgressValue(taskPage.stats?.runningProgress) : 0;
|
|
4786
|
+
const autoRunPaused = Boolean(settings.autoRunEnabled && settings.autoRunPaused);
|
|
4787
|
+
const autoRunActive = Boolean(settings.autoRunEnabled && !autoRunPaused);
|
|
4788
|
+
const autoRunPauseLabel = autoRunPaused ? "恢复自动执行" : "暂停自动执行";
|
|
4789
|
+
const queueProgressLabel = runningCount
|
|
4790
|
+
? "运行中任务平均进度"
|
|
4791
|
+
: autoRunPaused ? "自动执行已暂停" : autoRunActive ? "等待任务开始" : "自动执行已关闭";
|
|
4792
|
+
const queueProgressClass = runningCount ? "is-running" : autoRunPaused ? "is-paused" : "is-waiting";
|
|
4634
4793
|
const visibleTaskIds = new Set(tasks.map((item) => String(item.id)));
|
|
4635
4794
|
for (const taskId of taskStatusSnapshots.keys()) {
|
|
4636
4795
|
if (!visibleTaskIds.has(taskId)) taskStatusSnapshots.delete(taskId);
|
|
@@ -4647,27 +4806,34 @@ async function renderTasks(page = taskListPage) {
|
|
|
4647
4806
|
<div class="task-auto-run-copy">
|
|
4648
4807
|
<strong id="task-auto-run-title">自动执行待分析任务</strong>
|
|
4649
4808
|
<small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
|
|
4650
|
-
<small
|
|
4809
|
+
<small>开启后会持续执行直到队列清空;临时错误自动退避重试,连续失败达到阈值后暂停。</small>
|
|
4810
|
+
</div>
|
|
4811
|
+
<div class="task-auto-run-actions">
|
|
4812
|
+
${autoRunEditing ? "" : '<button id="task-auto-run-edit" class="ghost-button" type="button">编辑</button>'}
|
|
4651
4813
|
</div>
|
|
4652
4814
|
<div class="task-auto-run-controls">
|
|
4653
|
-
<label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""}><span
|
|
4654
|
-
<label>同时运行上限<input id="task-auto-run-concurrency" type="number" min="1" max="8" value="${esc(String(settings.autoRunConcurrency ?? 2))}"></label>
|
|
4655
|
-
<label
|
|
4656
|
-
<
|
|
4657
|
-
|
|
4815
|
+
<label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""} ${autoRunEditing ? "" : "disabled"}><span>持续自动执行</span></label>
|
|
4816
|
+
<label>同时运行上限<input id="task-auto-run-concurrency" type="number" min="1" max="8" value="${esc(String(settings.autoRunConcurrency ?? 2))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}"></label>
|
|
4817
|
+
<label>每日任务上限<input id="task-auto-run-daily-limit" type="number" min="0" max="10000" value="${esc(String(settings.autoRunDailyTaskLimit ?? 0))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}" aria-describedby="task-auto-run-daily-help"></label>
|
|
4818
|
+
<label>连续失败暂停阈值<input id="task-auto-run-failure-threshold" type="number" min="1" max="10" value="${esc(String(settings.autoRunFailureThreshold ?? 3))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}"></label>
|
|
4819
|
+
${autoRunEditing
|
|
4820
|
+
? `<button id="task-auto-run-save" class="primary-button" type="button">保存并生效</button><button id="task-auto-run-pause" class="ghost-button" type="button">${autoRunPauseLabel}</button>`
|
|
4821
|
+
: ""}
|
|
4658
4822
|
</div>
|
|
4659
|
-
<p class="task-auto-run-
|
|
4823
|
+
<p id="task-auto-run-daily-help" class="task-auto-run-help">每日任务上限填 0 表示不限制;达到上限后会在下一个 UTC 自然日自动恢复。</p>
|
|
4824
|
+
<p class="task-auto-run-meta">待执行队列 ${pendingCount} 个 · 正在运行 ${runningCount} 个 · ${autoRunPaused ? "已暂停" : autoRunActive ? "持续执行中" : "未开启"}</p>
|
|
4825
|
+
${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
4826
|
<div class="task-auto-run-progress ${activeTaskCount ? "" : "hidden"}" aria-live="polite">
|
|
4661
|
-
<div class="task-auto-run-progress-ring ${
|
|
4827
|
+
<div class="task-auto-run-progress-ring ${queueProgressClass}" role="progressbar" aria-label="${queueProgressLabel}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${runningProgress}">
|
|
4662
4828
|
<svg viewBox="0 0 120 120" aria-hidden="true" focusable="false">
|
|
4663
4829
|
<circle class="task-auto-run-progress-ring-track" cx="60" cy="60" r="52"></circle>
|
|
4664
4830
|
<circle class="task-auto-run-progress-ring-value" cx="60" cy="60" r="52" pathLength="100" stroke-dasharray="${runningProgress} 100"></circle>
|
|
4665
4831
|
</svg>
|
|
4666
|
-
<div class="task-auto-run-progress-ring-label"><strong>${runningProgress}%</strong><span>${
|
|
4832
|
+
<div class="task-auto-run-progress-ring-label"><strong>${runningProgress}%</strong><span>${queueProgressLabel}</span></div>
|
|
4667
4833
|
</div>
|
|
4668
4834
|
<div class="task-auto-run-progress-bar-layout">
|
|
4669
|
-
<div class="task-auto-run-progress-label"><span>${
|
|
4670
|
-
<progress class="task-auto-run-progress-bar ${
|
|
4835
|
+
<div class="task-auto-run-progress-label"><span>${queueProgressLabel}</span><strong>${runningProgress}%</strong></div>
|
|
4836
|
+
<progress class="task-auto-run-progress-bar ${queueProgressClass}" max="100" value="${runningProgress}" aria-label="${queueProgressLabel}">${runningProgress}%</progress>
|
|
4671
4837
|
</div>
|
|
4672
4838
|
</div>
|
|
4673
4839
|
</section>
|
|
@@ -4680,7 +4846,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4680
4846
|
<td class="task-progress-cell">${renderAnalysisTaskProgress(item)}</td>
|
|
4681
4847
|
<td class="task-row-actions">
|
|
4682
4848
|
<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>` : ""}
|
|
4849
|
+
${item.status === "pending" && !item.nextAttemptAt ? `<button class="ghost-button" type="button" data-run-task="${esc(item.id)}">运行</button>` : ""}
|
|
4684
4850
|
${item.status === "pending" || item.status === "running" ? `<button class="ghost-button" type="button" data-cancel-task="${esc(item.id)}">取消</button>` : ""}
|
|
4685
4851
|
</td>
|
|
4686
4852
|
</tr>`).join("")}</tbody></table>${pagination}` : emptyModule("还没有 AI 分析记录", "点击“开始 AI 分析”,可分析指定章节或整部作品。")}`;
|
|
@@ -4691,6 +4857,12 @@ async function renderTasks(page = taskListPage) {
|
|
|
4691
4857
|
await renderTasks(Number(button.dataset.taskPage));
|
|
4692
4858
|
}));
|
|
4693
4859
|
|
|
4860
|
+
$("#task-auto-run-edit")?.addEventListener("click", () => {
|
|
4861
|
+
taskAutoRunEditing = true;
|
|
4862
|
+
taskAutoRunEditingWorkId = state.work.id;
|
|
4863
|
+
void renderTasks(taskListPage).catch((error) => toast(error.message, "error"));
|
|
4864
|
+
});
|
|
4865
|
+
|
|
4694
4866
|
$("#task-auto-run-save")?.addEventListener("click", async () => {
|
|
4695
4867
|
const button = $("#task-auto-run-save");
|
|
4696
4868
|
button.disabled = true;
|
|
@@ -4700,32 +4872,42 @@ async function renderTasks(page = taskListPage) {
|
|
|
4700
4872
|
body: {
|
|
4701
4873
|
autoRunEnabled: $("#task-auto-run-enabled").checked,
|
|
4702
4874
|
autoRunConcurrency: Number($("#task-auto-run-concurrency").value),
|
|
4703
|
-
|
|
4875
|
+
autoRunDailyTaskLimit: Number($("#task-auto-run-daily-limit").value),
|
|
4876
|
+
autoRunFailureThreshold: Number($("#task-auto-run-failure-threshold").value)
|
|
4704
4877
|
}
|
|
4705
4878
|
});
|
|
4706
4879
|
toast(updated.autoRunEnabled
|
|
4707
|
-
?
|
|
4880
|
+
? `持续自动执行已开启:同时最多 ${updated.autoRunConcurrency} 个`
|
|
4708
4881
|
: "自动执行已关闭");
|
|
4882
|
+
taskAutoRunEditing = false;
|
|
4883
|
+
taskAutoRunEditingWorkId = null;
|
|
4709
4884
|
await renderTasks();
|
|
4710
4885
|
} catch (error) {
|
|
4711
4886
|
toast(error.message, "error");
|
|
4712
4887
|
button.disabled = false;
|
|
4713
4888
|
}
|
|
4714
4889
|
});
|
|
4715
|
-
$("#task-auto-run-
|
|
4716
|
-
const button = $("#task-auto-run-
|
|
4890
|
+
$("#task-auto-run-pause")?.addEventListener("click", async () => {
|
|
4891
|
+
const button = $("#task-auto-run-pause");
|
|
4717
4892
|
button.disabled = true;
|
|
4718
4893
|
try {
|
|
4719
|
-
|
|
4720
|
-
|
|
4894
|
+
if (autoRunPaused) {
|
|
4895
|
+
await api(`/api/works/${state.work.id}/tasks/auto-run`, { method: "POST", body: {} });
|
|
4896
|
+
toast("自动执行已恢复");
|
|
4897
|
+
} else {
|
|
4898
|
+
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { autoRunEnabled: false } });
|
|
4899
|
+
toast("已暂停自动执行");
|
|
4900
|
+
}
|
|
4901
|
+
taskAutoRunEditing = false;
|
|
4902
|
+
taskAutoRunEditingWorkId = null;
|
|
4721
4903
|
await refreshBackgroundTaskCenter({ announce: false });
|
|
4722
4904
|
await renderTasks();
|
|
4905
|
+
window.setTimeout(() => $("#task-auto-run-edit")?.focus(), 0);
|
|
4723
4906
|
} catch (error) {
|
|
4724
4907
|
toast(error.message, "error");
|
|
4725
4908
|
button.disabled = false;
|
|
4726
4909
|
}
|
|
4727
4910
|
});
|
|
4728
|
-
|
|
4729
4911
|
$("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
|
|
4730
4912
|
if (button.disabled) return;
|
|
4731
4913
|
button.disabled = true;
|
|
@@ -4776,7 +4958,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4776
4958
|
button.disabled = false;
|
|
4777
4959
|
}
|
|
4778
4960
|
}));
|
|
4779
|
-
scheduleTaskProgressRefresh(state.work.id, runningCount);
|
|
4961
|
+
scheduleTaskProgressRefresh(state.work.id, runningCount > 0 || (pendingCount > 0 && autoRunActive) ? 1 : 0);
|
|
4780
4962
|
}
|
|
4781
4963
|
|
|
4782
4964
|
async function rerunAnalysisTask(taskId, button, { closeDetail = false } = {}) {
|
|
@@ -4796,6 +4978,51 @@ async function rerunAnalysisTask(taskId, button, { closeDetail = false } = {}) {
|
|
|
4796
4978
|
}
|
|
4797
4979
|
}
|
|
4798
4980
|
|
|
4981
|
+
async function rerunAnalysisTaskWithModel(task, button) {
|
|
4982
|
+
const originalLabel = button.textContent;
|
|
4983
|
+
button.disabled = true;
|
|
4984
|
+
button.textContent = "加载模型";
|
|
4985
|
+
try {
|
|
4986
|
+
const models = await api(`/api/works/${encodeURIComponent(task.workId)}/models`);
|
|
4987
|
+
const currentModelId = String(task.model?.id ?? "");
|
|
4988
|
+
const availableModels = models.filter((model) =>
|
|
4989
|
+
model.id !== currentModelId
|
|
4990
|
+
&& model.enabled
|
|
4991
|
+
&& model.providerStatus === "enabled"
|
|
4992
|
+
&& model.providerConnectionStatus === "success"
|
|
4993
|
+
);
|
|
4994
|
+
if (!availableModels.length) throw new Error("当前没有其他可用模型,请先配置并测试模型");
|
|
4995
|
+
$("#form-dialog").close();
|
|
4996
|
+
const currentModelLabel = task.model?.displayName || "运行时默认模型";
|
|
4997
|
+
openDialog("选择重试模型",
|
|
4998
|
+
`<p class="task-rerun-model-intro">当前模型:${esc(currentModelLabel)}。选择其他模型后,将按原任务范围和最新资料创建新任务。</p><label>重试模型<select name="modelId" required aria-label="重试模型">
|
|
4999
|
+
${availableModels.map((model) => `<option value="${esc(model.id)}">${esc(modelOptionLabel(model))}</option>`).join("")}
|
|
5000
|
+
</select></label>`,
|
|
5001
|
+
async (form) => {
|
|
5002
|
+
const modelId = String(form.get("modelId") ?? "").trim();
|
|
5003
|
+
const selectedModel = availableModels.find((model) => model.id === modelId);
|
|
5004
|
+
const rerun = await api(`/api/tasks/${encodeURIComponent(task.id)}/rerun`, {
|
|
5005
|
+
method: "POST",
|
|
5006
|
+
body: { modelId }
|
|
5007
|
+
});
|
|
5008
|
+
toast(`已使用${selectedModel ? `“${modelOptionLabel(selectedModel)}”` : "新模型"}创建重试任务 ${rerun.id}`);
|
|
5009
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
5010
|
+
if (state.module === "tasks" && state.work?.id === task.workId) await renderTasks();
|
|
5011
|
+
},
|
|
5012
|
+
"AI 分析重试",
|
|
5013
|
+
{
|
|
5014
|
+
submitLabel: "换模型重试",
|
|
5015
|
+
pendingLabel: "创建中…",
|
|
5016
|
+
pendingMessage: "正在使用新模型创建重试任务,请稍候",
|
|
5017
|
+
errorPrefix: "换模型重试失败:"
|
|
5018
|
+
});
|
|
5019
|
+
} catch (error) {
|
|
5020
|
+
toast(error.message, "error");
|
|
5021
|
+
button.disabled = false;
|
|
5022
|
+
button.textContent = originalLabel;
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
|
|
4799
5026
|
function stopTaskProgressRefresh() {
|
|
4800
5027
|
if (taskProgressRefreshTimer === null) return;
|
|
4801
5028
|
window.clearTimeout(taskProgressRefreshTimer);
|
|
@@ -5246,7 +5473,7 @@ function openTaskDetailDialog(task, trace) {
|
|
|
5246
5473
|
<div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
|
|
5247
5474
|
<div><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
|
|
5248
5475
|
<div><strong>结果摘要</strong>${resultPreview}</div>
|
|
5249
|
-
${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
|
|
5476
|
+
${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><button class="ghost-button" type="button" data-rerun-task-model="${esc(task.id)}">换模型重试</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
|
|
5250
5477
|
</section>
|
|
5251
5478
|
${renderTaskTraceVisualization(trace, task.id)}
|
|
5252
5479
|
</div>`,
|
|
@@ -5258,6 +5485,9 @@ function openTaskDetailDialog(task, trace) {
|
|
|
5258
5485
|
$("#dialog-fields").querySelector("[data-rerun-task-detail]")?.addEventListener("click", async (event) => {
|
|
5259
5486
|
await rerunAnalysisTask(event.currentTarget.dataset.rerunTaskDetail, event.currentTarget, { closeDetail: true });
|
|
5260
5487
|
});
|
|
5488
|
+
$("#dialog-fields").querySelector("[data-rerun-task-model]")?.addEventListener("click", async (event) => {
|
|
5489
|
+
await rerunAnalysisTaskWithModel(task, event.currentTarget);
|
|
5490
|
+
});
|
|
5261
5491
|
$("#dialog-fields").querySelector("[data-task-identity-repair]")?.addEventListener("click", async (event) => {
|
|
5262
5492
|
const button = event.currentTarget;
|
|
5263
5493
|
button.disabled = true;
|
|
@@ -5703,7 +5933,7 @@ async function renderBookAiSettings() {
|
|
|
5703
5933
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
5704
5934
|
title: "本书 Token 用量",
|
|
5705
5935
|
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
|
|
5936
|
+
})}</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
5937
|
scrollUsageCalendarsToLatest(host);
|
|
5708
5938
|
host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
|
|
5709
5939
|
"beforebegin",
|
|
@@ -6801,6 +7031,107 @@ function toggleVditorLineNumbers(editor) {
|
|
|
6801
7031
|
const enabled = host.classList.toggle("show-line-numbers");
|
|
6802
7032
|
const button = host.querySelector(".vditor-line-number-button");
|
|
6803
7033
|
if (button) button.setAttribute("aria-pressed", String(enabled));
|
|
7034
|
+
updateVditorLineNumbers(editor, getVditorMarkdown(editor));
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
function getVditorMarkdown(editor) {
|
|
7038
|
+
if (typeof editor?.getValue === "function") {
|
|
7039
|
+
try {
|
|
7040
|
+
return String(editor.getValue() ?? "");
|
|
7041
|
+
} catch {
|
|
7042
|
+
return "";
|
|
7043
|
+
}
|
|
7044
|
+
}
|
|
7045
|
+
const host = editor?.vditor?.element;
|
|
7046
|
+
const valueField = host?.closest("[data-vditor-editor-field], .setting-markdown-field, .character-markdown-editor")?.querySelector("[data-vditor-value]")
|
|
7047
|
+
?? host?.parentElement?.querySelector("[data-vditor-value]");
|
|
7048
|
+
return String(valueField?.value ?? "");
|
|
7049
|
+
}
|
|
7050
|
+
|
|
7051
|
+
function getVditorVisibleSurface(content) {
|
|
7052
|
+
const candidates = [
|
|
7053
|
+
content.querySelector(".vditor-ir .vditor-reset"),
|
|
7054
|
+
content.querySelector(".vditor-wysiwyg .vditor-reset"),
|
|
7055
|
+
content.querySelector(".vditor-sv textarea"),
|
|
7056
|
+
content.querySelector(".vditor-sv")
|
|
7057
|
+
];
|
|
7058
|
+
return candidates.find((surface) => {
|
|
7059
|
+
if (!surface) return false;
|
|
7060
|
+
const rect = surface.getBoundingClientRect();
|
|
7061
|
+
const style = getComputedStyle(surface);
|
|
7062
|
+
return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
|
|
7063
|
+
}) ?? null;
|
|
7064
|
+
}
|
|
7065
|
+
|
|
7066
|
+
function getVditorLineHeight(surface) {
|
|
7067
|
+
const style = getComputedStyle(surface);
|
|
7068
|
+
return parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5 || 24;
|
|
7069
|
+
}
|
|
7070
|
+
|
|
7071
|
+
function collectVditorVisualLineRects(surface) {
|
|
7072
|
+
const rects = [];
|
|
7073
|
+
const scrollTop = Number(surface.scrollTop) || 0;
|
|
7074
|
+
const visit = (node) => {
|
|
7075
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
7076
|
+
if (!node.textContent) return;
|
|
7077
|
+
const range = document.createRange();
|
|
7078
|
+
range.selectNodeContents(node);
|
|
7079
|
+
[...range.getClientRects()].forEach((rect) => {
|
|
7080
|
+
if (rect.height > 0) rects.push({ top: rect.top + scrollTop, height: rect.height });
|
|
7081
|
+
});
|
|
7082
|
+
return;
|
|
7083
|
+
}
|
|
7084
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
7085
|
+
if (node.tagName === "BR") {
|
|
7086
|
+
const rect = node.getBoundingClientRect();
|
|
7087
|
+
if (rect.height > 0) rects.push({ top: rect.top + scrollTop, height: rect.height });
|
|
7088
|
+
}
|
|
7089
|
+
[...node.childNodes].forEach(visit);
|
|
7090
|
+
};
|
|
7091
|
+
visit(surface);
|
|
7092
|
+
if (rects.length === 0) {
|
|
7093
|
+
const rect = surface.getBoundingClientRect();
|
|
7094
|
+
const style = getComputedStyle(surface);
|
|
7095
|
+
rects.push({ top: rect.top + scrollTop + (parseFloat(style.paddingTop) || 0), height: getVditorLineHeight(surface) });
|
|
7096
|
+
}
|
|
7097
|
+
return rects;
|
|
7098
|
+
}
|
|
7099
|
+
|
|
7100
|
+
function syncVditorLineNumberScroll(editor) {
|
|
7101
|
+
const surface = editor?.__vditorLineNumberSurface;
|
|
7102
|
+
const gutter = editor?.vditor?.element?.querySelector(".vditor-line-numbers");
|
|
7103
|
+
if (!surface || !gutter) return;
|
|
7104
|
+
gutter.style.transform = `translateY(${-surface.scrollTop}px)`;
|
|
7105
|
+
gutter.dataset.scrollTop = String(surface.scrollTop);
|
|
7106
|
+
}
|
|
7107
|
+
|
|
7108
|
+
function scheduleVditorLineNumbers(editor) {
|
|
7109
|
+
if (!editor || editor.__vditorLineNumberFrame) return;
|
|
7110
|
+
editor.__vditorLineNumberFrame = requestAnimationFrame(() => {
|
|
7111
|
+
editor.__vditorLineNumberFrame = null;
|
|
7112
|
+
updateVditorLineNumbers(editor, getVditorMarkdown(editor));
|
|
7113
|
+
});
|
|
7114
|
+
}
|
|
7115
|
+
|
|
7116
|
+
function setupVditorLineNumberSync(editor, content, surface) {
|
|
7117
|
+
if (editor.__vditorLineNumberSurface !== surface) {
|
|
7118
|
+
editor.__vditorLineNumberSurface?.removeEventListener("scroll", editor.__vditorLineNumberScrollHandler);
|
|
7119
|
+
editor.__vditorLineNumberSurface = surface;
|
|
7120
|
+
editor.__vditorLineNumberScrollHandler = () => syncVditorLineNumberScroll(editor);
|
|
7121
|
+
surface.addEventListener("scroll", editor.__vditorLineNumberScrollHandler, { passive: true });
|
|
7122
|
+
}
|
|
7123
|
+
if (!editor.__vditorLineNumberObserver) {
|
|
7124
|
+
editor.__vditorLineNumberObserver = new MutationObserver((mutations) => {
|
|
7125
|
+
if (mutations.some((mutation) => !(mutation.target instanceof Element) || !mutation.target.closest(".vditor-line-numbers"))) {
|
|
7126
|
+
scheduleVditorLineNumbers(editor);
|
|
7127
|
+
}
|
|
7128
|
+
});
|
|
7129
|
+
editor.__vditorLineNumberObserver.observe(content, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "style"] });
|
|
7130
|
+
}
|
|
7131
|
+
if (!editor.__vditorLineNumberResizeObserver && typeof ResizeObserver !== "undefined") {
|
|
7132
|
+
editor.__vditorLineNumberResizeObserver = new ResizeObserver(() => scheduleVditorLineNumbers(editor));
|
|
7133
|
+
editor.__vditorLineNumberResizeObserver.observe(content);
|
|
7134
|
+
}
|
|
6804
7135
|
}
|
|
6805
7136
|
|
|
6806
7137
|
function updateVditorLineNumbers(editor, markdown) {
|
|
@@ -6813,14 +7144,30 @@ function updateVditorLineNumbers(editor, markdown) {
|
|
|
6813
7144
|
gutter.setAttribute("aria-hidden", "true");
|
|
6814
7145
|
content.prepend(gutter);
|
|
6815
7146
|
}
|
|
6816
|
-
const
|
|
7147
|
+
const surface = getVditorVisibleSurface(content);
|
|
7148
|
+
if (!surface) return;
|
|
7149
|
+
setupVditorLineNumberSync(editor, content, surface);
|
|
7150
|
+
const contentRect = content.getBoundingClientRect();
|
|
7151
|
+
const lineHeight = getVditorLineHeight(surface);
|
|
7152
|
+
const normalizedMarkdown = String(markdown ?? "").replace(/\r\n?/gu, "\n");
|
|
7153
|
+
const blankLineCount = normalizedMarkdown.trim() === "" ? 0 : normalizedMarkdown.split("\n").filter((line) => line.trim() === "").length;
|
|
7154
|
+
const visualRows = buildVditorLineNumberRows(collectVditorVisualLineRects(surface), { lineHeight, blankLineCount });
|
|
7155
|
+
gutter.style.height = `${content.clientHeight}px`;
|
|
6817
7156
|
const fragment = document.createDocumentFragment();
|
|
6818
|
-
|
|
7157
|
+
visualRows.forEach((row, index) => {
|
|
6819
7158
|
const number = document.createElement("span");
|
|
6820
|
-
number.
|
|
7159
|
+
number.className = "vditor-line-number";
|
|
7160
|
+
number.textContent = String(index + 1);
|
|
7161
|
+
number.dataset.lineNumber = String(index + 1);
|
|
7162
|
+
number.dataset.blank = String(row.blank);
|
|
7163
|
+
number.style.top = `${row.top - contentRect.top}px`;
|
|
7164
|
+
number.style.height = `${row.height}px`;
|
|
7165
|
+
number.style.lineHeight = `${row.height}px`;
|
|
6821
7166
|
fragment.append(number);
|
|
6822
|
-
}
|
|
7167
|
+
});
|
|
6823
7168
|
gutter.replaceChildren(fragment);
|
|
7169
|
+
gutter.dataset.lineCount = String(visualRows.length);
|
|
7170
|
+
syncVditorLineNumberScroll(editor);
|
|
6824
7171
|
}
|
|
6825
7172
|
|
|
6826
7173
|
function updateVditorWordCount(editor, markdown) {
|
|
@@ -6865,6 +7212,12 @@ function ensureVditorIconScript() {
|
|
|
6865
7212
|
function destroyVditorEditor(editor) {
|
|
6866
7213
|
if (!editor) return;
|
|
6867
7214
|
editor.__attachmentObserver?.disconnect();
|
|
7215
|
+
editor.__vditorLineNumberObserver?.disconnect();
|
|
7216
|
+
editor.__vditorLineNumberResizeObserver?.disconnect();
|
|
7217
|
+
if (editor.__vditorLineNumberSurface && editor.__vditorLineNumberScrollHandler) {
|
|
7218
|
+
editor.__vditorLineNumberSurface.removeEventListener("scroll", editor.__vditorLineNumberScrollHandler);
|
|
7219
|
+
}
|
|
7220
|
+
if (editor.__vditorLineNumberFrame) cancelAnimationFrame(editor.__vditorLineNumberFrame);
|
|
6868
7221
|
const host = editor.vditor?.element;
|
|
6869
7222
|
editor.destroy();
|
|
6870
7223
|
if (host) delete host.__vditor;
|
|
@@ -7307,7 +7660,7 @@ async function showCharacterHistory() {
|
|
|
7307
7660
|
async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
7308
7661
|
entityEditorReadOnly = readOnly;
|
|
7309
7662
|
[state.races, state.organizations, state.characters] = await Promise.all([
|
|
7310
|
-
canReadModule("races") ?
|
|
7663
|
+
canReadModule("races") ? api(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
7311
7664
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([]),
|
|
7312
7665
|
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
7313
7666
|
]);
|
|
@@ -7440,6 +7793,7 @@ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
|
|
|
7440
7793
|
async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
|
|
7441
7794
|
entityEditorReadOnly = readOnly;
|
|
7442
7795
|
await discardPendingMarkdownAttachments();
|
|
7796
|
+
if (kind === "race" && !(await ensureCompleteRaceList())) return;
|
|
7443
7797
|
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
7444
7798
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
7445
7799
|
const isRace = kind === "race";
|
|
@@ -8553,18 +8907,22 @@ async function showChapterInsight() {
|
|
|
8553
8907
|
if (requestId !== chapterInsightRequestId || state.chapter?.id !== chapterId || $("#editor-view").classList.contains("hidden")) return;
|
|
8554
8908
|
const insight = insights.find((item) => item.chapterVersion === state.chapter.versionNo) ?? insights[0];
|
|
8555
8909
|
const region = $("#toast-region");
|
|
8910
|
+
const chapterTitle = state.chapter.title;
|
|
8556
8911
|
const element = document.createElement("section");
|
|
8557
8912
|
element.id = "chapter-insight-toast";
|
|
8558
8913
|
element.className = "toast chapter-insight-toast";
|
|
8559
8914
|
element.setAttribute("role", "region");
|
|
8560
|
-
element.setAttribute("aria-label",
|
|
8915
|
+
element.setAttribute("aria-label", `章节概览:${chapterTitle}`);
|
|
8561
8916
|
const header = document.createElement("header");
|
|
8562
8917
|
header.className = "chapter-insight-toast-header";
|
|
8563
8918
|
const heading = document.createElement("div");
|
|
8564
8919
|
heading.className = "chapter-insight-toast-heading";
|
|
8565
8920
|
const title = document.createElement("strong");
|
|
8566
8921
|
title.textContent = "章节概览";
|
|
8567
|
-
|
|
8922
|
+
const chapterName = document.createElement("span");
|
|
8923
|
+
chapterName.className = "chapter-insight-toast-chapter-title";
|
|
8924
|
+
chapterName.textContent = chapterTitle;
|
|
8925
|
+
heading.append(title, chapterName);
|
|
8568
8926
|
if (insight && insight.chapterVersion !== state.chapter.versionNo) {
|
|
8569
8927
|
const stale = document.createElement("span");
|
|
8570
8928
|
stale.textContent = `基于旧版本 v${insight.chapterVersion}`;
|
|
@@ -9209,7 +9567,6 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
9209
9567
|
pageSizes: {
|
|
9210
9568
|
settings: Number($("#page-size-settings").value),
|
|
9211
9569
|
characters: Number($("#page-size-characters").value),
|
|
9212
|
-
races: Number($("#page-size-races").value),
|
|
9213
9570
|
organizations: Number($("#page-size-organizations").value),
|
|
9214
9571
|
timeline: Number($("#page-size-timeline").value),
|
|
9215
9572
|
outlines: Number($("#page-size-outlines").value),
|