@musnows/scriverse 0.5.3 → 0.5.5
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/README.md +4 -3
- package/dist/ai-protocol.js +216 -0
- package/dist/ai-protocol.js.map +1 -0
- package/dist/ai.js +961 -130
- package/dist/ai.js.map +1 -1
- package/dist/app.js +126 -58
- package/dist/app.js.map +1 -1
- package/dist/collaboration-presence.js +18 -7
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/database.js +85 -0
- package/dist/database.js.map +1 -1
- package/dist/public/ai-usage.d.ts +20 -0
- package/dist/public/ai-usage.js +75 -0
- package/dist/public/app.js +1107 -117
- package/dist/public/background-task-center.d.ts +28 -0
- package/dist/public/background-task-center.js +30 -0
- package/dist/public/display-labels.d.ts +1 -0
- package/dist/public/display-labels.js +7 -0
- package/dist/public/index.html +41 -4
- package/dist/public/page-route.js +2 -2
- package/dist/public/race-hierarchy.js +35 -0
- package/dist/public/styles.css +230 -10
- package/dist/public/theme-init.js +2 -2
- package/dist/relationship-search.js +8 -1
- package/dist/relationship-search.js.map +1 -1
- package/dist/store.js +135 -75
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +7 -2
- package/dist/user-auth.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -7,6 +7,7 @@ import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineN
|
|
|
7
7
|
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
|
|
8
8
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
9
9
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
10
|
+
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
10
11
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
11
12
|
import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=20260718-layered-context";
|
|
12
13
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
occurrenceRoleLabel,
|
|
22
23
|
outlineStatusLabel,
|
|
23
24
|
providerConnectionLabel,
|
|
25
|
+
providerProtocolLabel,
|
|
24
26
|
providerStatusLabel,
|
|
25
27
|
relationshipCategoryLabel,
|
|
26
28
|
relationshipConfirmationLabel,
|
|
@@ -31,11 +33,11 @@ import {
|
|
|
31
33
|
taskScopeLabel,
|
|
32
34
|
timelineStatusLabel,
|
|
33
35
|
characterStateFieldLabel
|
|
34
|
-
} from "/display-labels.js?v=20260726-
|
|
35
|
-
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=
|
|
36
|
+
} from "/display-labels.js?v=20260726-anthropic-messages-v2";
|
|
37
|
+
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260727-ai-usage";
|
|
36
38
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
37
39
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
38
|
-
import {
|
|
40
|
+
import { eligibleRaceParents, orderRaceFilterOptions, paginateRaceForest, racePathLabel } from "/race-hierarchy.js?v=20260727-race-tree-pagination-v1";
|
|
39
41
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
40
42
|
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
|
|
41
43
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
@@ -43,6 +45,7 @@ import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global
|
|
|
43
45
|
import { resolveGlobalSearchTarget } from "/global-search.js?v=20260726-search-result-details";
|
|
44
46
|
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
45
47
|
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
48
|
+
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
|
|
46
49
|
import {
|
|
47
50
|
clampCropRect,
|
|
48
51
|
containImageRect,
|
|
@@ -55,7 +58,14 @@ import {
|
|
|
55
58
|
} from "/avatar-crop.js?v=20260725-avatar-crop";
|
|
56
59
|
|
|
57
60
|
const defaultPageSizes = Object.freeze({
|
|
61
|
+
settings: 30,
|
|
58
62
|
characters: 30,
|
|
63
|
+
races: 30,
|
|
64
|
+
organizations: 30,
|
|
65
|
+
timeline: 30,
|
|
66
|
+
outlines: 30,
|
|
67
|
+
relationships: 30,
|
|
68
|
+
reviews: 30,
|
|
59
69
|
analysisTasks: 30,
|
|
60
70
|
fileVersions: 30
|
|
61
71
|
});
|
|
@@ -121,11 +131,18 @@ let presenceHeartbeatQueued = null;
|
|
|
121
131
|
let presenceHeartbeatRequest = 0;
|
|
122
132
|
const acknowledgedCollaborativeChangeIds = new Set();
|
|
123
133
|
let collaborativeChangePromptOpen = false;
|
|
124
|
-
let
|
|
134
|
+
let relationshipPresenceId = null;
|
|
125
135
|
let collaborationAutoSaveDisabled = false;
|
|
126
136
|
|
|
127
137
|
let timelineMultiSelectEnabled = false;
|
|
128
138
|
let taskProgressRefreshTimer = null;
|
|
139
|
+
let relationshipSearchIndexRefreshTimer = null;
|
|
140
|
+
let backgroundTaskCenterTimer = null;
|
|
141
|
+
let backgroundTaskCenterRequest = 0;
|
|
142
|
+
let backgroundTaskCenterWorkId = null;
|
|
143
|
+
let backgroundTaskCenterTasksInitialized = false;
|
|
144
|
+
let backgroundTaskCenterTaskSnapshots = new Map();
|
|
145
|
+
let backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
129
146
|
const taskProgressRefreshInterval = 2_500;
|
|
130
147
|
const taskStatusSnapshots = new Map();
|
|
131
148
|
|
|
@@ -167,6 +184,10 @@ function analysisTaskStatusLabel(status) {
|
|
|
167
184
|
})[String(status)] ?? "未知状态";
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
function canRerunAnalysisTask(task) {
|
|
188
|
+
return ["review", "completed", "partial", "expired", "cancelled"].includes(String(task?.status ?? ""));
|
|
189
|
+
}
|
|
190
|
+
|
|
170
191
|
function normalizedAnalysisTaskStatus(status) {
|
|
171
192
|
const value = String(status);
|
|
172
193
|
return ["pending", "running", "review", "completed", "partial", "expired", "cancelled"].includes(value)
|
|
@@ -264,6 +285,7 @@ function applyWorkAccessMode() {
|
|
|
264
285
|
$("#ai-prompt").readOnly = aiReadOnly;
|
|
265
286
|
$("#ai-prompt").setAttribute("aria-readonly", String(aiReadOnly));
|
|
266
287
|
$("#ai-send").classList.toggle("permission-hidden", aiReadOnly);
|
|
288
|
+
updateBackgroundTaskCenterVisibility();
|
|
267
289
|
if (proseReadOnly) {
|
|
268
290
|
chapterEditorReadOnly = true;
|
|
269
291
|
cancelChapterAutoSave();
|
|
@@ -337,6 +359,7 @@ let workScopedUiGeneration = 0;
|
|
|
337
359
|
let importHistoryRecords = [];
|
|
338
360
|
let importHistoryNextPage = null;
|
|
339
361
|
let importHistoryRequestId = 0;
|
|
362
|
+
let chapterInsightRequestId = 0;
|
|
340
363
|
|
|
341
364
|
const shelfOnboardingSteps = [
|
|
342
365
|
{ selector: "#home-button", eyebrow: "作品入口", title: "这里是你的创作书架", description: "点击左上角的叙界标志,可以随时回到书架,在不同作品之间切换。", placement: "bottom" },
|
|
@@ -537,11 +560,12 @@ function replacePageRoute(route) {
|
|
|
537
560
|
}
|
|
538
561
|
|
|
539
562
|
function presencePageForRoute(route = currentPageRoute()) {
|
|
540
|
-
if (!state.work || route.view === "shelf" || route.view === "platform-ai") return null;
|
|
563
|
+
if (!state.work || route.view === "shelf" || route.view === "platform-ai" || route.view === "platform-usage") return null;
|
|
564
|
+
if (relationshipPresenceId) return { kind: "entity-editor", module: "relationship", resourceId: relationshipPresenceId };
|
|
541
565
|
if (route.view === "editor") return { kind: "editor", resourceId: String(route.chapterId ?? "") || undefined };
|
|
542
566
|
if (route.view === "entity-editor") return { kind: "entity-editor", module: route.entity, resourceId: String(route.entityId ?? "") || undefined };
|
|
543
567
|
if (route.view === "module") return { kind: "module", module: route.module };
|
|
544
|
-
if (route.view === "settings" || route.view === "platform-ai") return { kind: "settings" };
|
|
568
|
+
if (route.view === "settings" || route.view === "platform-ai" || route.view === "platform-usage") return { kind: "settings" };
|
|
545
569
|
return { kind: "welcome" };
|
|
546
570
|
}
|
|
547
571
|
|
|
@@ -627,7 +651,7 @@ async function refreshPresence() {
|
|
|
627
651
|
presenceParticipants = Array.isArray(payload) ? payload : (payload?.participants ?? []);
|
|
628
652
|
renderPresence();
|
|
629
653
|
const recentChanges = Array.isArray(payload) ? [] : (payload?.recentChanges ?? []);
|
|
630
|
-
void
|
|
654
|
+
void handleRelationshipCollaborativeChanges(recentChanges);
|
|
631
655
|
}
|
|
632
656
|
} catch {
|
|
633
657
|
if (state.work?.id === workId) renderPresence();
|
|
@@ -637,14 +661,10 @@ async function refreshPresence() {
|
|
|
637
661
|
return presenceParticipants;
|
|
638
662
|
}
|
|
639
663
|
|
|
640
|
-
function
|
|
641
|
-
return Boolean(state.dirty || entityEditorDirty || characterSectionEditorDirty || knowledgeSectionEditorDirty);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async function handleCollaborativeChanges(recentChanges) {
|
|
664
|
+
async function handleRelationshipCollaborativeChanges(recentChanges) {
|
|
645
665
|
if (!Array.isArray(recentChanges) || !recentChanges.length || collaborativeChangePromptOpen) return;
|
|
646
666
|
const localKey = presencePageKey(presencePageForRoute());
|
|
647
|
-
if (!localKey) return;
|
|
667
|
+
if (!localKey.startsWith("entity-editor:relationship:")) return;
|
|
648
668
|
const selfId = state.user?.userId;
|
|
649
669
|
const incoming = recentChanges.filter((change) => (
|
|
650
670
|
change
|
|
@@ -661,14 +681,10 @@ async function handleCollaborativeChanges(recentChanges) {
|
|
|
661
681
|
const oldest = acknowledgedCollaborativeChangeIds.values().next().value;
|
|
662
682
|
acknowledgedCollaborativeChangeIds.delete(oldest);
|
|
663
683
|
}
|
|
664
|
-
peerPageStale = true;
|
|
665
684
|
collaborativeChangePromptOpen = true;
|
|
666
|
-
const dirtyHint = localEditingDirty()
|
|
667
|
-
? "你当前页面还有未保存修改。请先把正在编辑的内容复制保存到别处(本页无法代为另存),再刷新页面后重新提交。"
|
|
668
|
-
: "请先确认本地没有需要保留的草稿;如有,请复制保存到别处(本页无法代为另存),再刷新页面后继续编辑。";
|
|
669
685
|
try {
|
|
670
|
-
const shouldReload = await confirmToast(`${latest.actorDisplayName || "协作者"}
|
|
671
|
-
title: "
|
|
686
|
+
const shouldReload = await confirmToast(`${latest.actorDisplayName || "协作者"}已更新当前人物关系。请先确认本地没有需要保留的修改,再刷新页面继续查看。`, {
|
|
687
|
+
title: "人物关系已更新",
|
|
672
688
|
confirmLabel: "刷新页面",
|
|
673
689
|
cancelLabel: "稍后处理"
|
|
674
690
|
});
|
|
@@ -678,17 +694,6 @@ async function handleCollaborativeChanges(recentChanges) {
|
|
|
678
694
|
}
|
|
679
695
|
}
|
|
680
696
|
|
|
681
|
-
async function confirmPeerStaleSave() {
|
|
682
|
-
if (!peerPageStale) return true;
|
|
683
|
-
const confirmed = await confirmToast("协作者已更新当前页面。请先把正在编辑的内容复制保存到别处(本页无法代为另存),再刷新页面后重新提交。继续保存可能覆盖对方修改或触发冲突。", {
|
|
684
|
-
title: "页面内容已过期",
|
|
685
|
-
confirmLabel: "仍然保存",
|
|
686
|
-
cancelLabel: "暂不保存"
|
|
687
|
-
});
|
|
688
|
-
if (confirmed) peerPageStale = false;
|
|
689
|
-
return confirmed;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
697
|
function schedulePresenceHeartbeat() {
|
|
693
698
|
if (presenceHeartbeatQueued !== null) clearTimeout(presenceHeartbeatQueued);
|
|
694
699
|
presenceHeartbeatQueued = setTimeout(() => {
|
|
@@ -697,9 +702,15 @@ function schedulePresenceHeartbeat() {
|
|
|
697
702
|
}, 80);
|
|
698
703
|
}
|
|
699
704
|
|
|
705
|
+
function setRelationshipPresence(relationshipId) {
|
|
706
|
+
const nextId = relationshipId ? String(relationshipId) : null;
|
|
707
|
+
if (relationshipPresenceId === nextId) return;
|
|
708
|
+
relationshipPresenceId = nextId;
|
|
709
|
+
void refreshPresence();
|
|
710
|
+
}
|
|
711
|
+
|
|
700
712
|
async function confirmConcurrentSave() {
|
|
701
713
|
await refreshPresence();
|
|
702
|
-
if (!(await confirmPeerStaleSave())) return false;
|
|
703
714
|
const localKey = presencePageKey(presencePageForRoute());
|
|
704
715
|
const peers = presenceParticipants.filter((participant) => participant.clientId !== presenceClientId && participant.page.key === localKey);
|
|
705
716
|
if (!peers.length) return true;
|
|
@@ -719,6 +730,7 @@ function currentPageRoute() {
|
|
|
719
730
|
}
|
|
720
731
|
if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
|
|
721
732
|
if (!$("#platform-ai-view").classList.contains("hidden")) return { view: "platform-ai", workId, ...settingsRouteContext() };
|
|
733
|
+
if (!$("#platform-usage-view").classList.contains("hidden")) return { view: "platform-usage", workId, ...settingsRouteContext() };
|
|
722
734
|
if (!$("#shelf-view").classList.contains("hidden")) return { view: "shelf" };
|
|
723
735
|
if (!workId) return { view: "shelf" };
|
|
724
736
|
if (!$("#editor-view").classList.contains("hidden")) return { view: "editor", workId, chapterId: state.chapter?.id ?? null };
|
|
@@ -842,6 +854,16 @@ let entityEditorReadOnly = false;
|
|
|
842
854
|
let chapterEditorReadOnly = true;
|
|
843
855
|
let characterListPage = 1;
|
|
844
856
|
let taskListPage = 1;
|
|
857
|
+
const moduleListPages = {
|
|
858
|
+
settings: 1,
|
|
859
|
+
races: 1,
|
|
860
|
+
organizations: 1,
|
|
861
|
+
timeline: 1,
|
|
862
|
+
outlinePlans: 1,
|
|
863
|
+
foreshadows: 1,
|
|
864
|
+
relationships: 1,
|
|
865
|
+
reviews: 1
|
|
866
|
+
};
|
|
845
867
|
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
846
868
|
let characterFiltersPanelOpen = false;
|
|
847
869
|
const relationshipFilters = { fromCharacterIds: [], toCharacterIds: [] };
|
|
@@ -2076,6 +2098,40 @@ function pageSizeFor(module) {
|
|
|
2076
2098
|
return normalizePageSize(state.uiSettings.pageSizes[module], defaultPageSizes[module] ?? 30);
|
|
2077
2099
|
}
|
|
2078
2100
|
|
|
2101
|
+
function paginateModuleItems(items, page, sizeKey) {
|
|
2102
|
+
const limit = pageSizeFor(sizeKey);
|
|
2103
|
+
const total = items.length;
|
|
2104
|
+
const pageCount = Math.max(1, Math.ceil(total / limit));
|
|
2105
|
+
const safePage = Math.min(Math.max(1, Number(page) || 1), pageCount);
|
|
2106
|
+
const start = (safePage - 1) * limit;
|
|
2107
|
+
return {
|
|
2108
|
+
items: items.slice(start, start + limit),
|
|
2109
|
+
page: safePage,
|
|
2110
|
+
limit,
|
|
2111
|
+
total,
|
|
2112
|
+
pageCount,
|
|
2113
|
+
hasMore: safePage < pageCount,
|
|
2114
|
+
nextPage: safePage < pageCount ? safePage + 1 : null
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function renderModulePagination(pageResult, pageKey, label) {
|
|
2119
|
+
if (pageResult.pageCount <= 1) return "";
|
|
2120
|
+
return `<nav class="module-pagination" aria-label="${esc(label)}分页">
|
|
2121
|
+
<button type="button" data-module-page-key="${esc(pageKey)}" data-module-page="${pageResult.page - 1}" ${pageResult.page <= 1 ? "disabled" : ""}>上一页</button>
|
|
2122
|
+
<span>第 ${pageResult.page}/${pageResult.pageCount} 页 · 本页 ${pageResult.itemCount ?? pageResult.items.length} 条 · 共 ${pageResult.total} 条</span>
|
|
2123
|
+
<button type="button" data-module-page-key="${esc(pageKey)}" data-module-page="${pageResult.nextPage ?? pageResult.page + 1}" ${pageResult.hasMore ? "" : "disabled"}>下一页</button>
|
|
2124
|
+
</nav>`;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function bindModulePagination(pageKey, renderPage) {
|
|
2128
|
+
$("#module-content").querySelectorAll(`[data-module-page-key="${CSS.escape(pageKey)}"]`).forEach((button) => button.addEventListener("click", async () => {
|
|
2129
|
+
if (button.disabled) return;
|
|
2130
|
+
$("#module-content").querySelectorAll(`[data-module-page-key="${CSS.escape(pageKey)}"]`).forEach((control) => { control.disabled = true; });
|
|
2131
|
+
await renderPage(Number(button.dataset.modulePage));
|
|
2132
|
+
}));
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2079
2135
|
async function loadPlatformUiSettings() {
|
|
2080
2136
|
try {
|
|
2081
2137
|
applyPlatformUiSettings(await api("/api/ui-settings"));
|
|
@@ -2109,6 +2165,16 @@ function raiseToastRegion() {
|
|
|
2109
2165
|
region.showPopover();
|
|
2110
2166
|
}
|
|
2111
2167
|
|
|
2168
|
+
function dismissChapterInsightToast() {
|
|
2169
|
+
chapterInsightRequestId += 1;
|
|
2170
|
+
const region = $("#toast-region");
|
|
2171
|
+
region.querySelector(".chapter-insight-toast")?.remove();
|
|
2172
|
+
$("#insight-button").setAttribute("aria-expanded", "false");
|
|
2173
|
+
if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
|
|
2174
|
+
region.hidePopover();
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2112
2178
|
function toast(message, type = "info") {
|
|
2113
2179
|
const region = $("#toast-region");
|
|
2114
2180
|
const element = document.createElement("div");
|
|
@@ -2466,6 +2532,11 @@ async function initializePage() {
|
|
|
2466
2532
|
if (route.view === "platform-ai") {
|
|
2467
2533
|
await showPlatformAi();
|
|
2468
2534
|
settingsReturnContext = restoredSettingsReturnContext(route);
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (route.view === "platform-usage") {
|
|
2538
|
+
await showPlatformUsage();
|
|
2539
|
+
settingsReturnContext = restoredSettingsReturnContext(route);
|
|
2469
2540
|
}
|
|
2470
2541
|
} finally {
|
|
2471
2542
|
document.body.classList.remove("auth-pending");
|
|
@@ -2476,12 +2547,15 @@ async function initializePage() {
|
|
|
2476
2547
|
}
|
|
2477
2548
|
|
|
2478
2549
|
function showShelf() {
|
|
2550
|
+
stopBackgroundTaskCenter();
|
|
2551
|
+
dismissChapterInsightToast();
|
|
2479
2552
|
state.dirty = false;
|
|
2480
2553
|
settingsReturnContext = null;
|
|
2481
2554
|
updateDocumentTitle();
|
|
2482
2555
|
$("#app").classList.add("shelf-mode");
|
|
2483
2556
|
$("#shelf-view").classList.remove("hidden");
|
|
2484
2557
|
$("#platform-ai-view").classList.add("hidden");
|
|
2558
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2485
2559
|
$("#settings-hub-view").classList.add("hidden");
|
|
2486
2560
|
$("#welcome-view").classList.add("hidden");
|
|
2487
2561
|
$("#editor-view").classList.add("hidden");
|
|
@@ -2508,6 +2582,7 @@ function renderSettingsHub() {
|
|
|
2508
2582
|
const canReadAggregate = hasWork && canReadAggregateContent();
|
|
2509
2583
|
const isAdmin = state.user?.role === "admin";
|
|
2510
2584
|
$("#platform-ai-button").classList.toggle("hidden", !isAdmin);
|
|
2585
|
+
$("#platform-usage-button").classList.toggle("hidden", !isAdmin);
|
|
2511
2586
|
$("#user-management-button").classList.toggle("hidden", !isAdmin);
|
|
2512
2587
|
$("#platform-ui-settings-button").classList.toggle("hidden", !isAdmin);
|
|
2513
2588
|
$("#collaboration-button").disabled = !canManageWork;
|
|
@@ -2569,7 +2644,14 @@ async function openPlatformUiSettingsDialog() {
|
|
|
2569
2644
|
const settings = await api("/api/platform/ui-settings");
|
|
2570
2645
|
$("#toast-position").value = settings.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2571
2646
|
const pageSizes = normalizePageSizes(settings.pageSizes);
|
|
2647
|
+
$("#page-size-settings").value = String(pageSizes.settings);
|
|
2572
2648
|
$("#page-size-characters").value = String(pageSizes.characters);
|
|
2649
|
+
$("#page-size-races").value = String(pageSizes.races);
|
|
2650
|
+
$("#page-size-organizations").value = String(pageSizes.organizations);
|
|
2651
|
+
$("#page-size-timeline").value = String(pageSizes.timeline);
|
|
2652
|
+
$("#page-size-outlines").value = String(pageSizes.outlines);
|
|
2653
|
+
$("#page-size-relationships").value = String(pageSizes.relationships);
|
|
2654
|
+
$("#page-size-reviews").value = String(pageSizes.reviews);
|
|
2573
2655
|
$("#page-size-analysis-tasks").value = String(pageSizes.analysisTasks);
|
|
2574
2656
|
$("#page-size-file-versions").value = String(pageSizes.fileVersions);
|
|
2575
2657
|
$("#platform-ui-settings-dialog").showModal();
|
|
@@ -2726,7 +2808,9 @@ async function openSearchResult(result) {
|
|
|
2726
2808
|
const target = resolveGlobalSearchTarget(result);
|
|
2727
2809
|
if (!target) throw new Error("无法打开该搜索结果");
|
|
2728
2810
|
$("#search-dialog").close();
|
|
2729
|
-
const inSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2811
|
+
const inSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2812
|
+
|| !$("#platform-ai-view").classList.contains("hidden")
|
|
2813
|
+
|| !$("#platform-usage-view").classList.contains("hidden");
|
|
2730
2814
|
if (inSettings) await returnFromSettings();
|
|
2731
2815
|
if (target.kind === "chapter") {
|
|
2732
2816
|
await selectChapter(target.id);
|
|
@@ -2742,16 +2826,20 @@ async function openSearchResult(result) {
|
|
|
2742
2826
|
}
|
|
2743
2827
|
|
|
2744
2828
|
async function showSettingsHub() {
|
|
2745
|
-
const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2829
|
+
const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2830
|
+
|| !$("#platform-ai-view").classList.contains("hidden")
|
|
2831
|
+
|| !$("#platform-usage-view").classList.contains("hidden");
|
|
2746
2832
|
if (!alreadyInSettings) {
|
|
2747
2833
|
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?"))) return false;
|
|
2748
2834
|
settingsReturnContext = captureSettingsReturnContext();
|
|
2749
2835
|
state.dirty = false;
|
|
2750
2836
|
}
|
|
2837
|
+
dismissChapterInsightToast();
|
|
2751
2838
|
updateDocumentTitle(state.work);
|
|
2752
2839
|
$("#app").classList.add("shelf-mode");
|
|
2753
2840
|
$("#shelf-view").classList.add("hidden");
|
|
2754
2841
|
$("#platform-ai-view").classList.add("hidden");
|
|
2842
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2755
2843
|
$("#settings-hub-view").classList.remove("hidden");
|
|
2756
2844
|
$("#welcome-view").classList.add("hidden");
|
|
2757
2845
|
$("#editor-view").classList.add("hidden");
|
|
@@ -2777,6 +2865,7 @@ async function returnFromSettings() {
|
|
|
2777
2865
|
$("#settings-button").removeAttribute("aria-current");
|
|
2778
2866
|
$("#settings-hub-view").classList.add("hidden");
|
|
2779
2867
|
$("#platform-ai-view").classList.add("hidden");
|
|
2868
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2780
2869
|
if (context.view === "shelf" || !state.work) return showShelf();
|
|
2781
2870
|
$("#app").classList.remove("shelf-mode");
|
|
2782
2871
|
$("#shelf-view").classList.add("hidden");
|
|
@@ -2790,10 +2879,12 @@ async function returnFromSettings() {
|
|
|
2790
2879
|
async function showPlatformAi() {
|
|
2791
2880
|
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
|
|
2792
2881
|
state.dirty = false;
|
|
2882
|
+
dismissChapterInsightToast();
|
|
2793
2883
|
updateDocumentTitle();
|
|
2794
2884
|
$("#app").classList.add("shelf-mode");
|
|
2795
2885
|
$("#shelf-view").classList.add("hidden");
|
|
2796
2886
|
$("#platform-ai-view").classList.remove("hidden");
|
|
2887
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2797
2888
|
$("#settings-hub-view").classList.add("hidden");
|
|
2798
2889
|
$("#welcome-view").classList.add("hidden");
|
|
2799
2890
|
$("#editor-view").classList.add("hidden");
|
|
@@ -2806,6 +2897,26 @@ async function showPlatformAi() {
|
|
|
2806
2897
|
return true;
|
|
2807
2898
|
}
|
|
2808
2899
|
|
|
2900
|
+
async function showPlatformUsage() {
|
|
2901
|
+
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入 Token 用量面板将放弃本地修改。是否继续?"))) return false;
|
|
2902
|
+
state.dirty = false;
|
|
2903
|
+
updateDocumentTitle();
|
|
2904
|
+
$("#app").classList.add("shelf-mode");
|
|
2905
|
+
$("#shelf-view").classList.add("hidden");
|
|
2906
|
+
$("#platform-ai-view").classList.add("hidden");
|
|
2907
|
+
$("#platform-usage-view").classList.remove("hidden");
|
|
2908
|
+
$("#settings-hub-view").classList.add("hidden");
|
|
2909
|
+
$("#welcome-view").classList.add("hidden");
|
|
2910
|
+
$("#editor-view").classList.add("hidden");
|
|
2911
|
+
$("#module-view").classList.add("hidden");
|
|
2912
|
+
$("#work-meta").textContent = "Token 用量";
|
|
2913
|
+
$("#settings-button").setAttribute("aria-current", "page");
|
|
2914
|
+
setSaveState("Token 用量");
|
|
2915
|
+
await renderPlatformTokenUsage();
|
|
2916
|
+
replacePageRoute({ view: "platform-usage", workId: state.work?.id ?? null, ...settingsRouteContext() });
|
|
2917
|
+
return true;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2809
2920
|
function renderShelf() {
|
|
2810
2921
|
const shelf = $("#book-shelf");
|
|
2811
2922
|
shelf.innerHTML = `${state.works.map((work) => `
|
|
@@ -2829,6 +2940,7 @@ function renderShelf() {
|
|
|
2829
2940
|
}
|
|
2830
2941
|
|
|
2831
2942
|
function resetWorkScopedUiCaches() {
|
|
2943
|
+
stopBackgroundTaskCenter();
|
|
2832
2944
|
workScopedUiGeneration += 1;
|
|
2833
2945
|
loadedAiModelsWorkId = null;
|
|
2834
2946
|
loadedAiReferencesWorkId = null;
|
|
@@ -2843,6 +2955,7 @@ function resetWorkScopedUiCaches() {
|
|
|
2843
2955
|
state.characters = [];
|
|
2844
2956
|
state.settings = [];
|
|
2845
2957
|
characterListPage = 1;
|
|
2958
|
+
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
2846
2959
|
relationshipFilters.fromCharacterIds = [];
|
|
2847
2960
|
relationshipFilters.toCharacterIds = [];
|
|
2848
2961
|
taskListPage = 1;
|
|
@@ -2876,6 +2989,7 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2876
2989
|
$("#app").classList.remove("shelf-mode");
|
|
2877
2990
|
$("#shelf-view").classList.add("hidden");
|
|
2878
2991
|
$("#platform-ai-view").classList.add("hidden");
|
|
2992
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2879
2993
|
$("#settings-hub-view").classList.add("hidden");
|
|
2880
2994
|
$("#settings-button").removeAttribute("aria-current");
|
|
2881
2995
|
settingsReturnContext = null;
|
|
@@ -2884,6 +2998,7 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2884
2998
|
chapterEditorReadOnly = true;
|
|
2885
2999
|
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
2886
3000
|
applyWorkAccessMode();
|
|
3001
|
+
startBackgroundTaskCenter(nextWork.id);
|
|
2887
3002
|
updateDocumentTitle(state.work);
|
|
2888
3003
|
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
2889
3004
|
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
@@ -2992,7 +3107,7 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
2992
3107
|
$("#chapter-content").value = normalizedContent;
|
|
2993
3108
|
clearChapterLineSelection();
|
|
2994
3109
|
scheduleChapterLineNumbers();
|
|
2995
|
-
|
|
3110
|
+
dismissChapterInsightToast();
|
|
2996
3111
|
updateChapterStats();
|
|
2997
3112
|
if (!canEditProse()) setSaveState("正文只读");
|
|
2998
3113
|
else if (chapterEditorReadOnly) setSaveState("阅读模式");
|
|
@@ -3027,6 +3142,7 @@ function tidyChapterBlankLines() {
|
|
|
3027
3142
|
}
|
|
3028
3143
|
|
|
3029
3144
|
function showWelcome(hasWork = false) {
|
|
3145
|
+
dismissChapterInsightToast();
|
|
3030
3146
|
$("#editor-view").classList.add("hidden");
|
|
3031
3147
|
$("#module-view").classList.add("hidden");
|
|
3032
3148
|
$("#welcome-view").classList.remove("hidden");
|
|
@@ -3077,6 +3193,7 @@ async function showModule(module) {
|
|
|
3077
3193
|
}
|
|
3078
3194
|
return;
|
|
3079
3195
|
}
|
|
3196
|
+
dismissChapterInsightToast();
|
|
3080
3197
|
$("#welcome-view").classList.add("hidden");
|
|
3081
3198
|
$("#editor-view").classList.add("hidden");
|
|
3082
3199
|
$("#module-view").classList.remove("hidden");
|
|
@@ -3507,19 +3624,22 @@ function renderSettingRows(records) {
|
|
|
3507
3624
|
}).join("")}</div>`;
|
|
3508
3625
|
}
|
|
3509
3626
|
|
|
3510
|
-
async function renderSettings() {
|
|
3627
|
+
async function renderSettings(page = moduleListPages.settings) {
|
|
3511
3628
|
const records = await apiAllPages(`/api/works/${state.work.id}/settings`);
|
|
3512
3629
|
state.settings = records;
|
|
3513
3630
|
mountModuleCount(records.length);
|
|
3631
|
+
const pageResult = paginateModuleItems(records, page, "settings");
|
|
3632
|
+
moduleListPages.settings = pageResult.page;
|
|
3514
3633
|
const layout = readModuleLayout();
|
|
3515
3634
|
if (records.length) mountModuleLayoutToggle(layout, "设定列表样式");
|
|
3516
3635
|
$("#module-content").innerHTML = records.length
|
|
3517
|
-
? `${layout === "rows" ? renderSettingRows(
|
|
3636
|
+
? `${layout === "rows" ? renderSettingRows(pageResult.items) : renderSettingCards(pageResult.items)}${renderModulePagination(pageResult, "settings", "设定库")}`
|
|
3518
3637
|
: emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
|
|
3519
|
-
bindModuleLayoutToggle(renderSettings);
|
|
3638
|
+
bindModuleLayoutToggle(() => renderSettings(pageResult.page));
|
|
3639
|
+
bindModulePagination("settings", renderSettings);
|
|
3520
3640
|
const openSetting = async (id, readOnly) => openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly });
|
|
3521
3641
|
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => { void openSetting(button.dataset.editSetting, false); }));
|
|
3522
|
-
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
3642
|
+
bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
|
|
3523
3643
|
}
|
|
3524
3644
|
|
|
3525
3645
|
async function renderCharacters(page = characterListPage) {
|
|
@@ -3535,14 +3655,15 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3535
3655
|
: characterSource;
|
|
3536
3656
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3537
3657
|
characterListPage = characterPage.page;
|
|
3538
|
-
|
|
3658
|
+
const pageCharacters = characterPage.items;
|
|
3659
|
+
[state.races, state.organizations] = [races, organizations];
|
|
3539
3660
|
mountModuleCount(characterPage.total);
|
|
3540
3661
|
const layout = readModuleLayout();
|
|
3541
3662
|
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
|
|
3542
3663
|
const characterLockBadge = (item) => item.lockedFields.length
|
|
3543
3664
|
? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
|
|
3544
3665
|
: "";
|
|
3545
|
-
const characterCards = () => `<div class="card-grid">${
|
|
3666
|
+
const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
|
|
3546
3667
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
3547
3668
|
return `
|
|
3548
3669
|
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
@@ -3557,7 +3678,7 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3557
3678
|
${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
|
|
3558
3679
|
</article>`;
|
|
3559
3680
|
}).join("")}</div>`;
|
|
3560
|
-
const characterRows = () => `<div class="module-row-list">${
|
|
3681
|
+
const characterRows = () => `<div class="module-row-list">${pageCharacters.map((item) => {
|
|
3561
3682
|
const preview = moduleRowPreview(item.profile?.summary || item.attributes?.identity || Object.entries(item.currentState).map(([key, value]) => `${characterStateFieldLabel(key)}:${value}`).join(" ") || "尚未记录当前状态");
|
|
3562
3683
|
const meta = [
|
|
3563
3684
|
item.code ? `编号 ${item.code}` : "",
|
|
@@ -3573,10 +3694,10 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3573
3694
|
<div class="card-actions">${characterActions(item)}</div>
|
|
3574
3695
|
</article>`;
|
|
3575
3696
|
}).join("")}</div>`;
|
|
3576
|
-
const pagination =
|
|
3697
|
+
const pagination = pageCharacters.length && (characterPage.page > 1 || characterPage.hasMore)
|
|
3577
3698
|
? `<nav class="module-pagination" aria-label="角色列表分页">
|
|
3578
3699
|
<button type="button" data-character-page="${characterPage.page - 1}" ${characterPage.page <= 1 ? "disabled" : ""}>上一页</button>
|
|
3579
|
-
<span>第 ${characterPage.page}/${Math.ceil(characterPage.total / characterPage.limit)} 页 · 本页 ${
|
|
3700
|
+
<span>第 ${characterPage.page}/${Math.ceil(characterPage.total / characterPage.limit)} 页 · 本页 ${pageCharacters.length} 个角色 · 共 ${characterPage.total} 个角色</span>
|
|
3580
3701
|
<button type="button" data-character-page="${characterPage.nextPage ?? characterPage.page + 1}" ${characterPage.hasMore ? "" : "disabled"}>下一页</button>
|
|
3581
3702
|
</nav>`
|
|
3582
3703
|
: "";
|
|
@@ -3595,8 +3716,8 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3595
3716
|
<div class="character-filter-toolbar-actions">${hasCharacterFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${characterPage.total} 个角色</span>` : ""}<button id="clear-character-filters" class="ghost-button" type="button" ${hasCharacterFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
3596
3717
|
</section>`;
|
|
3597
3718
|
mountCharacterFilterToggle();
|
|
3598
|
-
if (
|
|
3599
|
-
$("#module-content").innerHTML = filterToolbar + (
|
|
3719
|
+
if (pageCharacters.length) mountModuleLayoutToggle(layout, "角色列表样式");
|
|
3720
|
+
$("#module-content").innerHTML = filterToolbar + (pageCharacters.length
|
|
3600
3721
|
? `${layout === "rows" ? characterRows() : characterCards()}${pagination}`
|
|
3601
3722
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
3602
3723
|
bindModuleLayoutToggle(renderCharacters);
|
|
@@ -3625,14 +3746,18 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3625
3746
|
$("#module-content").querySelectorAll("[data-character-page]").forEach((control) => { control.disabled = true; });
|
|
3626
3747
|
await renderCharacters(Number(button.dataset.characterPage));
|
|
3627
3748
|
}));
|
|
3628
|
-
bindRecordPreview("[data-open-character]", (id) => openCharacterEditor(
|
|
3629
|
-
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(
|
|
3749
|
+
bindRecordPreview("[data-open-character]", (id) => openCharacterEditor(pageCharacters.find((item) => item.id === id), { readOnly: true }));
|
|
3750
|
+
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(pageCharacters.find((item) => item.id === button.dataset.editCharacter))));
|
|
3630
3751
|
}
|
|
3631
3752
|
|
|
3632
|
-
async function renderRaces() {
|
|
3753
|
+
async function renderRaces(page = moduleListPages.races) {
|
|
3633
3754
|
state.races = await apiAllPages(`/api/works/${state.work.id}/races`);
|
|
3634
3755
|
mountModuleCount(state.races.length);
|
|
3635
3756
|
const layout = readModuleLayout();
|
|
3757
|
+
const pageResult = layout === "rows"
|
|
3758
|
+
? paginateModuleItems(state.races, page, "races")
|
|
3759
|
+
: paginateRaceForest(state.races, page, pageSizeFor("races"));
|
|
3760
|
+
moduleListPages.races = pageResult.page;
|
|
3636
3761
|
const canEditRaces = canEditModule("races");
|
|
3637
3762
|
const raceActions = (item) => canEditRaces
|
|
3638
3763
|
? recordCardEditButton("edit-race", item.id, `种族“${item.name}”`)
|
|
@@ -3653,7 +3778,7 @@ async function renderRaces() {
|
|
|
3653
3778
|
${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
|
|
3654
3779
|
</div>
|
|
3655
3780
|
</details>`;
|
|
3656
|
-
const raceRows = () => `<div class="module-row-list">${
|
|
3781
|
+
const raceRows = () => `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
3657
3782
|
const preview = moduleRowPreview(item.description || "尚未填写种族简介");
|
|
3658
3783
|
const meta = `${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
|
|
3659
3784
|
return `
|
|
@@ -3667,22 +3792,25 @@ async function renderRaces() {
|
|
|
3667
3792
|
if (state.races.length) mountModuleLayoutToggle(layout, "种族列表样式");
|
|
3668
3793
|
if (state.races.length && layout !== "rows") mountRaceTreeExpandToggle();
|
|
3669
3794
|
$("#module-content").innerHTML = state.races.length
|
|
3670
|
-
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${
|
|
3795
|
+
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${pageResult.items.map(renderRaceNode).join("")}</section>`}${renderModulePagination(pageResult, "races", "种族列表")}`
|
|
3671
3796
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
3672
|
-
bindModuleLayoutToggle(renderRaces);
|
|
3797
|
+
bindModuleLayoutToggle(() => renderRaces(pageResult.page));
|
|
3798
|
+
bindModulePagination("races", renderRaces);
|
|
3673
3799
|
bindRaceTreeExpandToggle();
|
|
3674
3800
|
bindRaceTreeNodeToggles();
|
|
3675
3801
|
const openRace = async (id, readOnly) => openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly });
|
|
3676
3802
|
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => { void openRace(button.dataset.editRace, false); }));
|
|
3677
|
-
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
3803
|
+
bindEntityHistoryButtons(async () => { await renderRaces(pageResult.page); await loadAiReferences(); });
|
|
3678
3804
|
}
|
|
3679
3805
|
|
|
3680
|
-
async function renderOrganizations() {
|
|
3806
|
+
async function renderOrganizations(page = moduleListPages.organizations) {
|
|
3681
3807
|
[state.organizations, state.characters] = await Promise.all([
|
|
3682
3808
|
apiAllPages(`/api/works/${state.work.id}/organizations`),
|
|
3683
3809
|
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
3684
3810
|
]);
|
|
3685
3811
|
mountModuleCount(state.organizations.length);
|
|
3812
|
+
const pageResult = paginateModuleItems(state.organizations, page, "organizations");
|
|
3813
|
+
moduleListPages.organizations = pageResult.page;
|
|
3686
3814
|
const layout = readModuleLayout();
|
|
3687
3815
|
const canEditOrganizations = canEditModule("organizations");
|
|
3688
3816
|
const organizationActions = (item) => canEditOrganizations
|
|
@@ -3691,14 +3819,14 @@ async function renderOrganizations() {
|
|
|
3691
3819
|
const organizationCardActions = (item) => canEditOrganizations
|
|
3692
3820
|
? organizationActions(item)
|
|
3693
3821
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
3694
|
-
const organizationCards = () => `<div class="card-grid organization-grid">${
|
|
3822
|
+
const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
|
|
3695
3823
|
<article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
3696
3824
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
3697
3825
|
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
3698
3826
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3699
3827
|
${organizationCardActions(item)}
|
|
3700
3828
|
</article>`).join("")}</div>`;
|
|
3701
|
-
const organizationRows = () => `<div class="module-row-list">${
|
|
3829
|
+
const organizationRows = () => `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
3702
3830
|
const preview = moduleRowPreview(item.description || "尚未填写组织简介");
|
|
3703
3831
|
const members = item.members.length ? item.members.map((member) => member.name).join("、") : "暂无绑定角色";
|
|
3704
3832
|
return `
|
|
@@ -3711,12 +3839,13 @@ async function renderOrganizations() {
|
|
|
3711
3839
|
}).join("")}</div>`;
|
|
3712
3840
|
if (state.organizations.length) mountModuleLayoutToggle(layout, "组织列表样式");
|
|
3713
3841
|
$("#module-content").innerHTML = state.organizations.length
|
|
3714
|
-
? `${layout === "rows" ? organizationRows() : organizationCards()}`
|
|
3842
|
+
? `${layout === "rows" ? organizationRows() : organizationCards()}${renderModulePagination(pageResult, "organizations", "组织列表")}`
|
|
3715
3843
|
: emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
3716
|
-
bindModuleLayoutToggle(renderOrganizations);
|
|
3844
|
+
bindModuleLayoutToggle(() => renderOrganizations(pageResult.page));
|
|
3845
|
+
bindModulePagination("organizations", renderOrganizations);
|
|
3717
3846
|
const openOrganization = async (id, readOnly) => openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly });
|
|
3718
3847
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => { void openOrganization(button.dataset.editOrganization, false); }));
|
|
3719
|
-
bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
|
|
3848
|
+
bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
|
|
3720
3849
|
}
|
|
3721
3850
|
|
|
3722
3851
|
function updateTimelineMultiSelectControls() {
|
|
@@ -3741,12 +3870,14 @@ function setTimelineMultiSelectMode(enabled) {
|
|
|
3741
3870
|
updateTimelineMultiSelectControls();
|
|
3742
3871
|
}
|
|
3743
3872
|
|
|
3744
|
-
async function renderTimeline() {
|
|
3873
|
+
async function renderTimeline(page = moduleListPages.timeline) {
|
|
3745
3874
|
const [events, tracks] = await Promise.all([
|
|
3746
3875
|
apiAllPages(`/api/works/${state.work.id}/timeline`),
|
|
3747
3876
|
apiAllPages(`/api/works/${state.work.id}/timeline-tracks`)
|
|
3748
3877
|
]);
|
|
3749
3878
|
mountModuleCount(events.length);
|
|
3879
|
+
const pageResult = paginateModuleItems(events, page, "timeline");
|
|
3880
|
+
moduleListPages.timeline = pageResult.page;
|
|
3750
3881
|
timelineMultiSelectEnabled = false;
|
|
3751
3882
|
$("#timeline-tools")?.remove();
|
|
3752
3883
|
$("#module-header-actions").insertAdjacentHTML("beforeend", `<div id="timeline-tools" class="timeline-tools" data-module-header-action="timeline-tools" role="group" aria-label="时间轴操作"><button id="create-timeline-track" class="ghost-button" type="button">新建独立时间轴</button><button id="timeline-multi-select-toggle" class="ghost-button" type="button" aria-pressed="false">多选</button>${events.length > 1 ? '<button id="merge-events" class="ghost-button" type="button" hidden>合并所选事件</button>' : ""}</div>`);
|
|
@@ -3754,9 +3885,10 @@ async function renderTimeline() {
|
|
|
3754
3885
|
const lanes = [...tracks, { id: "", name: "未分组时间轴", description: "尚未归入独立大事件的时间节点。", sortOrder: Number.MAX_SAFE_INTEGER }];
|
|
3755
3886
|
const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(timelineStatusLabel(item.status))}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
|
|
3756
3887
|
$("#module-content").innerHTML = `<div class="timeline-kanban" data-testid="timeline-kanban">${lanes.map((track) => {
|
|
3757
|
-
const laneEvents =
|
|
3888
|
+
const laneEvents = pageResult.items.filter((item) => (item.trackId ?? "") === track.id);
|
|
3758
3889
|
return `<section class="timeline-lane" data-track-id="${esc(track.id)}"><header><div><small>${laneEvents.length} 个节点</small><h3>${esc(track.name)}</h3></div>${track.id ? `<div class="timeline-track-actions"><button class="timeline-track-menu" data-edit-timeline-track="${esc(track.id)}" type="button">编辑</button><button class="timeline-track-menu" data-entity-history="timeline-track" data-entity-id="${esc(track.id)}" data-entity-title="${esc(track.name)}" type="button">历史</button></div>` : ""}</header><p class="timeline-track-description">${esc(track.description || "暂无说明")}</p><div class="timeline-lane-events">${laneEvents.map(eventCard).join("") || '<div class="timeline-lane-empty">还没有时间节点</div>'}</div><button class="timeline-add-event" data-add-event-track="${esc(track.id)}" type="button">添加事件</button></section>`;
|
|
3759
|
-
}).join("")}</div
|
|
3890
|
+
}).join("")}</div>${renderModulePagination(pageResult, "timeline", "时间线事件")}`;
|
|
3891
|
+
bindModulePagination("timeline", renderTimeline);
|
|
3760
3892
|
$("#create-timeline-track").addEventListener("click", () => openTimelineTrackDialog());
|
|
3761
3893
|
$("#timeline-multi-select-toggle").addEventListener("click", () => setTimelineMultiSelectMode(!timelineMultiSelectEnabled));
|
|
3762
3894
|
$("#module-content").querySelectorAll("[data-event-select]").forEach((input) => input.addEventListener("change", updateTimelineMultiSelectControls));
|
|
@@ -3765,7 +3897,7 @@ async function renderTimeline() {
|
|
|
3765
3897
|
$("#module-content").querySelectorAll("[data-add-event-track]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(null, button.dataset.addEventTrack || null)));
|
|
3766
3898
|
$("#module-content").querySelectorAll("[data-edit-event]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(events.find((item) => item.id === button.dataset.editEvent))));
|
|
3767
3899
|
$("#module-content").querySelectorAll("[data-split-event]").forEach((button) => button.addEventListener("click", () => openTimelineSplitDialog(events.find((item) => item.id === button.dataset.splitEvent))));
|
|
3768
|
-
bindEntityHistoryButtons(renderTimeline);
|
|
3900
|
+
bindEntityHistoryButtons(() => renderTimeline(pageResult.page));
|
|
3769
3901
|
$("#merge-events")?.addEventListener("click", () => {
|
|
3770
3902
|
const eventIds = [...$("#module-content").querySelectorAll("[data-event-select]:checked")].map((input) => input.dataset.eventSelect);
|
|
3771
3903
|
if (eventIds.length < 2) return toast("请至少选择两个时间事件", "error");
|
|
@@ -3776,30 +3908,34 @@ async function renderTimeline() {
|
|
|
3776
3908
|
description: form.get("description") || undefined,
|
|
3777
3909
|
expectedVersionNos: Object.fromEntries(eventIds.map((eventId) => [eventId, Number(events.find((event) => event.id === eventId)?.versionNo)]))
|
|
3778
3910
|
} });
|
|
3779
|
-
await renderTimeline();
|
|
3911
|
+
await renderTimeline(pageResult.page);
|
|
3780
3912
|
}, "保留参与者与证据");
|
|
3781
3913
|
});
|
|
3782
3914
|
}
|
|
3783
3915
|
|
|
3784
|
-
async function renderOutlines() {
|
|
3916
|
+
async function renderOutlines(outlinePage = moduleListPages.outlinePlans, foreshadowPage = moduleListPages.foreshadows) {
|
|
3785
3917
|
const currentChapterId = state.chapter?.id;
|
|
3786
3918
|
const [outlines, foreshadows] = await Promise.all([
|
|
3787
3919
|
apiAllPages(`/api/works/${state.work.id}/outlines`),
|
|
3788
3920
|
apiAllPages(`/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `¤tChapterId=${encodeURIComponent(currentChapterId)}` : ""}`)
|
|
3789
3921
|
]);
|
|
3790
3922
|
mountModuleCount(outlines.length + foreshadows.length);
|
|
3923
|
+
const outlinePageResult = paginateModuleItems(outlines, outlinePage, "outlines");
|
|
3924
|
+
const foreshadowPageResult = paginateModuleItems(foreshadows, foreshadowPage, "outlines");
|
|
3925
|
+
moduleListPages.outlinePlans = outlinePageResult.page;
|
|
3926
|
+
moduleListPages.foreshadows = foreshadowPageResult.page;
|
|
3791
3927
|
const layout = readModuleLayout();
|
|
3792
3928
|
const unresolved = foreshadows.filter((item) => item.unresolved);
|
|
3793
3929
|
const overdue = unresolved.filter((item) => item.overdue);
|
|
3794
3930
|
const foreshadowActions = (item) => `<button data-edit-foreshadow="${esc(item.id)}">编辑伏笔</button><button data-entity-history="foreshadow" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button>`;
|
|
3795
|
-
const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${
|
|
3931
|
+
const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${foreshadowPageResult.items.map((item) => `
|
|
3796
3932
|
<article class="record-card foreshadow-card ${item.overdue ? "is-overdue" : ""}">
|
|
3797
3933
|
<small>${esc(levelLabel(item.importance))} · ${esc(foreshadowStatusLabel(item.status))}${item.overdue ? " · 已逾期" : ""}</small>
|
|
3798
3934
|
<h3>${esc(item.title)}</h3><p>${esc(item.description || "暂无说明")}</p>
|
|
3799
3935
|
<div class="foreshadow-links">${item.occurrences.length ? item.occurrences.map((link) => `<span class="pill">${esc(occurrenceRoleLabel(link.role))} · ${esc(link.volumeTitle)} / ${esc(link.chapterTitle)}</span>`).join("") : '<span class="pill">尚未关联章节</span>'}</div>
|
|
3800
3936
|
<div class="card-actions">${foreshadowActions(item)}</div>
|
|
3801
3937
|
</article>`).join("")}</div>`;
|
|
3802
|
-
const foreshadowRows = () => `<div class="module-row-list">${
|
|
3938
|
+
const foreshadowRows = () => `<div class="module-row-list">${foreshadowPageResult.items.map((item) => {
|
|
3803
3939
|
const preview = moduleRowPreview(item.description || "暂无说明");
|
|
3804
3940
|
const links = item.occurrences.length
|
|
3805
3941
|
? item.occurrences.map((link) => `${occurrenceRoleLabel(link.role)} · ${link.volumeTitle} / ${link.chapterTitle}`).join(";")
|
|
@@ -3813,28 +3949,32 @@ async function renderOutlines() {
|
|
|
3813
3949
|
</article>`;
|
|
3814
3950
|
}).join("")}</div>`;
|
|
3815
3951
|
const foreshadowHtml = foreshadows.length
|
|
3816
|
-
? `${layout === "rows" ? foreshadowRows() : foreshadowCards()}`
|
|
3952
|
+
? `${layout === "rows" ? foreshadowRows() : foreshadowCards()}${renderModulePagination(foreshadowPageResult, "foreshadows", "伏笔列表")}`
|
|
3817
3953
|
: emptyModule("还没有伏笔", "创建伏笔并关联埋设、提醒与回收章节,未回收项会持续显示。\n");
|
|
3818
3954
|
if (foreshadows.length) mountModuleLayoutToggle(layout, "伏笔列表样式");
|
|
3819
|
-
const outlineHtml = outlines.length ? `<div class="outline-list">${
|
|
3955
|
+
const outlineHtml = outlines.length ? `<div class="outline-list">${outlinePageResult.items.map((item) => `
|
|
3820
3956
|
<article class="outline-row ${item.status === "completed" ? "is-complete" : ""}">
|
|
3821
3957
|
<div><small>${esc(item.volumeTitle)} · ${esc(outlineStatusLabel(item.status))}</small><h3>${esc(item.chapterTitle)}</h3></div>
|
|
3822
3958
|
<div><b>目标</b><p>${esc(item.goal || "未填写")}</p></div>
|
|
3823
3959
|
<div><b>冲突</b><p>${esc(item.conflict || "未填写")}</p></div>
|
|
3824
3960
|
<div><b>转折</b><p>${esc(item.turningPoint || "未填写")}</p></div>
|
|
3825
3961
|
<div class="outline-actions">${item.unresolvedForeshadowCount ? `<span>${item.unresolvedForeshadowCount} 个未回收伏笔</span>` : ""}<button data-edit-outline="${esc(item.chapterId)}">编辑</button><button data-entity-history="chapter-outline" data-entity-id="${esc(item.chapterId)}" data-entity-title="${esc(item.chapterTitle)}">版本历史</button></div>
|
|
3826
|
-
</article>`).join("")}</div
|
|
3962
|
+
</article>`).join("")}</div>${renderModulePagination(outlinePageResult, "outlinePlans", "章节规划")}` : emptyModule("还没有章节", "先创建章节,再为每章维护目标、冲突和转折。\n");
|
|
3827
3963
|
$("#module-content").innerHTML = `<div class="outline-summary"><article><strong>${outlines.length}</strong><span>章节规划</span></article><article><strong>${unresolved.length}</strong><span>未回收伏笔</span></article><article class="${overdue.length ? "danger-text" : ""}"><strong>${overdue.length}</strong><span>已逾期</span></article></div><section class="planning-section"><div class="section-title"><div><span class="eyebrow">伏笔追踪</span><h2>尚未回收与历史伏笔</h2></div></div>${foreshadowHtml}</section><section class="planning-section"><div class="section-title"><div><span class="eyebrow">逐章规划</span><h2>章节目标、冲突与转折</h2></div></div>${outlineHtml}</section>`;
|
|
3828
|
-
bindModuleLayoutToggle(renderOutlines);
|
|
3964
|
+
bindModuleLayoutToggle(() => renderOutlines(outlinePageResult.page, foreshadowPageResult.page));
|
|
3965
|
+
bindModulePagination("foreshadows", (page) => renderOutlines(outlinePageResult.page, page));
|
|
3966
|
+
bindModulePagination("outlinePlans", (page) => renderOutlines(page, foreshadowPageResult.page));
|
|
3829
3967
|
$("#module-content").querySelectorAll("[data-edit-outline]").forEach((button) => button.addEventListener("click", () => openOutlineDialog(outlines.find((item) => item.chapterId === button.dataset.editOutline))));
|
|
3830
3968
|
$("#module-content").querySelectorAll("[data-edit-foreshadow]").forEach((button) => button.addEventListener("click", () => openForeshadowDialog(foreshadows.find((item) => item.id === button.dataset.editForeshadow))));
|
|
3831
|
-
bindEntityHistoryButtons(renderOutlines);
|
|
3969
|
+
bindEntityHistoryButtons(() => renderOutlines(outlinePageResult.page, foreshadowPageResult.page));
|
|
3832
3970
|
}
|
|
3833
3971
|
|
|
3834
|
-
async function renderRelationships() {
|
|
3972
|
+
async function renderRelationships(page = moduleListPages.relationships) {
|
|
3835
3973
|
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
3836
3974
|
const relationships = await apiAllPages(`/api/works/${state.work.id}/relationships`);
|
|
3837
3975
|
const filteredRelationships = filterRelationships(relationships, relationshipFilters);
|
|
3976
|
+
const pageResult = paginateModuleItems(filteredRelationships, page, "relationships");
|
|
3977
|
+
moduleListPages.relationships = pageResult.page;
|
|
3838
3978
|
const hasRelationshipFilters = relationshipFilters.fromCharacterIds.length > 0 || relationshipFilters.toCharacterIds.length > 0;
|
|
3839
3979
|
const canEditRelationships = canEditModule("relationships");
|
|
3840
3980
|
mountModuleCount(filteredRelationships.length);
|
|
@@ -3858,10 +3998,11 @@ async function renderRelationships() {
|
|
|
3858
3998
|
if ($("#relationship-map-dialog").open) $("#relationship-map-dialog").close();
|
|
3859
3999
|
const graph = buildRelationshipGraph(state.characters, relationships);
|
|
3860
4000
|
state.relationshipGraph = graph;
|
|
3861
|
-
const relationshipList = filteredRelationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${
|
|
4001
|
+
const relationshipList = filteredRelationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${pageResult.items.map((item) => `
|
|
3862
4002
|
<tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
|
|
3863
4003
|
<td>${esc(relationshipCategoryLabel(item.category))} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(relationshipConfirmationLabel(item.confirmationStatus))}</td><td class="relationship-actions">${canEditRelationships ? `<button data-edit-relationship="${esc(item.id)}">编辑</button>` : ""}<button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : relationships.length ? '<div class="relationship-empty-note">没有符合当前筛选条件的关系。</div>' : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>';
|
|
3864
|
-
$("#module-content").innerHTML = `${filterToolbar}<div id="relationship-map-host"></div>${relationshipList}`;
|
|
4004
|
+
$("#module-content").innerHTML = `${filterToolbar}<div id="relationship-map-host"></div>${relationshipList}${renderModulePagination(pageResult, "relationships", "人物关系列表")}`;
|
|
4005
|
+
bindModulePagination("relationships", renderRelationships);
|
|
3865
4006
|
const openGalaxy = () => {
|
|
3866
4007
|
state.galaxy?.destroy();
|
|
3867
4008
|
state.galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, { workId: state.work.id });
|
|
@@ -3881,24 +4022,24 @@ async function renderRelationships() {
|
|
|
3881
4022
|
$("#relationship-from-character-filter").addEventListener("change", async () => {
|
|
3882
4023
|
relationshipFiltersPanelOpen = true;
|
|
3883
4024
|
relationshipFilters.fromCharacterIds = readSelectedValues("#relationship-from-character-filter");
|
|
3884
|
-
await renderRelationships();
|
|
4025
|
+
await renderRelationships(1);
|
|
3885
4026
|
});
|
|
3886
4027
|
$("#relationship-to-character-filter").addEventListener("change", async () => {
|
|
3887
4028
|
relationshipFiltersPanelOpen = true;
|
|
3888
4029
|
relationshipFilters.toCharacterIds = readSelectedValues("#relationship-to-character-filter");
|
|
3889
|
-
await renderRelationships();
|
|
4030
|
+
await renderRelationships(1);
|
|
3890
4031
|
});
|
|
3891
4032
|
$("#clear-relationship-filters")?.addEventListener("click", async () => {
|
|
3892
4033
|
relationshipFiltersPanelOpen = true;
|
|
3893
4034
|
relationshipFilters.fromCharacterIds = [];
|
|
3894
4035
|
relationshipFilters.toCharacterIds = [];
|
|
3895
|
-
await renderRelationships();
|
|
4036
|
+
await renderRelationships(1);
|
|
3896
4037
|
});
|
|
3897
4038
|
$("#module-content").querySelectorAll("[data-edit-relationship]").forEach((button) => button.addEventListener("click", () => openRelationshipDialog(filteredRelationships.find((item) => item.id === button.dataset.editRelationship))));
|
|
3898
|
-
bindEntityHistoryButtons(async () => { await renderRelationships(); await loadAiReferences(); });
|
|
4039
|
+
bindEntityHistoryButtons(async () => { await renderRelationships(pageResult.page); await loadAiReferences(); });
|
|
3899
4040
|
}
|
|
3900
4041
|
|
|
3901
|
-
async function renderReviews() {
|
|
4042
|
+
async function renderReviews(page = moduleListPages.reviews) {
|
|
3902
4043
|
const canReadCharacters = canReadModule("characters");
|
|
3903
4044
|
const canResolveReview = canEditModule("reviews");
|
|
3904
4045
|
const canMergeCharacters = canResolveReview
|
|
@@ -3908,6 +4049,8 @@ async function renderReviews() {
|
|
|
3908
4049
|
canReadCharacters ? apiAllPages(`/api/works/${state.work.id}/characters?includeMerged=1`) : Promise.resolve([])
|
|
3909
4050
|
]);
|
|
3910
4051
|
mountModuleCount(reviews.length);
|
|
4052
|
+
const pageResult = paginateModuleItems(reviews, page, "reviews");
|
|
4053
|
+
moduleListPages.reviews = pageResult.page;
|
|
3911
4054
|
const characterById = new Map(characters.map((character) => [character.id, character]));
|
|
3912
4055
|
const duplicateCard = (item) => {
|
|
3913
4056
|
const refs = (item.entityRefs ?? []).filter((reference) => reference?.type === "character" && characterById.has(reference.id));
|
|
@@ -3945,13 +4088,14 @@ async function renderReviews() {
|
|
|
3945
4088
|
};
|
|
3946
4089
|
if (reviews.length) mountModuleLayoutToggle(layout, "审核列表样式");
|
|
3947
4090
|
$("#module-content").innerHTML = reviews.length
|
|
3948
|
-
? `${layout === "rows" ? `<div class="module-row-list">${
|
|
4091
|
+
? `${layout === "rows" ? `<div class="module-row-list">${pageResult.items.map(reviewRow).join("")}</div>` : `<div class="card-grid">${pageResult.items.map(reviewCard).join("")}</div>`}${renderModulePagination(pageResult, "reviews", "审核列表")}`
|
|
3949
4092
|
: emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
|
|
3950
|
-
bindModuleLayoutToggle(renderReviews);
|
|
4093
|
+
bindModuleLayoutToggle(() => renderReviews(pageResult.page));
|
|
4094
|
+
bindModulePagination("reviews", renderReviews);
|
|
3951
4095
|
bindRecordPreview("[data-open-review]", (id) => openReviewDetailDialog(reviews.find((item) => item.id === id)));
|
|
3952
4096
|
$("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
|
|
3953
4097
|
await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
|
|
3954
|
-
await renderReviews();
|
|
4098
|
+
await renderReviews(pageResult.page);
|
|
3955
4099
|
}));
|
|
3956
4100
|
$("#module-content").querySelectorAll("[data-merge-review]").forEach((button) => button.addEventListener("click", async () => {
|
|
3957
4101
|
const target = characterById.get(button.dataset.mergeTarget);
|
|
@@ -3970,7 +4114,7 @@ async function renderReviews() {
|
|
|
3970
4114
|
expectedSourceVersionNo: Number(button.dataset.sourceVersion)
|
|
3971
4115
|
} });
|
|
3972
4116
|
toast(`已将“${source.name}”合并到“${target.name}”`);
|
|
3973
|
-
await renderReviews();
|
|
4117
|
+
await renderReviews(pageResult.page);
|
|
3974
4118
|
await loadAiReferences();
|
|
3975
4119
|
} catch (error) {
|
|
3976
4120
|
toast(error.message, "error");
|
|
@@ -3982,7 +4126,7 @@ async function renderReviews() {
|
|
|
3982
4126
|
try {
|
|
3983
4127
|
await api(`/api/reviews/${button.dataset.keepCharactersSeparate}/character-resolution`, { method: "POST", body: { action: "keep-separate" } });
|
|
3984
4128
|
toast("已确认这两个档案属于不同角色");
|
|
3985
|
-
await renderReviews();
|
|
4129
|
+
await renderReviews(pageResult.page);
|
|
3986
4130
|
} catch (error) {
|
|
3987
4131
|
toast(error.message, "error");
|
|
3988
4132
|
button.disabled = false;
|
|
@@ -4096,6 +4240,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4096
4240
|
try {
|
|
4097
4241
|
const result = await api(`/api/works/${state.work.id}/tasks/auto-run`, { method: "POST", body: {} });
|
|
4098
4242
|
toast(`已开始下一轮,队列中还有 ${result.pendingCount} 个待执行任务`);
|
|
4243
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
4099
4244
|
await renderTasks();
|
|
4100
4245
|
} catch (error) {
|
|
4101
4246
|
toast(error.message, "error");
|
|
@@ -4134,6 +4279,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4134
4279
|
scheduleTaskProgressRefresh(workId, 1);
|
|
4135
4280
|
const completed = await api(`/api/tasks/${button.dataset.runTask}/run`, { method: "POST", body: {} });
|
|
4136
4281
|
toast(completed.status === "cancelled" ? "分析任务已取消" : completed.status === "expired" ? "正文已变化,本次分析已过期" : "分析已完成");
|
|
4282
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
4137
4283
|
if (state.module === "tasks" && state.work?.id === workId) await renderTasks();
|
|
4138
4284
|
} catch (error) {
|
|
4139
4285
|
toast(error.message, "error");
|
|
@@ -4145,6 +4291,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4145
4291
|
try {
|
|
4146
4292
|
await api(`/api/tasks/${button.dataset.cancelTask}/cancel`, { method: "POST", body: {} });
|
|
4147
4293
|
toast("分析任务已取消");
|
|
4294
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
4148
4295
|
if (state.module === "tasks") await renderTasks();
|
|
4149
4296
|
} catch (error) {
|
|
4150
4297
|
toast(error.message, "error");
|
|
@@ -4154,6 +4301,23 @@ async function renderTasks(page = taskListPage) {
|
|
|
4154
4301
|
scheduleTaskProgressRefresh(state.work.id, runningCount);
|
|
4155
4302
|
}
|
|
4156
4303
|
|
|
4304
|
+
async function rerunAnalysisTask(taskId, button, { closeDetail = false } = {}) {
|
|
4305
|
+
const workId = state.work?.id;
|
|
4306
|
+
button.disabled = true;
|
|
4307
|
+
const originalLabel = button.textContent;
|
|
4308
|
+
button.textContent = "正在创建";
|
|
4309
|
+
try {
|
|
4310
|
+
const rerun = await api(`/api/tasks/${encodeURIComponent(taskId)}/rerun`, { method: "POST", body: {} });
|
|
4311
|
+
if (closeDetail) $("#form-dialog").close();
|
|
4312
|
+
toast(`已按原配置创建新任务 ${rerun.id}`);
|
|
4313
|
+
if (state.module === "tasks" && state.work?.id === workId) await renderTasks();
|
|
4314
|
+
} catch (error) {
|
|
4315
|
+
toast(error.message, "error");
|
|
4316
|
+
button.disabled = false;
|
|
4317
|
+
button.textContent = originalLabel;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4157
4321
|
function stopTaskProgressRefresh() {
|
|
4158
4322
|
if (taskProgressRefreshTimer === null) return;
|
|
4159
4323
|
window.clearTimeout(taskProgressRefreshTimer);
|
|
@@ -4372,6 +4536,30 @@ function renderTaskResultItem(item) {
|
|
|
4372
4536
|
</article>`;
|
|
4373
4537
|
}
|
|
4374
4538
|
|
|
4539
|
+
function renderRelationshipChangePreview(task, result) {
|
|
4540
|
+
const preview = result.relationshipChangePreview && typeof result.relationshipChangePreview === "object"
|
|
4541
|
+
? result.relationshipChangePreview
|
|
4542
|
+
: null;
|
|
4543
|
+
if (!preview) return "";
|
|
4544
|
+
const totalCount = Number(preview.totalCount ?? 0);
|
|
4545
|
+
const counts = `新增 ${Number(preview.createdCount ?? 0)} 项 · 更新 ${Number(preview.updatedCount ?? 0)} 项 · 删除 ${Number(preview.deletedCount ?? 0)} 项`;
|
|
4546
|
+
if (preview.status === "pending") {
|
|
4547
|
+
return `<section class="relationship-change-preview is-pending" aria-label="人物关系变更待确认">
|
|
4548
|
+
<div><small>写入前预览</small><h4>人物关系库尚未修改</h4><p>${esc(counts)}。请先核对下方关系和原文证据,再决定是否应用。</p></div>
|
|
4549
|
+
${Number(preview.deletedCount ?? 0) > 0 ? `<p class="relationship-change-preview-warning">确认应用后将删除 ${Number(preview.deletedCount)} 条已有关系;若资料已被他人修改,系统会阻止过期预览覆盖新版本。</p>` : ""}
|
|
4550
|
+
<div class="relationship-change-preview-actions">
|
|
4551
|
+
${totalCount > 0 && canEditModule("relationships") ? `<button class="primary-button" type="button" data-apply-relationship-changes="${esc(task.id)}">确认并应用 ${totalCount} 项变更</button>` : ""}
|
|
4552
|
+
<button class="ghost-button" type="button" data-discard-relationship-changes="${esc(task.id)}">${totalCount > 0 ? "放弃本次变更" : "结束无变更预览"}</button>
|
|
4553
|
+
${!canEditModule("relationships") && totalCount > 0 ? "<small>当前账号没有编辑人物关系的权限,只能查看或放弃本次变更。</small>" : ""}
|
|
4554
|
+
</div>
|
|
4555
|
+
</section>`;
|
|
4556
|
+
}
|
|
4557
|
+
const applied = preview.status === "applied";
|
|
4558
|
+
return `<section class="relationship-change-preview ${applied ? "is-applied" : "is-discarded"}" aria-label="人物关系变更状态">
|
|
4559
|
+
<div><small>写入前预览</small><h4>${applied ? "变更已应用" : "本次变更已放弃"}</h4><p>${esc(counts)}。${applied ? "人物关系库已按确认结果更新。" : "人物关系库未因本次预览发生修改。"}</p></div>
|
|
4560
|
+
</section>`;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4375
4563
|
function renderTaskResult(task) {
|
|
4376
4564
|
const result = task.resultSummary && typeof task.resultSummary === "object" ? task.resultSummary : {};
|
|
4377
4565
|
const metrics = Array.isArray(result.metrics) ? result.metrics : [];
|
|
@@ -4384,6 +4572,7 @@ function renderTaskResult(task) {
|
|
|
4384
4572
|
<p class="task-result-summary">${esc(result.summary || "任务尚未产生分析结果。")}</p>
|
|
4385
4573
|
${result.restricted ? '<p class="task-result-warning">部分结果因当前账号权限受限而隐藏。</p>' : ""}
|
|
4386
4574
|
</section>
|
|
4575
|
+
${renderRelationshipChangePreview(task, result)}
|
|
4387
4576
|
<section class="task-result-section">
|
|
4388
4577
|
<h4>结果保存位置</h4>
|
|
4389
4578
|
<p><strong>作品</strong> ${esc(state.work?.title || "当前作品")}</p>
|
|
@@ -4425,6 +4614,111 @@ function bindTaskResultActions(container) {
|
|
|
4425
4614
|
button.textContent = "重新加载完整 JSON";
|
|
4426
4615
|
}
|
|
4427
4616
|
}));
|
|
4617
|
+
container.querySelectorAll("[data-apply-relationship-changes], [data-discard-relationship-changes]").forEach((button) => button.addEventListener("click", async () => {
|
|
4618
|
+
if (button.disabled) return;
|
|
4619
|
+
const taskId = button.dataset.applyRelationshipChanges || button.dataset.discardRelationshipChanges;
|
|
4620
|
+
const action = button.dataset.applyRelationshipChanges ? "apply" : "discard";
|
|
4621
|
+
const controls = button.closest(".relationship-change-preview")?.querySelectorAll("button") ?? [];
|
|
4622
|
+
controls.forEach((control) => { control.disabled = true; });
|
|
4623
|
+
button.textContent = action === "apply" ? "正在应用变更" : "正在放弃变更";
|
|
4624
|
+
try {
|
|
4625
|
+
await api(`/api/tasks/${encodeURIComponent(taskId)}/relationship-changes/${action}`, { method: "POST", body: {} });
|
|
4626
|
+
$("#form-dialog").close();
|
|
4627
|
+
toast(action === "apply" ? "人物关系变更已应用" : "本次人物关系变更已放弃");
|
|
4628
|
+
if (state.module === "tasks") await renderTasks();
|
|
4629
|
+
} catch (error) {
|
|
4630
|
+
toast(error.message, "error");
|
|
4631
|
+
controls.forEach((control) => { control.disabled = false; });
|
|
4632
|
+
button.textContent = action === "apply" ? "重新确认并应用" : "重新放弃本次变更";
|
|
4633
|
+
}
|
|
4634
|
+
}));
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
function relationshipIdentityRepairFailure(task) {
|
|
4638
|
+
if (task?.taskType !== "relationship-analysis" || !Array.isArray(task.failures)) return null;
|
|
4639
|
+
return task.failures.find((failure) =>
|
|
4640
|
+
failure?.code === "RELATIONSHIP_MATCH_CANDIDATES_EXCEEDED"
|
|
4641
|
+
&& failure.details
|
|
4642
|
+
&& typeof failure.details === "object"
|
|
4643
|
+
&& typeof failure.details.characterId === "string"
|
|
4644
|
+
) ?? null;
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
function relationshipIdentityRepairMetrics(details) {
|
|
4648
|
+
const metrics = [];
|
|
4649
|
+
if (Number.isFinite(Number(details.candidateCount))) {
|
|
4650
|
+
metrics.push(["疑似来源", `${Number(details.candidateCount)} / 上限 ${Number(details.maximum)}`]);
|
|
4651
|
+
}
|
|
4652
|
+
if (Number.isFinite(Number(details.fuzzyReferenceCount))) {
|
|
4653
|
+
metrics.push(["名称与别名", `${Number(details.fuzzyReferenceCount)} / 上限 ${Number(details.maximumFuzzyReferences)}`]);
|
|
4654
|
+
}
|
|
4655
|
+
if (Number.isFinite(Number(details.scannedCharacters))) {
|
|
4656
|
+
metrics.push(["待核对文本", `${Number(details.scannedCharacters).toLocaleString("zh-CN")} / 上限 ${Number(details.maximumScannedCharacters).toLocaleString("zh-CN")} 字符`]);
|
|
4657
|
+
}
|
|
4658
|
+
if (Number.isFinite(Number(details.fuzzyMatchCount))) {
|
|
4659
|
+
metrics.push(["疑似写法", `${Number(details.fuzzyMatchCount)} / 上限 ${Number(details.maximumFuzzyMatches)}`]);
|
|
4660
|
+
}
|
|
4661
|
+
metrics.push(["身份锚点", `${Number(details.identityAnchorCount ?? 0)} 个`]);
|
|
4662
|
+
return metrics;
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
async function openRelationshipIdentityRepairDialog(task, failure) {
|
|
4666
|
+
const details = failure.details && typeof failure.details === "object" ? failure.details : {};
|
|
4667
|
+
const character = await api(`/api/characters/${encodeURIComponent(details.characterId)}`);
|
|
4668
|
+
const anchors = [
|
|
4669
|
+
character.code,
|
|
4670
|
+
character.attributes?.identity,
|
|
4671
|
+
character.race?.name || character.species,
|
|
4672
|
+
...(Array.isArray(character.organizations) ? character.organizations.map((organization) => organization.name) : [])
|
|
4673
|
+
].map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
4674
|
+
const metrics = relationshipIdentityRepairMetrics(details);
|
|
4675
|
+
const metricHtml = metrics.map(([label, value]) => `<div><dt>${esc(label)}</dt><dd>${esc(value)}</dd></div>`).join("");
|
|
4676
|
+
const anchorHtml = anchors.length
|
|
4677
|
+
? anchors.map((anchor) => `<span>${esc(anchor)}</span>`).join("")
|
|
4678
|
+
: "<em>尚未登记可用于消歧的身份锚点</em>";
|
|
4679
|
+
openDialog("修复人物身份资料",
|
|
4680
|
+
`<section class="relationship-identity-repair-summary">
|
|
4681
|
+
<div><span>匹配诊断</span><h3>${esc(character.name)}</h3><p>${esc(failure.message)}</p></div>
|
|
4682
|
+
<dl>${metricHtml}</dl>
|
|
4683
|
+
<div class="relationship-identity-anchor-list"><strong>当前身份锚点</strong><div>${anchorHtml}</div></div>
|
|
4684
|
+
</section>
|
|
4685
|
+
<p class="relationship-identity-repair-guidance">把原文中确实指向该人物的稳定称呼登记为别名;编号和身份定位应填写会在正文或设定中共同出现的辨识词。保存后,别名命中会转为精确匹配,身份锚点可收窄过多的拼音疑似来源。</p>
|
|
4686
|
+
<label>角色标准名<input type="text" value="${esc(character.name)}" readonly></label>
|
|
4687
|
+
${field("aliases", "确认别名", "item-list", character.aliases ?? [])}
|
|
4688
|
+
${field("code", "人物编号或代号", "text", character.code)}
|
|
4689
|
+
${field("identity", "身份与定位", "text", character.attributes?.identity)}
|
|
4690
|
+
<button class="ghost-button relationship-identity-full-profile" type="button" data-open-identity-character-profile>打开完整人物档案</button>`,
|
|
4691
|
+
async (form) => {
|
|
4692
|
+
const aliases = form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean);
|
|
4693
|
+
const code = String(form.get("code") ?? "").trim();
|
|
4694
|
+
const identity = String(form.get("identity") ?? "").trim();
|
|
4695
|
+
const saved = await api(`/api/characters/${encodeURIComponent(character.id)}`, {
|
|
4696
|
+
method: "PATCH",
|
|
4697
|
+
body: {
|
|
4698
|
+
aliases,
|
|
4699
|
+
code,
|
|
4700
|
+
attributes: { ...(character.attributes ?? {}), identity },
|
|
4701
|
+
expectedVersionNo: character.versionNo,
|
|
4702
|
+
changeNote: `修复人物关系来源匹配:${failure.message}`.slice(0, 500)
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
await loadAiReferences();
|
|
4706
|
+
if (state.module === "tasks") await renderTasks(taskListPage);
|
|
4707
|
+
toast(`“${saved.name}”的身份资料已保存为 v${saved.versionNo},可重新运行原分析配置`);
|
|
4708
|
+
},
|
|
4709
|
+
"人物关系匹配修复",
|
|
4710
|
+
{
|
|
4711
|
+
submitLabel: "保存身份修复",
|
|
4712
|
+
pendingLabel: "保存中…",
|
|
4713
|
+
pendingMessage: "正在保存人物身份资料",
|
|
4714
|
+
errorPrefix: "身份修复失败:",
|
|
4715
|
+
wide: true
|
|
4716
|
+
});
|
|
4717
|
+
$("#dialog-fields").querySelector("[data-open-identity-character-profile]")?.addEventListener("click", async () => {
|
|
4718
|
+
$("#form-dialog").close();
|
|
4719
|
+
await openCharacterEditor(character);
|
|
4720
|
+
activateCharacterEditorTab("profile");
|
|
4721
|
+
});
|
|
4428
4722
|
}
|
|
4429
4723
|
|
|
4430
4724
|
function openTaskDetailDialog(task, trace) {
|
|
@@ -4453,6 +4747,15 @@ function openTaskDetailDialog(task, trace) {
|
|
|
4453
4747
|
const failureHtml = failures.length
|
|
4454
4748
|
? `<ul>${failures.map((item) => `<li>${esc(item.message || JSON.stringify(item))}</li>`).join("")}</ul>`
|
|
4455
4749
|
: "<p>无</p>";
|
|
4750
|
+
const identityRepairFailure = relationshipIdentityRepairFailure(task);
|
|
4751
|
+
const identityRepairHtml = identityRepairFailure
|
|
4752
|
+
? `<section class="relationship-identity-repair-entry">
|
|
4753
|
+
<div><strong>可修复的人物身份匹配问题</strong><p>系统已定位到触发来源超限的角色。补充准确别名或身份锚点后,再按原配置重试。</p></div>
|
|
4754
|
+
${canEditModule("characters")
|
|
4755
|
+
? '<button class="primary-button" type="button" data-task-identity-repair>修复人物身份资料</button>'
|
|
4756
|
+
: "<small>当前账户没有人物模块编辑权限。</small>"}
|
|
4757
|
+
</section>`
|
|
4758
|
+
: "";
|
|
4456
4759
|
const resultPreview = renderTaskResult(task);
|
|
4457
4760
|
openDialog("任务详情",
|
|
4458
4761
|
`<div class="task-detail">
|
|
@@ -4463,8 +4766,9 @@ function openTaskDetailDialog(task, trace) {
|
|
|
4463
4766
|
<p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
|
|
4464
4767
|
<p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
|
|
4465
4768
|
<div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
|
|
4466
|
-
<div><strong>失败信息</strong>${failureHtml}</div>
|
|
4769
|
+
<div><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
|
|
4467
4770
|
<div><strong>结果摘要</strong>${resultPreview}</div>
|
|
4771
|
+
${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
|
|
4468
4772
|
</section>
|
|
4469
4773
|
${renderTaskTraceVisualization(trace, task.id)}
|
|
4470
4774
|
</div>`,
|
|
@@ -4473,15 +4777,28 @@ function openTaskDetailDialog(task, trace) {
|
|
|
4473
4777
|
{ submitLabel: "关闭", wide: true, trace: true });
|
|
4474
4778
|
bindTaskTraceCallActions($("#dialog-fields"));
|
|
4475
4779
|
bindTaskResultActions($("#dialog-fields"));
|
|
4780
|
+
$("#dialog-fields").querySelector("[data-rerun-task-detail]")?.addEventListener("click", async (event) => {
|
|
4781
|
+
await rerunAnalysisTask(event.currentTarget.dataset.rerunTaskDetail, event.currentTarget, { closeDetail: true });
|
|
4782
|
+
});
|
|
4783
|
+
$("#dialog-fields").querySelector("[data-task-identity-repair]")?.addEventListener("click", async (event) => {
|
|
4784
|
+
const button = event.currentTarget;
|
|
4785
|
+
button.disabled = true;
|
|
4786
|
+
$("#form-dialog").close();
|
|
4787
|
+
try {
|
|
4788
|
+
await openRelationshipIdentityRepairDialog(task, identityRepairFailure);
|
|
4789
|
+
} catch (error) {
|
|
4790
|
+
toast(`身份修复向导加载失败:${error.message}`, "error");
|
|
4791
|
+
}
|
|
4792
|
+
});
|
|
4476
4793
|
}
|
|
4477
4794
|
|
|
4478
4795
|
function renderProviderCards(providers, models) {
|
|
4479
4796
|
return providers.length ? `<div class="card-grid provider-card-grid">${providers.map((provider) => `
|
|
4480
|
-
<article class="record-card provider-card"><small>平台级 · ${esc(providerStatusLabel(provider.status))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><h3>${esc(provider.name)}</h3>
|
|
4797
|
+
<article class="record-card provider-card"><small>平台级 · ${esc(providerProtocolLabel(provider.protocol))} · ${esc(providerStatusLabel(provider.status))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><h3>${esc(provider.name)}</h3>
|
|
4481
4798
|
<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit} · 最大输出:${provider.maxTokens ?? 32000}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
|
|
4482
4799
|
<div class="provider-models">${models.filter((model) => model.providerId === provider.id).map((model) => `<button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>`).join("")}</div>
|
|
4483
4800
|
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button><button data-test-provider="${esc(provider.id)}">测试连接</button><button data-add-model="${esc(provider.id)}">添加模型</button></div></article>`).join("")}</div>`
|
|
4484
|
-
: emptyModule("尚未配置 AI 供应商", "添加 OpenAI 兼容接口地址和密钥,测试成功后再添加模型。");
|
|
4801
|
+
: emptyModule("尚未配置 AI 供应商", "添加 OpenAI 或 Anthropic 兼容接口地址和密钥,测试成功后再添加模型。");
|
|
4485
4802
|
}
|
|
4486
4803
|
|
|
4487
4804
|
function bindPlatformProviderActions(host, providers, models) {
|
|
@@ -4517,6 +4834,266 @@ function renderTaskDefaults(models, providers, taskDefaults) {
|
|
|
4517
4834
|
</section>` : emptyModule("尚未配置平台模型", "请先在平台 AI 管理中添加并测试供应商模型。");
|
|
4518
4835
|
}
|
|
4519
4836
|
|
|
4837
|
+
const relationshipIndexStatusLabels = Object.freeze({
|
|
4838
|
+
queued: "等待同步",
|
|
4839
|
+
building: "正在同步",
|
|
4840
|
+
ready: "已索引",
|
|
4841
|
+
failed: "同步失败"
|
|
4842
|
+
});
|
|
4843
|
+
|
|
4844
|
+
const relationshipIndexSourceLabels = Object.freeze({
|
|
4845
|
+
work: "作品资料",
|
|
4846
|
+
chapter: "正文",
|
|
4847
|
+
setting: "设定",
|
|
4848
|
+
character: "人物",
|
|
4849
|
+
race: "种族",
|
|
4850
|
+
organization: "组织",
|
|
4851
|
+
"timeline-track": "时间轴",
|
|
4852
|
+
"timeline-event": "时间线事件",
|
|
4853
|
+
relationship: "人物关系",
|
|
4854
|
+
"chapter-outline": "章节大纲",
|
|
4855
|
+
foreshadow: "伏笔",
|
|
4856
|
+
review: "审核项"
|
|
4857
|
+
});
|
|
4858
|
+
|
|
4859
|
+
function relationshipIndexStatusMarkup(status) {
|
|
4860
|
+
const queuedSources = Array.isArray(status.queuedSources) ? status.queuedSources : [];
|
|
4861
|
+
const statusName = relationshipIndexStatusLabels[status.status] ?? "状态未知";
|
|
4862
|
+
const queueMarkup = queuedSources.length
|
|
4863
|
+
? queuedSources.map((item) => `<span class="relationship-index-queue-chip"><span>${esc(relationshipIndexSourceLabels[item.sourceType] ?? item.sourceType)}</span><strong>${esc(String(item.count))}</strong></span>`).join("")
|
|
4864
|
+
: '<span class="relationship-index-queue-empty">队列为空</span>';
|
|
4865
|
+
const updatedAt = formatDateTime(status.updatedAt) || "尚未构建";
|
|
4866
|
+
const errorMarkup = status.error
|
|
4867
|
+
? `<p class="relationship-index-error">${esc(status.error)}</p>`
|
|
4868
|
+
: "";
|
|
4869
|
+
return `<div class="relationship-index-summary">
|
|
4870
|
+
<div class="relationship-index-state-row"><span class="relationship-index-state is-${esc(String(status.status ?? "unknown"))}">${esc(statusName)}</span><span>索引代次 <strong>${esc(String(status.generation ?? 0))}</strong></span><span>最后更新 <strong>${esc(updatedAt)}</strong></span></div>
|
|
4871
|
+
<dl class="relationship-index-metrics"><div><dt>待同步任务</dt><dd>${esc(String(status.queuedSourceCount ?? 0))}</dd></div><div><dt>已索引正文段落</dt><dd>${esc(String(status.indexedParagraphCount ?? 0))}</dd></div><div><dt>已索引设定来源</dt><dd>${esc(String(status.indexedSourceCount ?? 0))}</dd></div></dl>
|
|
4872
|
+
<div class="relationship-index-queue"><div class="relationship-index-queue-heading"><strong>增量任务队列</strong><span>仅包含新增、修改或删除后待同步的来源</span></div><div class="relationship-index-queue-list">${queueMarkup}</div></div>
|
|
4873
|
+
${errorMarkup}
|
|
4874
|
+
</div>`;
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
function updateBackgroundTaskCenterVisibility() {
|
|
4878
|
+
const button = $("#background-task-button");
|
|
4879
|
+
if (!button) return;
|
|
4880
|
+
const visible = Boolean(state.work)
|
|
4881
|
+
&& (canReadModule("tasks") || canReadModule("ai-settings"));
|
|
4882
|
+
button.classList.toggle("hidden", !visible);
|
|
4883
|
+
if (!visible) {
|
|
4884
|
+
$("#background-task-count")?.classList.add("hidden");
|
|
4885
|
+
return;
|
|
4886
|
+
}
|
|
4887
|
+
const activityCount = backgroundTaskActivityCount(
|
|
4888
|
+
backgroundTaskCenterSnapshot.taskPage,
|
|
4889
|
+
backgroundTaskCenterSnapshot.relationshipIndex
|
|
4890
|
+
);
|
|
4891
|
+
const badge = $("#background-task-count");
|
|
4892
|
+
badge.textContent = activityCount > 99 ? "99+" : String(activityCount);
|
|
4893
|
+
badge.classList.toggle("hidden", activityCount === 0);
|
|
4894
|
+
button.classList.toggle("is-active", activityCount > 0);
|
|
4895
|
+
button.setAttribute("aria-label", activityCount > 0 ? `后台任务中心,${activityCount} 项进行中` : "后台任务中心");
|
|
4896
|
+
button.setAttribute("title", activityCount > 0 ? `后台任务中心 · ${activityCount} 项进行中` : "后台任务中心");
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
function backgroundTaskTransitionMessage(transition) {
|
|
4900
|
+
const label = analysisTaskTypeLabel(transition.task.taskType);
|
|
4901
|
+
if (transition.status === "partial") return { message: `${label}部分失败,请打开任务详情查看`, type: "error" };
|
|
4902
|
+
if (transition.status === "expired") return { message: `${label}已过期,正文可能已发生变化`, type: "error" };
|
|
4903
|
+
if (transition.status === "cancelled") return { message: `${label}已取消`, type: "info" };
|
|
4904
|
+
return { message: `${label}已完成`, type: "info" };
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
function backgroundTaskListMarkup(taskPage, error) {
|
|
4908
|
+
if (!canReadModule("tasks")) {
|
|
4909
|
+
return '<section class="background-task-section"><div class="background-task-section-heading"><div><strong>AI 分析任务</strong><small>当前账户无权查看此模块</small></div></div></section>';
|
|
4910
|
+
}
|
|
4911
|
+
if (!taskPage) {
|
|
4912
|
+
return `<section class="background-task-section"><div class="background-task-section-heading"><div><strong>AI 分析任务</strong><small>${esc(error || "正在读取任务队列")}</small></div></div></section>`;
|
|
4913
|
+
}
|
|
4914
|
+
const tasks = Array.isArray(taskPage.items) ? taskPage.items.slice(0, 10) : [];
|
|
4915
|
+
const pendingCount = Number(taskPage.stats?.pendingCount ?? 0);
|
|
4916
|
+
const runningCount = Number(taskPage.stats?.runningCount ?? 0);
|
|
4917
|
+
return `<section class="background-task-section">
|
|
4918
|
+
<div class="background-task-section-heading"><div><strong>AI 分析任务</strong><small>待执行 ${pendingCount} 个 · 运行中 ${runningCount} 个 · 共 ${Number(taskPage.stats?.total ?? taskPage.total ?? tasks.length)} 个</small></div></div>
|
|
4919
|
+
${error ? `<p class="background-task-error">${esc(error)}</p>` : ""}
|
|
4920
|
+
${tasks.length ? `<div class="background-task-list">${tasks.map((item) => {
|
|
4921
|
+
const status = normalizedAnalysisTaskStatus(item.status);
|
|
4922
|
+
return `<article class="background-task-row">
|
|
4923
|
+
<span class="task-status-badge is-${esc(status)}"><span class="task-status-indicator" aria-hidden="true"></span><span>${esc(analysisTaskStatusLabel(item.status))}</span></span>
|
|
4924
|
+
<div><strong>${esc(analysisTaskTypeLabel(item.taskType))}</strong><small>${esc(item.scopeSummary || "未指定范围")} · ${esc(item.model?.displayName || "默认模型")}</small></div>
|
|
4925
|
+
<span class="background-task-progress">${analysisTaskProgressValue(item.progress)}%</span>
|
|
4926
|
+
<button class="ghost-button" type="button" data-background-task-detail="${esc(item.id)}">详情</button>
|
|
4927
|
+
</article>`;
|
|
4928
|
+
}).join("")}</div>` : '<p class="background-task-empty">还没有 AI 分析任务。</p>'}
|
|
4929
|
+
</section>`;
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4932
|
+
function backgroundIndexMarkup(relationshipIndex, error) {
|
|
4933
|
+
if (!canReadModule("ai-settings")) {
|
|
4934
|
+
return '<section class="background-task-section"><div class="background-task-section-heading"><div><strong>人物关系拼音索引</strong><small>当前账户无权查看此模块</small></div></div></section>';
|
|
4935
|
+
}
|
|
4936
|
+
if (!relationshipIndex) {
|
|
4937
|
+
return `<section class="background-task-section"><div class="background-task-section-heading"><div><strong>人物关系拼音索引</strong><small>${esc(error || "正在读取索引队列")}</small></div></div></section>`;
|
|
4938
|
+
}
|
|
4939
|
+
const editable = canEditModule("ai-settings");
|
|
4940
|
+
return `<section class="background-task-section">
|
|
4941
|
+
<div class="background-task-section-heading">
|
|
4942
|
+
<div><strong>人物关系拼音索引</strong><small>可在任何模块查看增量同步状态</small></div>
|
|
4943
|
+
<div class="background-index-actions ${editable ? "" : "hidden"}">
|
|
4944
|
+
<button class="primary-button" type="button" data-background-index-action="sync">同步增量队列</button>
|
|
4945
|
+
<button class="ghost-button" type="button" data-background-index-action="rebuild">完整重建</button>
|
|
4946
|
+
</div>
|
|
4947
|
+
</div>
|
|
4948
|
+
${error ? `<p class="background-task-error">${esc(error)}</p>` : ""}
|
|
4949
|
+
${relationshipIndexStatusMarkup(relationshipIndex)}
|
|
4950
|
+
</section>`;
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
function renderBackgroundTaskCenter() {
|
|
4954
|
+
updateBackgroundTaskCenterVisibility();
|
|
4955
|
+
const content = $("#background-task-content");
|
|
4956
|
+
if (!content) return;
|
|
4957
|
+
content.innerHTML = `${backgroundTaskListMarkup(
|
|
4958
|
+
backgroundTaskCenterSnapshot.taskPage,
|
|
4959
|
+
backgroundTaskCenterSnapshot.errors.tasks
|
|
4960
|
+
)}${backgroundIndexMarkup(
|
|
4961
|
+
backgroundTaskCenterSnapshot.relationshipIndex,
|
|
4962
|
+
backgroundTaskCenterSnapshot.errors.index
|
|
4963
|
+
)}`;
|
|
4964
|
+
$("#background-task-dialog-meta").textContent = state.work
|
|
4965
|
+
? `《${state.work.title}》的分析任务与索引队列`
|
|
4966
|
+
: "当前作品的分析任务与索引队列";
|
|
4967
|
+
$("#background-task-open-analysis").classList.toggle("hidden", !canReadModule("tasks"));
|
|
4968
|
+
content.querySelectorAll("[data-background-task-detail]").forEach((button) => button.addEventListener("click", async () => {
|
|
4969
|
+
button.disabled = true;
|
|
4970
|
+
try {
|
|
4971
|
+
const taskId = encodeURIComponent(button.dataset.backgroundTaskDetail);
|
|
4972
|
+
const [task, trace] = await Promise.all([
|
|
4973
|
+
api(`/api/tasks/${taskId}/detail`),
|
|
4974
|
+
api(`/api/tasks/${taskId}/trace`).catch((error) => {
|
|
4975
|
+
if (error.code === "WORK_MODULE_READ_DENIED") return { restricted: true, captured: false, calls: [] };
|
|
4976
|
+
throw error;
|
|
4977
|
+
})
|
|
4978
|
+
]);
|
|
4979
|
+
$("#background-task-dialog").close();
|
|
4980
|
+
openTaskDetailDialog(task, trace);
|
|
4981
|
+
} catch (error) {
|
|
4982
|
+
toast(error.message, "error");
|
|
4983
|
+
button.disabled = false;
|
|
4984
|
+
}
|
|
4985
|
+
}));
|
|
4986
|
+
content.querySelectorAll("[data-background-index-action]").forEach((button) => button.addEventListener("click", async () => {
|
|
4987
|
+
if (!backgroundTaskCenterWorkId) return;
|
|
4988
|
+
button.disabled = true;
|
|
4989
|
+
const action = button.dataset.backgroundIndexAction;
|
|
4990
|
+
try {
|
|
4991
|
+
const status = await api(`/api/works/${encodeURIComponent(backgroundTaskCenterWorkId)}/ai-settings/relationship-search-index/${action}`, {
|
|
4992
|
+
method: "POST"
|
|
4993
|
+
});
|
|
4994
|
+
backgroundTaskCenterSnapshot.relationshipIndex = status;
|
|
4995
|
+
renderBackgroundTaskCenter();
|
|
4996
|
+
toast(action === "rebuild"
|
|
4997
|
+
? `已将全部来源加入索引队列,共 ${status.queuedSourceCount} 项`
|
|
4998
|
+
: Number(status.queuedSourceCount) > 0
|
|
4999
|
+
? `开始同步 ${status.queuedSourceCount} 项增量任务`
|
|
5000
|
+
: "增量任务队列为空,索引已是最新状态");
|
|
5001
|
+
scheduleBackgroundTaskCenterRefresh();
|
|
5002
|
+
} catch (error) {
|
|
5003
|
+
toast(error.message, "error");
|
|
5004
|
+
button.disabled = false;
|
|
5005
|
+
}
|
|
5006
|
+
}));
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
function scheduleBackgroundTaskCenterRefresh() {
|
|
5010
|
+
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
5011
|
+
backgroundTaskCenterTimer = null;
|
|
5012
|
+
if (!backgroundTaskCenterWorkId) return;
|
|
5013
|
+
const activityCount = backgroundTaskActivityCount(
|
|
5014
|
+
backgroundTaskCenterSnapshot.taskPage,
|
|
5015
|
+
backgroundTaskCenterSnapshot.relationshipIndex
|
|
5016
|
+
);
|
|
5017
|
+
const delay = backgroundTaskPollDelay(activityCount, Boolean($("#background-task-dialog")?.open));
|
|
5018
|
+
backgroundTaskCenterTimer = window.setTimeout(() => {
|
|
5019
|
+
backgroundTaskCenterTimer = null;
|
|
5020
|
+
void refreshBackgroundTaskCenter();
|
|
5021
|
+
}, delay);
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
async function refreshBackgroundTaskCenter({ announce = true } = {}) {
|
|
5025
|
+
const workId = backgroundTaskCenterWorkId;
|
|
5026
|
+
if (!workId || state.work?.id !== workId) return;
|
|
5027
|
+
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
5028
|
+
backgroundTaskCenterTimer = null;
|
|
5029
|
+
const requestId = ++backgroundTaskCenterRequest;
|
|
5030
|
+
const readTasks = canReadModule("tasks");
|
|
5031
|
+
const readIndex = canReadModule("ai-settings");
|
|
5032
|
+
const [taskResult, indexResult] = await Promise.all([
|
|
5033
|
+
readTasks
|
|
5034
|
+
? api(`/api/works/${encodeURIComponent(workId)}/tasks?page=1&limit=30`)
|
|
5035
|
+
.then((value) => ({ value }))
|
|
5036
|
+
.catch((error) => ({ error }))
|
|
5037
|
+
: Promise.resolve(null),
|
|
5038
|
+
readIndex
|
|
5039
|
+
? api(`/api/works/${encodeURIComponent(workId)}/ai-settings/relationship-search-index`)
|
|
5040
|
+
.then((value) => ({ value }))
|
|
5041
|
+
.catch((error) => ({ error }))
|
|
5042
|
+
: Promise.resolve(null)
|
|
5043
|
+
]);
|
|
5044
|
+
if (requestId !== backgroundTaskCenterRequest || backgroundTaskCenterWorkId !== workId || state.work?.id !== workId) return;
|
|
5045
|
+
const errors = {};
|
|
5046
|
+
if (taskResult?.value) {
|
|
5047
|
+
const transitionResult = collectBackgroundTaskTransitions(
|
|
5048
|
+
backgroundTaskCenterTaskSnapshots,
|
|
5049
|
+
taskResult.value.items,
|
|
5050
|
+
backgroundTaskCenterTasksInitialized
|
|
5051
|
+
);
|
|
5052
|
+
backgroundTaskCenterTaskSnapshots = transitionResult.snapshots;
|
|
5053
|
+
backgroundTaskCenterSnapshot.taskPage = taskResult.value;
|
|
5054
|
+
if (backgroundTaskCenterTasksInitialized && announce && state.module !== "tasks") {
|
|
5055
|
+
for (const transition of transitionResult.transitions) {
|
|
5056
|
+
const notification = backgroundTaskTransitionMessage(transition);
|
|
5057
|
+
toast(notification.message, notification.type);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
backgroundTaskCenterTasksInitialized = true;
|
|
5061
|
+
} else if (taskResult?.error) {
|
|
5062
|
+
errors.tasks = taskResult.error.message;
|
|
5063
|
+
console.error("Failed to refresh background analysis tasks", taskResult.error);
|
|
5064
|
+
}
|
|
5065
|
+
if (indexResult?.value) {
|
|
5066
|
+
backgroundTaskCenterSnapshot.relationshipIndex = indexResult.value;
|
|
5067
|
+
} else if (indexResult?.error) {
|
|
5068
|
+
errors.index = indexResult.error.message;
|
|
5069
|
+
console.error("Failed to refresh relationship search index status", indexResult.error);
|
|
5070
|
+
}
|
|
5071
|
+
backgroundTaskCenterSnapshot.errors = errors;
|
|
5072
|
+
renderBackgroundTaskCenter();
|
|
5073
|
+
scheduleBackgroundTaskCenterRefresh();
|
|
5074
|
+
}
|
|
5075
|
+
|
|
5076
|
+
function stopBackgroundTaskCenter() {
|
|
5077
|
+
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
5078
|
+
backgroundTaskCenterTimer = null;
|
|
5079
|
+
backgroundTaskCenterRequest += 1;
|
|
5080
|
+
backgroundTaskCenterWorkId = null;
|
|
5081
|
+
backgroundTaskCenterTasksInitialized = false;
|
|
5082
|
+
backgroundTaskCenterTaskSnapshots = new Map();
|
|
5083
|
+
backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
5084
|
+
const dialog = $("#background-task-dialog");
|
|
5085
|
+
if (dialog?.open) dialog.close();
|
|
5086
|
+
$("#background-task-button")?.classList.add("hidden");
|
|
5087
|
+
$("#background-task-count")?.classList.add("hidden");
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
function startBackgroundTaskCenter(workId) {
|
|
5091
|
+
stopBackgroundTaskCenter();
|
|
5092
|
+
backgroundTaskCenterWorkId = String(workId);
|
|
5093
|
+
renderBackgroundTaskCenter();
|
|
5094
|
+
void refreshBackgroundTaskCenter();
|
|
5095
|
+
}
|
|
5096
|
+
|
|
4520
5097
|
async function renderPlatformAiConfig() {
|
|
4521
5098
|
const [providers, models, settings] = await Promise.all([
|
|
4522
5099
|
api("/api/platform/ai/providers"),
|
|
@@ -4540,16 +5117,116 @@ async function renderPlatformAiConfig() {
|
|
|
4540
5117
|
bindPlatformProviderActions(host, providers, models);
|
|
4541
5118
|
}
|
|
4542
5119
|
|
|
5120
|
+
function tokenUsageDateLabel(date) {
|
|
5121
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
5122
|
+
year: "numeric",
|
|
5123
|
+
month: "short",
|
|
5124
|
+
day: "numeric",
|
|
5125
|
+
weekday: "short"
|
|
5126
|
+
}).format(new Date(`${date}T00:00:00`));
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
function tokenUsageCalendarMarkup(daily) {
|
|
5130
|
+
const calendar = buildUsageCalendar(daily);
|
|
5131
|
+
const cells = calendar.cells.map((cell) => {
|
|
5132
|
+
const label = `${tokenUsageDateLabel(cell.date)}:${Number(cell.totalTokens).toLocaleString("zh-CN")} Token`;
|
|
5133
|
+
return `<span class="usage-calendar-cell${cell.future ? " is-future" : ""}" data-level="${cell.level}" role="gridcell" aria-label="${esc(label)}" title="${esc(label)}" ${cell.future ? 'aria-disabled="true"' : 'tabindex="0"'}></span>`;
|
|
5134
|
+
}).join("");
|
|
5135
|
+
const months = calendar.months.map((month) => `<span style="grid-column:${month.week + 1}">${esc(month.label)}</span>`).join("");
|
|
5136
|
+
return `<div class="usage-calendar-scroll" tabindex="0" aria-label="每日 Token 用量日历,可横向滚动">
|
|
5137
|
+
<div class="usage-calendar-frame" style="--usage-week-count:${calendar.weekCount}">
|
|
5138
|
+
<div class="usage-calendar-months" aria-hidden="true">${months}</div>
|
|
5139
|
+
<div class="usage-calendar-body">
|
|
5140
|
+
<div class="usage-calendar-weekdays" aria-hidden="true"><span>一</span><span>三</span><span>五</span></div>
|
|
5141
|
+
<div class="usage-calendar-grid" role="grid" aria-label="过去 53 周每日 Token 用量">${cells}</div>
|
|
5142
|
+
</div>
|
|
5143
|
+
</div>
|
|
5144
|
+
</div>
|
|
5145
|
+
<div class="usage-calendar-legend"><span>少</span>${[0, 1, 2, 3, 4].map((level) => `<i data-level="${level}" aria-hidden="true"></i>`).join("")}<span>多</span></div>`;
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
function scrollUsageCalendarsToLatest(root) {
|
|
5149
|
+
window.requestAnimationFrame(() => {
|
|
5150
|
+
root.querySelectorAll(".usage-calendar-scroll").forEach((calendar) => {
|
|
5151
|
+
calendar.scrollLeft = calendar.scrollWidth;
|
|
5152
|
+
});
|
|
5153
|
+
});
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false } = {}) {
|
|
5157
|
+
const summary = usage?.summary ?? {};
|
|
5158
|
+
const totalTokens = Number(summary.totalTokens) || 0;
|
|
5159
|
+
const exactTotal = totalTokens.toLocaleString("zh-CN");
|
|
5160
|
+
const estimatedRequests = Number(summary.estimatedRequestCount) || 0;
|
|
5161
|
+
const requestCount = Number(summary.requestCount) || 0;
|
|
5162
|
+
const cachedInputTokens = Number(summary.cachedInputTokens) || 0;
|
|
5163
|
+
const cacheEligibleInputTokens = Number(summary.cacheEligibleInputTokens) || 0;
|
|
5164
|
+
const cacheDescription = summary.cacheHitRate === null || summary.cacheHitRate === undefined
|
|
5165
|
+
? "供应商尚未返回可计算的缓存明细"
|
|
5166
|
+
: `${cachedInputTokens.toLocaleString("zh-CN")} / ${cacheEligibleInputTokens.toLocaleString("zh-CN")} 个可统计输入 Token 命中缓存`;
|
|
5167
|
+
const estimateNote = estimatedRequests > 0
|
|
5168
|
+
? `其中 ${estimatedRequests.toLocaleString("zh-CN")} 次调用包含历史或供应商缺失用量时的估算。`
|
|
5169
|
+
: "全部用量均来自供应商返回的 Token 统计。";
|
|
5170
|
+
const works = Array.isArray(usage?.works) ? usage.works : [];
|
|
5171
|
+
const workRows = works.map((work) => `<tr>
|
|
5172
|
+
<th scope="row">${esc(work.workTitle)}</th>
|
|
5173
|
+
<td title="${esc(Number(work.totalTokens || 0).toLocaleString("zh-CN"))} Token">${esc(formatTokenCount(work.totalTokens))}</td>
|
|
5174
|
+
<td>${esc(formatTokenCount(work.inputTokens))}</td>
|
|
5175
|
+
<td>${esc(formatTokenCount(work.outputTokens))}</td>
|
|
5176
|
+
<td>${esc(formatCacheHitRate(work.cacheHitRate))}</td>
|
|
5177
|
+
<td>${Number(work.requestCount || 0).toLocaleString("zh-CN")}</td>
|
|
5178
|
+
</tr>`).join("");
|
|
5179
|
+
return `<section class="usage-overview" aria-labelledby="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">
|
|
5180
|
+
<div class="config-section-header"><div><h2 id="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">${esc(title || "Token 用量")}</h2><p>${esc(description || "统计该范围内的全部 AI 调用。")}</p></div></div>
|
|
5181
|
+
<div class="usage-stat-grid">
|
|
5182
|
+
<article class="usage-stat is-primary"><span>总消耗</span><strong title="${esc(exactTotal)} Token">${esc(formatTokenCount(totalTokens))}</strong><small>${esc(exactTotal)} Token</small></article>
|
|
5183
|
+
<article class="usage-stat"><span>输入 Token</span><strong>${esc(formatTokenCount(summary.inputTokens))}</strong><small>${Number(summary.inputTokens || 0).toLocaleString("zh-CN")}</small></article>
|
|
5184
|
+
<article class="usage-stat"><span>输出 Token</span><strong>${esc(formatTokenCount(summary.outputTokens))}</strong><small>${Number(summary.outputTokens || 0).toLocaleString("zh-CN")}</small></article>
|
|
5185
|
+
<article class="usage-stat"><span>缓存命中率</span><strong>${esc(formatCacheHitRate(summary.cacheHitRate))}</strong><small>${esc(cacheDescription)}</small></article>
|
|
5186
|
+
</div>
|
|
5187
|
+
<p class="usage-measurement-note">${requestCount.toLocaleString("zh-CN")} 次有用量记录的调用。${esc(estimateNote)}</p>
|
|
5188
|
+
<section class="usage-calendar-section" aria-labelledby="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">
|
|
5189
|
+
<header><div><h3 id="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">每日用量</h3><p>GitHub 风格网格展示过去 53 周;颜色越深,当天消耗越高。</p></div></header>
|
|
5190
|
+
${tokenUsageCalendarMarkup(usage?.daily)}
|
|
5191
|
+
</section>
|
|
5192
|
+
${showWorks ? `<section class="usage-work-section" aria-labelledby="usage-work-title"><header><div><h3 id="usage-work-title">各作品用量</h3><p>按 Token 总消耗从高到低排列,包含尚未使用 AI 的作品。</p></div></header><div class="usage-work-table-scroll"><table class="usage-work-table"><thead><tr><th>作品</th><th>总消耗</th><th>输入</th><th>输出</th><th>缓存命中率</th><th>调用</th></tr></thead><tbody>${workRows || '<tr><td colspan="6">还没有作品用量记录。</td></tr>'}</tbody></table></div></section>` : ""}
|
|
5193
|
+
</section>`;
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
async function renderPlatformTokenUsage() {
|
|
5197
|
+
const host = $("#platform-usage-content");
|
|
5198
|
+
host.innerHTML = '<div class="empty-state">正在汇总 Token 用量……</div>';
|
|
5199
|
+
const timezoneOffset = -new Date().getTimezoneOffset();
|
|
5200
|
+
const usage = await api(`/api/platform/ai/usage?timezoneOffset=${timezoneOffset}`);
|
|
5201
|
+
host.innerHTML = tokenUsageOverviewMarkup(usage, {
|
|
5202
|
+
title: "项目累计用量",
|
|
5203
|
+
description: "汇总所有作品迄今产生的输入与输出 Token;缓存命中率仅基于供应商返回了缓存明细的调用。",
|
|
5204
|
+
showWorks: true
|
|
5205
|
+
});
|
|
5206
|
+
scrollUsageCalendarsToLatest(host);
|
|
5207
|
+
}
|
|
5208
|
+
|
|
4543
5209
|
async function renderBookAiSettings() {
|
|
4544
|
-
|
|
5210
|
+
if (relationshipSearchIndexRefreshTimer) {
|
|
5211
|
+
clearTimeout(relationshipSearchIndexRefreshTimer);
|
|
5212
|
+
relationshipSearchIndexRefreshTimer = null;
|
|
5213
|
+
}
|
|
5214
|
+
const [settings, providers, models, taskDefaults, relationshipIndex, usage] = await Promise.all([
|
|
4545
5215
|
api(`/api/works/${state.work.id}/ai-settings`),
|
|
4546
5216
|
api("/api/platform/ai/providers"),
|
|
4547
5217
|
api(`/api/works/${state.work.id}/models`),
|
|
4548
|
-
api(`/api/works/${state.work.id}/task-defaults`)
|
|
5218
|
+
api(`/api/works/${state.work.id}/task-defaults`),
|
|
5219
|
+
api(`/api/works/${state.work.id}/ai-settings/relationship-search-index`),
|
|
5220
|
+
api(`/api/works/${state.work.id}/ai-settings/usage?timezoneOffset=${-new Date().getTimezoneOffset()}`)
|
|
4549
5221
|
]);
|
|
4550
5222
|
const host = $("#module-content");
|
|
5223
|
+
const workId = String(state.work.id);
|
|
4551
5224
|
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"]);
|
|
4552
|
-
host.innerHTML = `<section class="config-section"
|
|
5225
|
+
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
5226
|
+
title: "本书 Token 用量",
|
|
5227
|
+
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
5228
|
+
})}</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)}`;
|
|
5229
|
+
scrollUsageCalendarsToLatest(host);
|
|
4553
5230
|
host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
|
|
4554
5231
|
"beforebegin",
|
|
4555
5232
|
`<label><input name="agent-tool" type="checkbox" value="grep" ${agentTools.has("grep") ? "checked" : ""}><span><strong>查询正文关键字</strong><small>从段落索引查询关键字,默认返回前 20 条完整段落和章节信息。</small></span></label>`
|
|
@@ -4562,6 +5239,33 @@ async function renderBookAiSettings() {
|
|
|
4562
5239
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
4563
5240
|
host.querySelectorAll(".config-save-button").forEach((button) => button.classList.add("permission-hidden"));
|
|
4564
5241
|
}
|
|
5242
|
+
const isCurrentRelationshipIndexPanel = () => state.module === "ai-settings"
|
|
5243
|
+
&& String(state.work?.id ?? "") === workId
|
|
5244
|
+
&& Boolean($("#relationship-search-index-status"));
|
|
5245
|
+
const updateRelationshipIndexStatus = (status) => {
|
|
5246
|
+
const statusHost = $("#relationship-search-index-status");
|
|
5247
|
+
if (!statusHost) return;
|
|
5248
|
+
statusHost.innerHTML = relationshipIndexStatusMarkup(status);
|
|
5249
|
+
if (relationshipSearchIndexRefreshTimer) clearTimeout(relationshipSearchIndexRefreshTimer);
|
|
5250
|
+
relationshipSearchIndexRefreshTimer = null;
|
|
5251
|
+
if (!["queued", "building"].includes(String(status.status))) return;
|
|
5252
|
+
relationshipSearchIndexRefreshTimer = setTimeout(async () => {
|
|
5253
|
+
relationshipSearchIndexRefreshTimer = null;
|
|
5254
|
+
if (!isCurrentRelationshipIndexPanel()) return;
|
|
5255
|
+
try {
|
|
5256
|
+
const nextStatus = await api(`/api/works/${workId}/ai-settings/relationship-search-index`);
|
|
5257
|
+
if (isCurrentRelationshipIndexPanel()) updateRelationshipIndexStatus(nextStatus);
|
|
5258
|
+
} catch {
|
|
5259
|
+
// 后台轮询失败时保留当前状态,用户仍可手动刷新。
|
|
5260
|
+
}
|
|
5261
|
+
}, 1_200);
|
|
5262
|
+
};
|
|
5263
|
+
const refreshRelationshipIndexStatus = async () => {
|
|
5264
|
+
const status = await api(`/api/works/${workId}/ai-settings/relationship-search-index`);
|
|
5265
|
+
updateRelationshipIndexStatus(status);
|
|
5266
|
+
return status;
|
|
5267
|
+
};
|
|
5268
|
+
updateRelationshipIndexStatus(relationshipIndex);
|
|
4565
5269
|
$("#save-work-system-prompt").addEventListener("click", async () => {
|
|
4566
5270
|
const button = $("#save-work-system-prompt");
|
|
4567
5271
|
button.disabled = true;
|
|
@@ -4575,6 +5279,46 @@ async function renderBookAiSettings() {
|
|
|
4575
5279
|
button.disabled = false;
|
|
4576
5280
|
}
|
|
4577
5281
|
});
|
|
5282
|
+
$("#sync-relationship-search-index").addEventListener("click", async () => {
|
|
5283
|
+
const button = $("#sync-relationship-search-index");
|
|
5284
|
+
button.disabled = true;
|
|
5285
|
+
try {
|
|
5286
|
+
const status = await api(`/api/works/${workId}/ai-settings/relationship-search-index/sync`, { method: "POST" });
|
|
5287
|
+
updateRelationshipIndexStatus(status);
|
|
5288
|
+
toast(Number(status.queuedSourceCount) > 0
|
|
5289
|
+
? `开始同步 ${status.queuedSourceCount} 项增量任务`
|
|
5290
|
+
: "增量任务队列为空,索引已是最新状态");
|
|
5291
|
+
} catch (error) {
|
|
5292
|
+
toast(error.message, "error");
|
|
5293
|
+
} finally {
|
|
5294
|
+
button.disabled = false;
|
|
5295
|
+
}
|
|
5296
|
+
});
|
|
5297
|
+
$("#refresh-relationship-search-index").addEventListener("click", async () => {
|
|
5298
|
+
const button = $("#refresh-relationship-search-index");
|
|
5299
|
+
button.disabled = true;
|
|
5300
|
+
try {
|
|
5301
|
+
await refreshRelationshipIndexStatus();
|
|
5302
|
+
toast("索引状态已刷新", "info");
|
|
5303
|
+
} catch (error) {
|
|
5304
|
+
toast(error.message, "error");
|
|
5305
|
+
} finally {
|
|
5306
|
+
button.disabled = false;
|
|
5307
|
+
}
|
|
5308
|
+
});
|
|
5309
|
+
$("#rebuild-relationship-search-index").addEventListener("click", async () => {
|
|
5310
|
+
const button = $("#rebuild-relationship-search-index");
|
|
5311
|
+
button.disabled = true;
|
|
5312
|
+
try {
|
|
5313
|
+
const status = await api(`/api/works/${workId}/ai-settings/relationship-search-index/rebuild`, { method: "POST" });
|
|
5314
|
+
updateRelationshipIndexStatus(status);
|
|
5315
|
+
toast(`已将全部来源加入索引队列,共 ${status.queuedSourceCount} 项`);
|
|
5316
|
+
} catch (error) {
|
|
5317
|
+
toast(error.message, "error");
|
|
5318
|
+
} finally {
|
|
5319
|
+
button.disabled = false;
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
4578
5322
|
$("#save-book-summary-context-percent").addEventListener("click", async () => {
|
|
4579
5323
|
const button = $("#save-book-summary-context-percent");
|
|
4580
5324
|
button.disabled = true;
|
|
@@ -6039,9 +6783,10 @@ async function showCharacterHistory() {
|
|
|
6039
6783
|
|
|
6040
6784
|
async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
6041
6785
|
entityEditorReadOnly = readOnly;
|
|
6042
|
-
[state.races, state.organizations] = await Promise.all([
|
|
6786
|
+
[state.races, state.organizations, state.characters] = await Promise.all([
|
|
6043
6787
|
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
6044
|
-
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
6788
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([]),
|
|
6789
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
6045
6790
|
]);
|
|
6046
6791
|
characterEditorItem = item ?? null;
|
|
6047
6792
|
characterEditorVersions = [];
|
|
@@ -6390,6 +7135,7 @@ async function openRelationshipDialog(item, options = {}) {
|
|
|
6390
7135
|
<div><strong>关系操作</strong><small>删除操作会记录到版本历史和审计日志。</small></div>
|
|
6391
7136
|
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-relationship-delete>删除关系</button></div>
|
|
6392
7137
|
</section>` : "";
|
|
7138
|
+
setRelationshipPresence(item?.id ?? null);
|
|
6393
7139
|
openDialog(item ? "编辑人物关系" : "新建人物关系", field("from", "起点人物", "select", item?.fromCharacterId ?? defaultFrom, characterOptions) + field("to", "终点人物", "select", item?.toCharacterId ?? defaultTo, characterOptions) + field("category", "关系大类", "select", item?.category ?? "social", [["family", "亲属"], ["social", "社交"], ["emotional", "情感"], ["conflict", "冲突"], ["uncertain", "未确定"]]) + field("subtype", "关系子类", "text", item?.subtype) + field("keywords", "关系关键词", "keyword-chips", item?.keywords ?? []) + field("confidence", "置信度(0-1)", "number", item?.confidence ?? "1") + field("directed", "有方向性", "checkbox", item?.directed ?? false) + management, async (form) => {
|
|
6394
7140
|
const keywords = uniqueRelationshipKeywords(form.getAll("keywords").map(String));
|
|
6395
7141
|
await api(item ? `/api/relationships/${item.id}` : `/api/works/${state.work.id}/relationships`, { method: item ? "PATCH" : "POST", body: { fromCharacterId: form.get("from"), toCharacterId: form.get("to"), category: form.get("category"), subtype: form.get("subtype"), keywords, confidence: Number(form.get("confidence")), directed: form.get("directed") === "on", confirmationStatus: item?.confirmationStatus ?? "confirmed", ...(item ? { expectedVersionNo: item.versionNo } : {}) } });
|
|
@@ -6397,15 +7143,19 @@ async function openRelationshipDialog(item, options = {}) {
|
|
|
6397
7143
|
}, item ? "关系档案" : "人工确认关系");
|
|
6398
7144
|
$("#dialog-fields").querySelector("[data-dialog-relationship-delete]")?.addEventListener("click", async () => {
|
|
6399
7145
|
const dialog = $("#form-dialog");
|
|
7146
|
+
const reopenDialog = () => {
|
|
7147
|
+
setRelationshipPresence(item.id);
|
|
7148
|
+
dialog.showModal();
|
|
7149
|
+
};
|
|
6400
7150
|
dialog.close();
|
|
6401
|
-
if (!await confirmToast(`确认删除人物关系“${relationshipName}”吗?`, { title: "删除人物关系", confirmLabel: "继续删除" })) return
|
|
6402
|
-
if (!await confirmToast(`删除人物关系“${relationshipName}”后无法恢复。`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return
|
|
7151
|
+
if (!await confirmToast(`确认删除人物关系“${relationshipName}”吗?`, { title: "删除人物关系", confirmLabel: "继续删除" })) return reopenDialog();
|
|
7152
|
+
if (!await confirmToast(`删除人物关系“${relationshipName}”后无法恢复。`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return reopenDialog();
|
|
6403
7153
|
try {
|
|
6404
7154
|
await api(`/api/relationships/${item.id}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
6405
7155
|
await Promise.all([refreshRelationshipSurfaces(options.characterId ?? null), loadAiReferences()]);
|
|
6406
7156
|
toast(`已删除人物关系“${relationshipName}”`);
|
|
6407
7157
|
} catch (error) {
|
|
6408
|
-
|
|
7158
|
+
reopenDialog();
|
|
6409
7159
|
toast(error.message, "error");
|
|
6410
7160
|
}
|
|
6411
7161
|
});
|
|
@@ -6470,30 +7220,76 @@ async function openTaskDialog() {
|
|
|
6470
7220
|
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
6471
7221
|
${relationshipCharacterPicker}
|
|
6472
7222
|
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
|
|
6473
|
-
<div class="relationship-overwrite-card
|
|
7223
|
+
<div class="relationship-overwrite-card relationship-prefilter-card">
|
|
7224
|
+
<label class="checkbox-field"><input name="preFilterRelationshipSources" type="checkbox" checked disabled><span>分析前按人物名称和拼音过滤来源</span></label>
|
|
7225
|
+
<p>仅对定向人物分析生效。取消勾选后,将跳过前置过滤,无差别发送所选范围内的全部章节和设定来源。</p>
|
|
7226
|
+
</div>
|
|
7227
|
+
<div class="relationship-overwrite-card relationship-change-preview-card">
|
|
7228
|
+
<label class="checkbox-field"><input name="previewRelationshipChanges" type="checkbox" checked><span>分析完成后先预览关系变更</span></label>
|
|
7229
|
+
<p>建议保持开启。分析只生成新增、更新和删除清单,确认应用前不会修改人物关系库;取消勾选则沿用直接写入逻辑。</p>
|
|
7230
|
+
</div>
|
|
7231
|
+
<section class="relationship-source-preview-card" aria-labelledby="relationship-source-preview-title">
|
|
7232
|
+
<div class="relationship-source-preview-header">
|
|
7233
|
+
<div><strong id="relationship-source-preview-title">发送来源预检</strong><p>创建任务前查看将发送的章节与设定,并可取消误命中的来源。疑似拼音写法可能调用当前任务模型确认身份。</p></div>
|
|
7234
|
+
<button type="button" class="ghost-button" data-relationship-source-preview disabled>预览来源</button>
|
|
7235
|
+
</div>
|
|
7236
|
+
<div class="relationship-source-preview-content" data-relationship-source-preview-content role="status" aria-live="polite">选择至少一个被分析角色后即可预览。</div>
|
|
7237
|
+
</section>
|
|
7238
|
+
<div class="relationship-overwrite-card relationship-existing-overwrite-card hidden">
|
|
6474
7239
|
<label class="checkbox-field"><input name="replaceExistingRelationships" type="checkbox" disabled><span>用本次结果覆盖所选角色的已有关系</span></label>
|
|
6475
7240
|
<p>勾选后,任务成功时会先删除所有涉及所选角色的旧关系,再写入本次分析结果;不勾选则只追加新关系。</p>
|
|
6476
7241
|
</div>
|
|
6477
7242
|
<label>额外分析提示<textarea name="additionalPrompt" maxlength="10000" placeholder="例如:重点识别权力继承、师承变化或隐秘亲缘关系"></textarea><small>将同时追加到证据收集和全局关系归纳提示词,仅影响本次任务。</small></label>
|
|
6478
7243
|
</div>`;
|
|
6479
|
-
|
|
7244
|
+
let relationshipSourcePreview = null;
|
|
7245
|
+
let relationshipSourcePreviewConfigKey = "";
|
|
7246
|
+
const buildRelationshipScope = (form) => {
|
|
6480
7247
|
const taskType = String(form.get("taskType"));
|
|
6481
|
-
const modelId = String(form.get("modelId"));
|
|
6482
7248
|
const scopeType = String(form.get("scopeType"));
|
|
6483
7249
|
const includeAllSettings = taskType === "relationship-analysis" && scopeType === "book-with-settings";
|
|
6484
7250
|
const settingsOnly = taskType === "relationship-analysis" && scopeType === "settings";
|
|
6485
7251
|
const additionalPrompt = taskType === "relationship-analysis" ? String(form.get("additionalPrompt") ?? "").trim() : "";
|
|
6486
7252
|
const characterIds = taskType === "relationship-analysis" ? form.getAll("characterIds").map(String).filter(Boolean) : [];
|
|
7253
|
+
const preFilterRelationshipSources = characterIds.length > 0 && form.get("preFilterRelationshipSources") === "on";
|
|
7254
|
+
const previewRelationshipChanges = taskType === "relationship-analysis" && form.get("previewRelationshipChanges") === "on";
|
|
6487
7255
|
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
6488
7256
|
const scope = settingsOnly
|
|
6489
|
-
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
7257
|
+
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6490
7258
|
: taskType === "character-identity-audit" || scopeType === "book" || includeAllSettings
|
|
6491
|
-
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6492
|
-
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
7259
|
+
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
7260
|
+
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
7261
|
+
return scope;
|
|
7262
|
+
};
|
|
7263
|
+
const relationshipPreviewKey = (scope, modelId) => JSON.stringify({
|
|
7264
|
+
type: scope.type,
|
|
7265
|
+
chapterId: scope.chapterId ?? null,
|
|
7266
|
+
includeAllSettings: scope.includeAllSettings === true,
|
|
7267
|
+
characterIds: scope.characterIds ?? [],
|
|
7268
|
+
preFilterRelationshipSources: scope.preFilterRelationshipSources !== false,
|
|
7269
|
+
modelId
|
|
7270
|
+
});
|
|
7271
|
+
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
7272
|
+
const taskType = String(form.get("taskType"));
|
|
7273
|
+
const modelId = String(form.get("modelId"));
|
|
7274
|
+
const scope = buildRelationshipScope(form);
|
|
7275
|
+
if (taskType === "relationship-analysis" && relationshipSourcePreview
|
|
7276
|
+
&& relationshipSourcePreviewConfigKey === relationshipPreviewKey(scope, modelId)) {
|
|
7277
|
+
scope.relationshipSourceRefs = [...$("#dialog-fields").querySelectorAll("[data-relationship-source-selected]:checked")]
|
|
7278
|
+
.map((input) => ({
|
|
7279
|
+
sourceType: input.dataset.sourceType,
|
|
7280
|
+
sourceId: input.dataset.sourceId,
|
|
7281
|
+
sourceVersion: input.dataset.sourceVersion
|
|
7282
|
+
}));
|
|
7283
|
+
}
|
|
6493
7284
|
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
|
|
7285
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
6494
7286
|
taskListPage = 1;
|
|
6495
|
-
|
|
6496
|
-
|
|
7287
|
+
try {
|
|
7288
|
+
await renderTasks(1);
|
|
7289
|
+
toast("分析任务已创建,已进入任务队列");
|
|
7290
|
+
} catch (error) {
|
|
7291
|
+
toast(`任务已创建,但列表刷新失败:${error.message}`, "error");
|
|
7292
|
+
}
|
|
6497
7293
|
}, "AI 分析", {
|
|
6498
7294
|
submitLabel: "创建任务",
|
|
6499
7295
|
pendingLabel: "创建中…",
|
|
@@ -6517,8 +7313,11 @@ async function openTaskDialog() {
|
|
|
6517
7313
|
const relationshipCharacterCount = relationshipOptions.querySelector("[data-relationship-character-count]");
|
|
6518
7314
|
const relationshipCharacterClear = relationshipOptions.querySelector("[data-relationship-character-clear]");
|
|
6519
7315
|
const relationshipCharacterEmpty = relationshipOptions.querySelector("[data-relationship-character-empty]");
|
|
7316
|
+
const preFilterRelationships = relationshipOptions.querySelector('input[name="preFilterRelationshipSources"]');
|
|
7317
|
+
const relationshipSourcePreviewButton = relationshipOptions.querySelector("[data-relationship-source-preview]");
|
|
7318
|
+
const relationshipSourcePreviewContent = relationshipOptions.querySelector("[data-relationship-source-preview-content]");
|
|
6520
7319
|
const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
|
|
6521
|
-
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-overwrite-card");
|
|
7320
|
+
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-existing-overwrite-card");
|
|
6522
7321
|
const allSettingsOption = document.createElement("option");
|
|
6523
7322
|
allSettingsOption.value = "book-with-settings";
|
|
6524
7323
|
allSettingsOption.textContent = "全书 + 设定集";
|
|
@@ -6546,6 +7345,41 @@ async function openTaskDialog() {
|
|
|
6546
7345
|
relationshipCharacterClear.disabled = selected.length === 0;
|
|
6547
7346
|
relationshipCharacterTrigger.setAttribute("aria-label", `筛选被分析角色,已选择 ${selected.length} 个`);
|
|
6548
7347
|
};
|
|
7348
|
+
const invalidateRelationshipSourcePreview = () => {
|
|
7349
|
+
relationshipSourcePreview = null;
|
|
7350
|
+
relationshipSourcePreviewConfigKey = "";
|
|
7351
|
+
relationshipSourcePreviewContent.classList.remove("is-loading");
|
|
7352
|
+
relationshipSourcePreviewContent.textContent = relationshipCharacterInputs.some((input) => input.checked)
|
|
7353
|
+
? "分析配置已变化,请重新预览将发送的来源。"
|
|
7354
|
+
: "选择至少一个被分析角色后即可预览。";
|
|
7355
|
+
};
|
|
7356
|
+
const relationshipSourceTypeLabel = (sourceType) => ({
|
|
7357
|
+
chapter: "章节",
|
|
7358
|
+
work: "作品资料",
|
|
7359
|
+
setting: "设定",
|
|
7360
|
+
character: "人物档案",
|
|
7361
|
+
race: "种族",
|
|
7362
|
+
organization: "组织",
|
|
7363
|
+
"timeline-track": "时间轴",
|
|
7364
|
+
"timeline-event": "时间线事件",
|
|
7365
|
+
relationship: "已有关系",
|
|
7366
|
+
"chapter-outline": "章节大纲",
|
|
7367
|
+
foreshadow: "伏笔",
|
|
7368
|
+
review: "审核项"
|
|
7369
|
+
}[sourceType] ?? sourceType);
|
|
7370
|
+
const renderRelationshipSourcePreview = (preview) => {
|
|
7371
|
+
const summary = `<div class="relationship-source-preview-summary"><strong>${preview.sourceCount} 条来源</strong><span>${preview.chapterCount} 章</span><span>${preview.settingCount} 条设定资料</span><span>约 ${Number(preview.totalCharacters).toLocaleString("zh-CN")} 字符</span><span>预计 ${preview.estimatedBatchCount} 个批次</span></div>`;
|
|
7372
|
+
if (!preview.sources.length) {
|
|
7373
|
+
relationshipSourcePreviewContent.innerHTML = `${summary}<p class="relationship-source-preview-empty">当前配置没有命中任何来源。仍可创建任务,但任务不会向模型发送正文或设定。</p>`;
|
|
7374
|
+
return;
|
|
7375
|
+
}
|
|
7376
|
+
const rows = preview.sources.map((source) => `<label class="relationship-source-preview-row">
|
|
7377
|
+
<input type="checkbox" data-relationship-source-selected data-source-type="${esc(source.sourceType)}" data-source-id="${esc(source.sourceId)}" data-source-version="${esc(source.version)}" checked>
|
|
7378
|
+
<span class="relationship-source-preview-main"><strong>${esc(source.title)}</strong><small>${esc(relationshipSourceTypeLabel(source.sourceType))} · ${Number(source.characterCount).toLocaleString("zh-CN")} 字符</small></span>
|
|
7379
|
+
<span class="relationship-source-preview-match ${source.matchType === "fuzzy" ? "is-fuzzy" : ""}">${source.matchType === "exact" ? "名称命中" : source.matchType === "fuzzy" ? "疑似写法确认" : "范围内来源"}</span>
|
|
7380
|
+
</label>`).join("");
|
|
7381
|
+
relationshipSourcePreviewContent.innerHTML = `${summary}<div class="relationship-source-preview-list" role="group" aria-label="选择要发送的来源">${rows}</div>`;
|
|
7382
|
+
};
|
|
6549
7383
|
const filterRelationshipCharacters = () => {
|
|
6550
7384
|
const query = relationshipCharacterSearch.value.trim().toLocaleLowerCase();
|
|
6551
7385
|
let visibleCount = 0;
|
|
@@ -6576,6 +7410,8 @@ async function openTaskDialog() {
|
|
|
6576
7410
|
for (const input of relationshipCharacterInputs) input.disabled = !enabled;
|
|
6577
7411
|
if (!enabled) setRelationshipCharacterBubbleOpen(false);
|
|
6578
7412
|
const hasSelectedCharacters = enabled && relationshipCharacterInputs.some((input) => input.checked);
|
|
7413
|
+
preFilterRelationships.disabled = !hasSelectedCharacters;
|
|
7414
|
+
relationshipSourcePreviewButton.disabled = !hasSelectedCharacters || !taskModelSelect.value;
|
|
6579
7415
|
replaceRelationships.disabled = !hasSelectedCharacters;
|
|
6580
7416
|
relationshipOverwriteCard.classList.toggle("hidden", !hasSelectedCharacters);
|
|
6581
7417
|
if (!hasSelectedCharacters) replaceRelationships.checked = false;
|
|
@@ -6590,17 +7426,56 @@ async function openTaskDialog() {
|
|
|
6590
7426
|
taskTypeSelect.addEventListener("change", () => {
|
|
6591
7427
|
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
6592
7428
|
syncTaskModelDefault();
|
|
7429
|
+
invalidateRelationshipSourcePreview();
|
|
7430
|
+
syncRelationshipOptions();
|
|
7431
|
+
});
|
|
7432
|
+
taskModelSelect.addEventListener("change", () => {
|
|
7433
|
+
invalidateRelationshipSourcePreview();
|
|
6593
7434
|
syncRelationshipOptions();
|
|
6594
7435
|
});
|
|
6595
7436
|
relationshipCharacterTrigger.addEventListener("click", () => {
|
|
6596
7437
|
setRelationshipCharacterBubbleOpen(relationshipCharacterTrigger.getAttribute("aria-expanded") !== "true");
|
|
6597
7438
|
});
|
|
6598
7439
|
relationshipCharacterSearch.addEventListener("input", filterRelationshipCharacters);
|
|
6599
|
-
for (const input of relationshipCharacterInputs) input.addEventListener("change",
|
|
7440
|
+
for (const input of relationshipCharacterInputs) input.addEventListener("change", () => {
|
|
7441
|
+
invalidateRelationshipSourcePreview();
|
|
7442
|
+
syncRelationshipOptions();
|
|
7443
|
+
});
|
|
6600
7444
|
relationshipCharacterClear.addEventListener("click", () => {
|
|
6601
7445
|
for (const input of relationshipCharacterInputs) input.checked = false;
|
|
7446
|
+
invalidateRelationshipSourcePreview();
|
|
6602
7447
|
syncRelationshipOptions();
|
|
6603
7448
|
});
|
|
7449
|
+
preFilterRelationships.addEventListener("change", invalidateRelationshipSourcePreview);
|
|
7450
|
+
relationshipSourcePreviewButton.addEventListener("click", async () => {
|
|
7451
|
+
const form = new FormData($("#dynamic-form"));
|
|
7452
|
+
const scope = buildRelationshipScope(form);
|
|
7453
|
+
const modelId = String(form.get("modelId") ?? "");
|
|
7454
|
+
if (!scope.characterIds?.length) return toast("请先选择至少一个被分析角色", "error");
|
|
7455
|
+
relationshipSourcePreviewButton.disabled = true;
|
|
7456
|
+
relationshipSourcePreviewButton.textContent = "预检中…";
|
|
7457
|
+
relationshipSourcePreviewContent.classList.add("is-loading");
|
|
7458
|
+
relationshipSourcePreviewContent.textContent = "正在构建索引并核对来源,请稍候。";
|
|
7459
|
+
try {
|
|
7460
|
+
const preview = await api(`/api/works/${state.work.id}/tasks/relationship-source-preview`, {
|
|
7461
|
+
method: "POST",
|
|
7462
|
+
body: { scope, modelId }
|
|
7463
|
+
});
|
|
7464
|
+
relationshipSourcePreview = preview;
|
|
7465
|
+
relationshipSourcePreviewConfigKey = relationshipPreviewKey(scope, modelId);
|
|
7466
|
+
relationshipSourcePreviewContent.classList.remove("is-loading");
|
|
7467
|
+
renderRelationshipSourcePreview(preview);
|
|
7468
|
+
} catch (error) {
|
|
7469
|
+
relationshipSourcePreview = null;
|
|
7470
|
+
relationshipSourcePreviewConfigKey = "";
|
|
7471
|
+
relationshipSourcePreviewContent.classList.remove("is-loading");
|
|
7472
|
+
relationshipSourcePreviewContent.textContent = `来源预检失败:${error.message}`;
|
|
7473
|
+
toast(`来源预检失败:${error.message}`, "error");
|
|
7474
|
+
} finally {
|
|
7475
|
+
relationshipSourcePreviewButton.textContent = "重新预览";
|
|
7476
|
+
syncRelationshipOptions();
|
|
7477
|
+
}
|
|
7478
|
+
});
|
|
6604
7479
|
$("#dynamic-form").onclick = (event) => {
|
|
6605
7480
|
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
6606
7481
|
};
|
|
@@ -6610,18 +7485,33 @@ async function openTaskDialog() {
|
|
|
6610
7485
|
setRelationshipCharacterBubbleOpen(false);
|
|
6611
7486
|
relationshipCharacterTrigger.focus();
|
|
6612
7487
|
};
|
|
6613
|
-
scopeTypeSelect.addEventListener("change",
|
|
7488
|
+
scopeTypeSelect.addEventListener("change", () => {
|
|
7489
|
+
invalidateRelationshipSourcePreview();
|
|
7490
|
+
syncChapterField();
|
|
7491
|
+
});
|
|
7492
|
+
chapterSelect.addEventListener("change", invalidateRelationshipSourcePreview);
|
|
6614
7493
|
syncRelationshipOptions();
|
|
6615
7494
|
}
|
|
6616
7495
|
|
|
6617
7496
|
function openProviderDialog(item) {
|
|
6618
|
-
|
|
6619
|
-
|
|
7497
|
+
const protocol = item?.protocol ?? "openai-chat-completions";
|
|
7498
|
+
const defaultBaseUrl = protocol === "anthropic-messages" ? "https://api.anthropic.com" : "https://api.openai.com/v1";
|
|
7499
|
+
openDialog(item ? "编辑 AI 供应商" : "新建 AI 供应商", field("name", "显示名称", "text", item?.name) + field("protocol", "接口协议", "select", protocol, [["openai-chat-completions", "OpenAI Chat Completions"], ["anthropic-messages", "Anthropic Messages"]]) + field("baseUrl", "API 基础地址", "url", item?.baseUrl ?? defaultBaseUrl) + field("apiKey", item ? "替换 API 密钥(留空则不变)" : "API 密钥", "password") + field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10) + field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10) + field("maxTokens", "最大输出令牌数", "number", item?.maxTokens ?? 32000) + field("note", "用途备注", "textarea", item?.note) + field("enabled", item ? "启用供应商" : "立即启用", "checkbox", item ? item.status === "enabled" : true), async (form) => {
|
|
7500
|
+
const body = { name: form.get("name"), protocol: form.get("protocol"), baseUrl: form.get("baseUrl"), concurrencyLimit: Number(form.get("concurrencyLimit")), rpmLimit: Number(form.get("rpmLimit")), maxTokens: Number(form.get("maxTokens")), note: form.get("note"), status: form.get("enabled") === "on" ? "enabled" : "disabled" };
|
|
6620
7501
|
if (!item || String(form.get("apiKey") ?? "").trim()) body.apiKey = form.get("apiKey");
|
|
6621
7502
|
await api(item ? `/api/providers/${item.id}` : "/api/platform/ai/providers", { method: item ? "PATCH" : "POST", body });
|
|
6622
7503
|
await renderPlatformAiConfig();
|
|
6623
7504
|
await loadModels();
|
|
6624
|
-
}, item ? "
|
|
7505
|
+
}, item ? "协议、限流与凭据" : "OpenAI / Anthropic 兼容协议");
|
|
7506
|
+
if (!item) {
|
|
7507
|
+
const protocolSelect = $("#dialog-fields select[name='protocol']");
|
|
7508
|
+
const baseUrlInput = $("#dialog-fields input[name='baseUrl']");
|
|
7509
|
+
protocolSelect.addEventListener("change", () => {
|
|
7510
|
+
baseUrlInput.value = protocolSelect.value === "anthropic-messages"
|
|
7511
|
+
? "https://api.anthropic.com"
|
|
7512
|
+
: "https://api.openai.com/v1";
|
|
7513
|
+
});
|
|
7514
|
+
}
|
|
6625
7515
|
}
|
|
6626
7516
|
|
|
6627
7517
|
function openModelDialog(providerId, item = null) {
|
|
@@ -7037,17 +7927,72 @@ async function openImportHistory() {
|
|
|
7037
7927
|
|
|
7038
7928
|
async function showChapterInsight() {
|
|
7039
7929
|
if (!state.chapter) return;
|
|
7040
|
-
const
|
|
7041
|
-
|
|
7930
|
+
const chapterId = state.chapter.id;
|
|
7931
|
+
dismissChapterInsightToast();
|
|
7932
|
+
const requestId = ++chapterInsightRequestId;
|
|
7933
|
+
const insights = await api(`/api/chapters/${chapterId}/insights`);
|
|
7934
|
+
if (requestId !== chapterInsightRequestId || state.chapter?.id !== chapterId || $("#editor-view").classList.contains("hidden")) return;
|
|
7042
7935
|
const insight = insights.find((item) => item.chapterVersion === state.chapter.versionNo) ?? insights[0];
|
|
7043
|
-
|
|
7936
|
+
const region = $("#toast-region");
|
|
7937
|
+
const element = document.createElement("section");
|
|
7938
|
+
element.id = "chapter-insight-toast";
|
|
7939
|
+
element.className = "toast chapter-insight-toast";
|
|
7940
|
+
element.setAttribute("role", "region");
|
|
7941
|
+
element.setAttribute("aria-label", "章节概览");
|
|
7942
|
+
const header = document.createElement("header");
|
|
7943
|
+
header.className = "chapter-insight-toast-header";
|
|
7944
|
+
const heading = document.createElement("div");
|
|
7945
|
+
heading.className = "chapter-insight-toast-heading";
|
|
7946
|
+
const title = document.createElement("strong");
|
|
7947
|
+
title.textContent = "章节概览";
|
|
7948
|
+
heading.append(title);
|
|
7949
|
+
if (insight && insight.chapterVersion !== state.chapter.versionNo) {
|
|
7950
|
+
const stale = document.createElement("span");
|
|
7951
|
+
stale.textContent = `基于旧版本 v${insight.chapterVersion}`;
|
|
7952
|
+
heading.append(stale);
|
|
7953
|
+
}
|
|
7954
|
+
const close = document.createElement("button");
|
|
7955
|
+
close.className = "ghost-button chapter-insight-toast-close";
|
|
7956
|
+
close.type = "button";
|
|
7957
|
+
close.textContent = "关闭";
|
|
7958
|
+
close.setAttribute("aria-label", "关闭章节概览");
|
|
7959
|
+
header.append(heading, close);
|
|
7960
|
+
const body = document.createElement("div");
|
|
7961
|
+
body.className = "chapter-insight-toast-body";
|
|
7044
7962
|
if (!insight) {
|
|
7045
|
-
|
|
7046
|
-
|
|
7963
|
+
const empty = document.createElement("p");
|
|
7964
|
+
empty.className = "chapter-insight-toast-empty";
|
|
7965
|
+
empty.textContent = "尚无章节概览。请在“AI 分析”中运行章节理解,完成后可在此查看结果。";
|
|
7966
|
+
body.append(empty);
|
|
7967
|
+
} else {
|
|
7968
|
+
const summary = document.createElement("p");
|
|
7969
|
+
summary.textContent = insight.summary || "暂无梗概";
|
|
7970
|
+
body.append(summary);
|
|
7971
|
+
const eventNames = insight.events.map((event) => typeof event === "string" ? event : (event.name ?? event.description ?? "未命名事件"));
|
|
7972
|
+
const uncertainties = insight.uncertainties.map((item) => typeof item === "string" ? item : JSON.stringify(item));
|
|
7973
|
+
[["事件", eventNames], ["待确认", uncertainties]].forEach(([label, items]) => {
|
|
7974
|
+
if (!items.length) return;
|
|
7975
|
+
const detail = document.createElement("div");
|
|
7976
|
+
detail.className = "chapter-insight-toast-detail";
|
|
7977
|
+
const detailLabel = document.createElement("strong");
|
|
7978
|
+
detailLabel.textContent = label;
|
|
7979
|
+
const detailContent = document.createElement("p");
|
|
7980
|
+
detailContent.textContent = items.join(";");
|
|
7981
|
+
detail.append(detailLabel, detailContent);
|
|
7982
|
+
body.append(detail);
|
|
7983
|
+
});
|
|
7047
7984
|
}
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7985
|
+
element.append(header, body);
|
|
7986
|
+
close.addEventListener("click", dismissChapterInsightToast, { once: true });
|
|
7987
|
+
element.addEventListener("keydown", (event) => {
|
|
7988
|
+
if (event.key !== "Escape") return;
|
|
7989
|
+
event.preventDefault();
|
|
7990
|
+
dismissChapterInsightToast();
|
|
7991
|
+
$("#insight-button").focus();
|
|
7992
|
+
});
|
|
7993
|
+
region.append(element);
|
|
7994
|
+
$("#insight-button").setAttribute("aria-expanded", "true");
|
|
7995
|
+
raiseToastRegion();
|
|
7051
7996
|
}
|
|
7052
7997
|
|
|
7053
7998
|
$("#home-button").addEventListener("click", async () => {
|
|
@@ -7594,6 +8539,20 @@ $("#register-form").addEventListener("submit", async (event) => {
|
|
|
7594
8539
|
$("#settings-return").addEventListener("click", () => returnFromSettings().catch((error) => toast(error.message, "error")));
|
|
7595
8540
|
$("#platform-ai-button").addEventListener("click", () => showPlatformAi().catch((error) => toast(error.message, "error")));
|
|
7596
8541
|
$("#platform-ai-return").addEventListener("click", () => returnToSettingsHub("#platform-ai-button").catch((error) => toast(error.message, "error")));
|
|
8542
|
+
$("#platform-usage-button").addEventListener("click", () => showPlatformUsage().catch((error) => toast(error.message, "error")));
|
|
8543
|
+
$("#platform-usage-return").addEventListener("click", () => returnToSettingsHub("#platform-usage-button").catch((error) => toast(error.message, "error")));
|
|
8544
|
+
$("#platform-usage-refresh").addEventListener("click", async () => {
|
|
8545
|
+
const button = $("#platform-usage-refresh");
|
|
8546
|
+
button.disabled = true;
|
|
8547
|
+
try {
|
|
8548
|
+
await renderPlatformTokenUsage();
|
|
8549
|
+
toast("Token 用量已刷新");
|
|
8550
|
+
} catch (error) {
|
|
8551
|
+
toast(error.message, "error");
|
|
8552
|
+
} finally {
|
|
8553
|
+
button.disabled = false;
|
|
8554
|
+
}
|
|
8555
|
+
});
|
|
7597
8556
|
$("#user-management-button").addEventListener("click", openUsersDialog);
|
|
7598
8557
|
$("#platform-ui-settings-button").addEventListener("click", openPlatformUiSettingsDialog);
|
|
7599
8558
|
$("#collaboration-button").addEventListener("click", () => openMembersDialog());
|
|
@@ -7617,7 +8576,14 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
7617
8576
|
body: {
|
|
7618
8577
|
toastPosition: $("#toast-position").value,
|
|
7619
8578
|
pageSizes: {
|
|
8579
|
+
settings: Number($("#page-size-settings").value),
|
|
7620
8580
|
characters: Number($("#page-size-characters").value),
|
|
8581
|
+
races: Number($("#page-size-races").value),
|
|
8582
|
+
organizations: Number($("#page-size-organizations").value),
|
|
8583
|
+
timeline: Number($("#page-size-timeline").value),
|
|
8584
|
+
outlines: Number($("#page-size-outlines").value),
|
|
8585
|
+
relationships: Number($("#page-size-relationships").value),
|
|
8586
|
+
reviews: Number($("#page-size-reviews").value),
|
|
7621
8587
|
analysisTasks: Number($("#page-size-analysis-tasks").value),
|
|
7622
8588
|
fileVersions: Number($("#page-size-file-versions").value)
|
|
7623
8589
|
}
|
|
@@ -7626,6 +8592,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
7626
8592
|
applyPlatformUiSettings(settings);
|
|
7627
8593
|
characterListPage = 1;
|
|
7628
8594
|
taskListPage = 1;
|
|
8595
|
+
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
7629
8596
|
$("#platform-ui-settings-dialog").close();
|
|
7630
8597
|
toast("界面与分页设置已保存");
|
|
7631
8598
|
} catch (error) {
|
|
@@ -7641,6 +8608,9 @@ $("#members-dialog").addEventListener("close", () => {
|
|
|
7641
8608
|
memberDialogMembers = [];
|
|
7642
8609
|
memberDialogDirectory = [];
|
|
7643
8610
|
});
|
|
8611
|
+
$("#form-dialog").addEventListener("close", () => {
|
|
8612
|
+
if (relationshipPresenceId && !$("#form-dialog").open) setRelationshipPresence(null);
|
|
8613
|
+
});
|
|
7644
8614
|
$("#member-user-select").addEventListener("change", () => selectMemberForConfiguration($("#member-user-select").value));
|
|
7645
8615
|
$("#member-permission-form").querySelectorAll("[data-permission-preset]").forEach((button) => button.addEventListener("click", () => {
|
|
7646
8616
|
$("#member-permission-grid").querySelectorAll("[data-member-permission]").forEach((select) => {
|
|
@@ -8047,6 +9017,26 @@ $(".quick-actions").addEventListener("click", (event) => {
|
|
|
8047
9017
|
$("#top-search-button").addEventListener("click", () => {
|
|
8048
9018
|
openSearchDialog().catch((error) => toast(error.message, "error"));
|
|
8049
9019
|
});
|
|
9020
|
+
$("#background-task-button").addEventListener("click", () => {
|
|
9021
|
+
renderBackgroundTaskCenter();
|
|
9022
|
+
const dialog = $("#background-task-dialog");
|
|
9023
|
+
if (!dialog.open) dialog.showModal();
|
|
9024
|
+
void refreshBackgroundTaskCenter();
|
|
9025
|
+
});
|
|
9026
|
+
$("#background-task-dialog").addEventListener("close", scheduleBackgroundTaskCenterRefresh);
|
|
9027
|
+
$("#background-task-refresh").addEventListener("click", async () => {
|
|
9028
|
+
const button = $("#background-task-refresh");
|
|
9029
|
+
button.disabled = true;
|
|
9030
|
+
try {
|
|
9031
|
+
await refreshBackgroundTaskCenter();
|
|
9032
|
+
} finally {
|
|
9033
|
+
button.disabled = false;
|
|
9034
|
+
}
|
|
9035
|
+
});
|
|
9036
|
+
$("#background-task-open-analysis").addEventListener("click", () => {
|
|
9037
|
+
$("#background-task-dialog").close();
|
|
9038
|
+
showModule("tasks").catch((error) => toast(error.message, "error"));
|
|
9039
|
+
});
|
|
8050
9040
|
$("#search-dialog-close").addEventListener("click", () => $("#search-dialog").close());
|
|
8051
9041
|
$("#search-form").addEventListener("submit", async (event) => {
|
|
8052
9042
|
event.preventDefault();
|