@musnows/scriverse 0.4.12 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +175 -4
- package/dist/ai.js.map +1 -1
- package/dist/app.js +30 -11
- package/dist/app.js.map +1 -1
- package/dist/database.js +55 -0
- package/dist/database.js.map +1 -1
- package/dist/pagination.js +1 -1
- package/dist/public/app.js +400 -74
- package/dist/public/global-search.d.ts +12 -0
- package/dist/public/global-search.js +23 -0
- package/dist/public/index.html +17 -4
- package/dist/public/relationship-filters.d.ts +10 -0
- package/dist/public/relationship-filters.js +11 -0
- package/dist/public/relationship-graph.js +59 -3
- package/dist/public/styles.css +209 -2
- package/dist/store.js +110 -32
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +12 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260726-network-
|
|
1
|
+
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260726-network-label-scale";
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
3
|
import { renderMarkdown } from "/markdown.js?v=20260725-ordered-list";
|
|
4
4
|
import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
|
|
@@ -40,7 +40,9 @@ import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20
|
|
|
40
40
|
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
|
|
41
41
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
42
42
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
43
|
+
import { resolveGlobalSearchTarget } from "/global-search.js?v=20260726-search-result-details";
|
|
43
44
|
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
45
|
+
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
44
46
|
import {
|
|
45
47
|
clampCropRect,
|
|
46
48
|
containImageRect,
|
|
@@ -52,6 +54,24 @@ import {
|
|
|
52
54
|
resizeCropRect
|
|
53
55
|
} from "/avatar-crop.js?v=20260725-avatar-crop";
|
|
54
56
|
|
|
57
|
+
const defaultPageSizes = Object.freeze({
|
|
58
|
+
characters: 30,
|
|
59
|
+
analysisTasks: 30,
|
|
60
|
+
fileVersions: 30
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function normalizePageSize(value, fallback = 30) {
|
|
64
|
+
const candidate = Number(value);
|
|
65
|
+
return Number.isInteger(candidate) && candidate >= 10 && candidate <= 100 ? candidate : fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizePageSizes(value) {
|
|
69
|
+
return Object.fromEntries(Object.entries(defaultPageSizes).map(([module, fallback]) => [
|
|
70
|
+
module,
|
|
71
|
+
normalizePageSize(value?.[module], fallback)
|
|
72
|
+
]));
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
const state = {
|
|
56
76
|
user: null,
|
|
57
77
|
csrfToken: null,
|
|
@@ -74,6 +94,7 @@ const state = {
|
|
|
74
94
|
dirty: false,
|
|
75
95
|
pendingImportMeta: null,
|
|
76
96
|
pendingCoverWorkId: null,
|
|
97
|
+
uiSettings: { toastPosition: "bottom-right", pageSizes: { ...defaultPageSizes } },
|
|
77
98
|
relationshipGraph: null,
|
|
78
99
|
galaxy: null,
|
|
79
100
|
relationshipMindMap: null,
|
|
@@ -106,6 +127,7 @@ let collaborationAutoSaveDisabled = false;
|
|
|
106
127
|
let timelineMultiSelectEnabled = false;
|
|
107
128
|
let taskProgressRefreshTimer = null;
|
|
108
129
|
const taskProgressRefreshInterval = 2_500;
|
|
130
|
+
const taskStatusSnapshots = new Map();
|
|
109
131
|
|
|
110
132
|
const chapterTypes = ["正文", "设定", "作者的话", "其他"];
|
|
111
133
|
|
|
@@ -137,6 +159,41 @@ function analysisTaskStatusLabel(status) {
|
|
|
137
159
|
})[String(status)] ?? "未知状态";
|
|
138
160
|
}
|
|
139
161
|
|
|
162
|
+
function normalizedAnalysisTaskStatus(status) {
|
|
163
|
+
const value = String(status);
|
|
164
|
+
return ["pending", "running", "review", "completed", "partial", "expired", "cancelled"].includes(value)
|
|
165
|
+
? value
|
|
166
|
+
: "unknown";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function analysisTaskProgressValue(progress) {
|
|
170
|
+
const value = Number(progress);
|
|
171
|
+
return Math.round(Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0)));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderAnalysisTaskStatus(item) {
|
|
175
|
+
const taskId = String(item.id);
|
|
176
|
+
const status = normalizedAnalysisTaskStatus(item.status);
|
|
177
|
+
const statusChanged = taskStatusSnapshots.get(taskId) !== status;
|
|
178
|
+
taskStatusSnapshots.set(taskId, status);
|
|
179
|
+
const label = analysisTaskStatusLabel(item.status);
|
|
180
|
+
return `<span class="task-status-badge is-${status}${statusChanged ? " is-state-change" : ""}" aria-label="任务状态:${esc(label)}">
|
|
181
|
+
<span class="task-status-indicator" aria-hidden="true"></span>
|
|
182
|
+
<span>${esc(label)}</span>
|
|
183
|
+
</span>`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderAnalysisTaskProgress(item) {
|
|
187
|
+
const status = normalizedAnalysisTaskStatus(item.status);
|
|
188
|
+
const progress = analysisTaskProgressValue(item.progress);
|
|
189
|
+
return `<div class="task-progress is-${status}" aria-label="任务进度 ${progress}%">
|
|
190
|
+
<span class="task-progress-value">${progress}%</span>
|
|
191
|
+
<span class="task-progress-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${progress}">
|
|
192
|
+
<span class="task-progress-fill" style="--task-progress: ${progress}%"></span>
|
|
193
|
+
</span>
|
|
194
|
+
</div>`;
|
|
195
|
+
}
|
|
196
|
+
|
|
140
197
|
function reviewSeverityLabel(severity) {
|
|
141
198
|
return levelLabel(severity);
|
|
142
199
|
}
|
|
@@ -776,8 +833,11 @@ let entityEditorDirty = false;
|
|
|
776
833
|
let entityEditorReadOnly = false;
|
|
777
834
|
let chapterEditorReadOnly = true;
|
|
778
835
|
let characterListPage = 1;
|
|
836
|
+
let taskListPage = 1;
|
|
779
837
|
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
780
838
|
let characterFiltersPanelOpen = false;
|
|
839
|
+
const relationshipFilters = { fromCharacterIds: [], toCharacterIds: [] };
|
|
840
|
+
let relationshipFiltersPanelOpen = false;
|
|
781
841
|
let settingEditorItem = null;
|
|
782
842
|
let characterEditorItem = null;
|
|
783
843
|
let knowledgeEditorItem = null;
|
|
@@ -1891,7 +1951,7 @@ async function api(path, options = {}) {
|
|
|
1891
1951
|
return payload.data;
|
|
1892
1952
|
}
|
|
1893
1953
|
|
|
1894
|
-
async function apiPage(path, page = 1, limit =
|
|
1954
|
+
async function apiPage(path, page = 1, limit = 30) {
|
|
1895
1955
|
const separator = path.includes("?") ? "&" : "?";
|
|
1896
1956
|
const result = await api(`${path}${separator}page=${page}&limit=${limit}`);
|
|
1897
1957
|
if (Array.isArray(result)) return { items: result, page, limit, hasMore: false, nextPage: null };
|
|
@@ -2000,9 +2060,14 @@ function applyAuthenticatedUser(session) {
|
|
|
2000
2060
|
|
|
2001
2061
|
function applyPlatformUiSettings(settings) {
|
|
2002
2062
|
const position = settings?.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2063
|
+
state.uiSettings = { toastPosition: position, pageSizes: normalizePageSizes(settings?.pageSizes) };
|
|
2003
2064
|
$("#toast-region").dataset.position = position;
|
|
2004
2065
|
}
|
|
2005
2066
|
|
|
2067
|
+
function pageSizeFor(module) {
|
|
2068
|
+
return normalizePageSize(state.uiSettings.pageSizes[module], defaultPageSizes[module] ?? 30);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2006
2071
|
async function loadPlatformUiSettings() {
|
|
2007
2072
|
try {
|
|
2008
2073
|
applyPlatformUiSettings(await api("/api/ui-settings"));
|
|
@@ -2495,6 +2560,10 @@ async function openPlatformUiSettingsDialog() {
|
|
|
2495
2560
|
try {
|
|
2496
2561
|
const settings = await api("/api/platform/ui-settings");
|
|
2497
2562
|
$("#toast-position").value = settings.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2563
|
+
const pageSizes = normalizePageSizes(settings.pageSizes);
|
|
2564
|
+
$("#page-size-characters").value = String(pageSizes.characters);
|
|
2565
|
+
$("#page-size-analysis-tasks").value = String(pageSizes.analysisTasks);
|
|
2566
|
+
$("#page-size-file-versions").value = String(pageSizes.fileVersions);
|
|
2498
2567
|
$("#platform-ui-settings-dialog").showModal();
|
|
2499
2568
|
} catch (error) {
|
|
2500
2569
|
toast(error.message, "error");
|
|
@@ -2645,36 +2714,22 @@ async function runWorkSearch() {
|
|
|
2645
2714
|
}
|
|
2646
2715
|
|
|
2647
2716
|
async function openSearchResult(result) {
|
|
2717
|
+
const target = resolveGlobalSearchTarget(result);
|
|
2718
|
+
if (!target) throw new Error("无法打开该搜索结果");
|
|
2648
2719
|
$("#search-dialog").close();
|
|
2649
2720
|
const inSettings = !$("#settings-hub-view").classList.contains("hidden") || !$("#platform-ai-view").classList.contains("hidden");
|
|
2650
2721
|
if (inSettings) await returnFromSettings();
|
|
2651
|
-
if (
|
|
2652
|
-
await selectChapter(
|
|
2653
|
-
return;
|
|
2654
|
-
}
|
|
2655
|
-
if (result.type === "character") {
|
|
2656
|
-
await showModule("characters");
|
|
2657
|
-
const character = state.characters.find((item) => item.id === result.id);
|
|
2658
|
-
if (character) openCharacterEditor(character);
|
|
2659
|
-
return;
|
|
2660
|
-
}
|
|
2661
|
-
if (result.type === "setting") {
|
|
2662
|
-
await showModule("settings");
|
|
2663
|
-
const setting = await api(`/api/settings/${encodeURIComponent(result.id)}`);
|
|
2664
|
-
openSettingEditor(setting);
|
|
2665
|
-
return;
|
|
2666
|
-
}
|
|
2667
|
-
if (result.type === "race") {
|
|
2668
|
-
await showModule("races");
|
|
2669
|
-
const race = state.races.find((item) => item.id === result.id);
|
|
2670
|
-
if (race) openRaceDialog(race);
|
|
2722
|
+
if (target.kind === "chapter") {
|
|
2723
|
+
await selectChapter(target.id);
|
|
2671
2724
|
return;
|
|
2672
2725
|
}
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
}
|
|
2726
|
+
await showModule(target.module);
|
|
2727
|
+
if (state.module !== target.module) return;
|
|
2728
|
+
const item = await api(target.apiPath);
|
|
2729
|
+
if (target.entity === "setting") openSettingEditor(item, { readOnly: true });
|
|
2730
|
+
if (target.entity === "character") await openCharacterEditor(item, { readOnly: true });
|
|
2731
|
+
if (target.entity === "race") await openRaceDialog(item, { readOnly: true });
|
|
2732
|
+
if (target.entity === "organization") await openOrganizationDialog(item, { readOnly: true });
|
|
2678
2733
|
}
|
|
2679
2734
|
|
|
2680
2735
|
async function showSettingsHub() {
|
|
@@ -2772,6 +2827,9 @@ function resetWorkScopedUiCaches() {
|
|
|
2772
2827
|
state.characters = [];
|
|
2773
2828
|
state.settings = [];
|
|
2774
2829
|
characterListPage = 1;
|
|
2830
|
+
relationshipFilters.fromCharacterIds = [];
|
|
2831
|
+
relationshipFilters.toCharacterIds = [];
|
|
2832
|
+
taskListPage = 1;
|
|
2775
2833
|
state.collapsedVolumeIds.clear();
|
|
2776
2834
|
state.collapsedRaceIds.clear();
|
|
2777
2835
|
lastSavedChapterSnapshot = null;
|
|
@@ -3024,7 +3082,7 @@ async function showModule(module) {
|
|
|
3024
3082
|
if (module === "outlines") await renderOutlines();
|
|
3025
3083
|
if (module === "relationships") await renderRelationships();
|
|
3026
3084
|
if (module === "reviews") await renderReviews();
|
|
3027
|
-
if (module === "tasks") await renderTasks();
|
|
3085
|
+
if (module === "tasks") await renderTasks(taskListPage);
|
|
3028
3086
|
if (module === "ai-settings") await renderBookAiSettings();
|
|
3029
3087
|
} catch (error) {
|
|
3030
3088
|
$("#module-content").innerHTML = `<div class="empty-state"><b>载入失败</b>${esc(error.message)}</div>`;
|
|
@@ -3319,6 +3377,15 @@ function mountCharacterFilterToggle() {
|
|
|
3319
3377
|
});
|
|
3320
3378
|
}
|
|
3321
3379
|
|
|
3380
|
+
function mountRelationshipFilterToggle() {
|
|
3381
|
+
$("#module-header-actions").querySelector('[data-module-header-action="relationship-filter-toggle"]')?.remove();
|
|
3382
|
+
$("#module-header-actions").insertAdjacentHTML("afterbegin", `<button type="button" class="module-filter-toggle" data-module-header-action="relationship-filter-toggle" aria-label="筛选关系" aria-controls="relationship-filter-panel" aria-expanded="${relationshipFiltersPanelOpen}" title="筛选关系"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16l-6.5 7.2v5.3l-3 1.5v-6.8L4 5Z"></path></svg></button>`);
|
|
3383
|
+
$("#module-header-actions").querySelector('[data-module-header-action="relationship-filter-toggle"]')?.addEventListener("click", async () => {
|
|
3384
|
+
relationshipFiltersPanelOpen = !relationshipFiltersPanelOpen;
|
|
3385
|
+
await renderRelationships();
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3322
3389
|
function bindRecordPreview(selector, open) {
|
|
3323
3390
|
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
3324
3391
|
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization ?? card.dataset.openReview;
|
|
@@ -3439,13 +3506,14 @@ async function renderSettings() {
|
|
|
3439
3506
|
|
|
3440
3507
|
async function renderCharacters(page = characterListPage) {
|
|
3441
3508
|
const hasCharacterFilters = characterFilters.raceIds.length > 0 || characterFilters.organizationIds.length > 0;
|
|
3509
|
+
const pageSize = pageSizeFor("characters");
|
|
3442
3510
|
const [characterSource, races, organizations] = await Promise.all([
|
|
3443
|
-
hasCharacterFilters ? apiAllPages(`/api/works/${state.work.id}/characters`) : apiPage(`/api/works/${state.work.id}/characters`, page),
|
|
3511
|
+
hasCharacterFilters ? apiAllPages(`/api/works/${state.work.id}/characters`) : apiPage(`/api/works/${state.work.id}/characters`, page, pageSize),
|
|
3444
3512
|
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
3445
3513
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
3446
3514
|
]);
|
|
3447
3515
|
const characterPage = hasCharacterFilters
|
|
3448
|
-
? paginateCharacters(filterCharacters(characterSource, characterFilters), page,
|
|
3516
|
+
? paginateCharacters(filterCharacters(characterSource, characterFilters), page, pageSize)
|
|
3449
3517
|
: characterSource;
|
|
3450
3518
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3451
3519
|
characterListPage = characterPage.page;
|
|
@@ -3748,17 +3816,34 @@ async function renderOutlines() {
|
|
|
3748
3816
|
async function renderRelationships() {
|
|
3749
3817
|
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
3750
3818
|
const relationships = await apiAllPages(`/api/works/${state.work.id}/relationships`);
|
|
3819
|
+
const filteredRelationships = filterRelationships(relationships, relationshipFilters);
|
|
3820
|
+
const hasRelationshipFilters = relationshipFilters.fromCharacterIds.length > 0 || relationshipFilters.toCharacterIds.length > 0;
|
|
3751
3821
|
const canEditRelationships = canEditModule("relationships");
|
|
3752
|
-
mountModuleCount(
|
|
3822
|
+
mountModuleCount(filteredRelationships.length);
|
|
3753
3823
|
const nameOf = (id) => state.characters.find((item) => item.id === id)?.name ?? "未知角色";
|
|
3824
|
+
const selectedFromCharacterIds = new Set(relationshipFilters.fromCharacterIds);
|
|
3825
|
+
const selectedToCharacterIds = new Set(relationshipFilters.toCharacterIds);
|
|
3826
|
+
const selectedFromCharacterNames = state.characters.filter((character) => selectedFromCharacterIds.has(String(character.id))).map((character) => character.name);
|
|
3827
|
+
const selectedToCharacterNames = state.characters.filter((character) => selectedToCharacterIds.has(String(character.id))).map((character) => character.name);
|
|
3828
|
+
const filterOptionList = (selectedIds) => state.characters.map((character) => {
|
|
3829
|
+
const value = String(character.id);
|
|
3830
|
+
return `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedIds.has(value) ? "checked" : ""}><span>${esc(character.name)}</span></label>`;
|
|
3831
|
+
}).join("");
|
|
3832
|
+
const filterToolbar = `<section id="relationship-filter-panel" class="character-filter-toolbar${relationshipFiltersPanelOpen ? "" : " hidden"}" aria-label="关系筛选">
|
|
3833
|
+
<details class="character-filter-dropdown"><summary><span>按起点角色筛选</span><strong>${selectedFromCharacterNames.length ? `已选 ${selectedFromCharacterNames.length} 项` : "全部起点"}</strong></summary><div id="relationship-from-character-filter" class="character-filter-options">${filterOptionList(selectedFromCharacterIds)}</div></details>
|
|
3834
|
+
<details class="character-filter-dropdown"><summary><span>按终点角色筛选</span><strong>${selectedToCharacterNames.length ? `已选 ${selectedToCharacterNames.length} 项` : "全部终点"}</strong></summary><div id="relationship-to-character-filter" class="character-filter-options">${filterOptionList(selectedToCharacterIds)}</div></details>
|
|
3835
|
+
<div class="character-filter-toolbar-actions">${hasRelationshipFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${filteredRelationships.length} 条关系</span>` : ""}<button id="clear-relationship-filters" class="ghost-button" type="button" ${hasRelationshipFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
3836
|
+
</section>`;
|
|
3837
|
+
mountRelationshipFilterToggle();
|
|
3754
3838
|
state.galaxy?.destroy();
|
|
3755
3839
|
state.relationshipExpandedMap?.destroy?.();
|
|
3756
3840
|
if ($("#relationship-map-dialog").open) $("#relationship-map-dialog").close();
|
|
3757
3841
|
const graph = buildRelationshipGraph(state.characters, relationships);
|
|
3758
3842
|
state.relationshipGraph = graph;
|
|
3759
|
-
|
|
3843
|
+
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>${filteredRelationships.map((item) => `
|
|
3760
3844
|
<tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
|
|
3761
|
-
<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>` : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>'
|
|
3845
|
+
<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>';
|
|
3846
|
+
$("#module-content").innerHTML = `${filterToolbar}<div id="relationship-map-host"></div>${relationshipList}`;
|
|
3762
3847
|
const openGalaxy = () => {
|
|
3763
3848
|
state.galaxy?.destroy();
|
|
3764
3849
|
state.galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, { workId: state.work.id });
|
|
@@ -3774,7 +3859,24 @@ async function renderRelationships() {
|
|
|
3774
3859
|
};
|
|
3775
3860
|
state.relationshipMindMap?.destroy?.();
|
|
3776
3861
|
state.relationshipMindMap = renderRelationshipMindMap($("#relationship-map-host"), graph, { onOpenGalaxy: openGalaxy, onOpenExpanded: openExpanded });
|
|
3777
|
-
|
|
3862
|
+
const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
3863
|
+
$("#relationship-from-character-filter").addEventListener("change", async () => {
|
|
3864
|
+
relationshipFiltersPanelOpen = true;
|
|
3865
|
+
relationshipFilters.fromCharacterIds = readSelectedValues("#relationship-from-character-filter");
|
|
3866
|
+
await renderRelationships();
|
|
3867
|
+
});
|
|
3868
|
+
$("#relationship-to-character-filter").addEventListener("change", async () => {
|
|
3869
|
+
relationshipFiltersPanelOpen = true;
|
|
3870
|
+
relationshipFilters.toCharacterIds = readSelectedValues("#relationship-to-character-filter");
|
|
3871
|
+
await renderRelationships();
|
|
3872
|
+
});
|
|
3873
|
+
$("#clear-relationship-filters")?.addEventListener("click", async () => {
|
|
3874
|
+
relationshipFiltersPanelOpen = true;
|
|
3875
|
+
relationshipFilters.fromCharacterIds = [];
|
|
3876
|
+
relationshipFilters.toCharacterIds = [];
|
|
3877
|
+
await renderRelationships();
|
|
3878
|
+
});
|
|
3879
|
+
$("#module-content").querySelectorAll("[data-edit-relationship]").forEach((button) => button.addEventListener("click", () => openRelationshipDialog(filteredRelationships.find((item) => item.id === button.dataset.editRelationship))));
|
|
3778
3880
|
bindEntityHistoryButtons(async () => { await renderRelationships(); await loadAiReferences(); });
|
|
3779
3881
|
}
|
|
3780
3882
|
|
|
@@ -3870,23 +3972,36 @@ async function renderReviews() {
|
|
|
3870
3972
|
}));
|
|
3871
3973
|
}
|
|
3872
3974
|
|
|
3873
|
-
async function renderTasks() {
|
|
3975
|
+
async function renderTasks(page = taskListPage) {
|
|
3874
3976
|
stopTaskProgressRefresh();
|
|
3875
|
-
const
|
|
3876
|
-
|
|
3977
|
+
const pageSize = pageSizeFor("analysisTasks");
|
|
3978
|
+
const [taskPage, settings] = await Promise.all([
|
|
3979
|
+
apiPage(`/api/works/${state.work.id}/tasks`, page, pageSize),
|
|
3877
3980
|
canReadModule("ai-settings")
|
|
3878
3981
|
? api(`/api/works/${state.work.id}/ai-settings`)
|
|
3879
3982
|
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
|
|
3880
3983
|
]);
|
|
3881
|
-
|
|
3984
|
+
if (!taskPage.items.length && page > 1) return renderTasks(page - 1);
|
|
3985
|
+
taskListPage = taskPage.page;
|
|
3986
|
+
const tasks = taskPage.items;
|
|
3987
|
+
const taskTotal = Number(taskPage.total ?? taskPage.stats?.total ?? tasks.length);
|
|
3988
|
+
mountModuleCount(taskTotal);
|
|
3882
3989
|
const canConfigureAutoRun = canEditModule("tasks") && canEditModule("ai-settings");
|
|
3883
|
-
const pendingCount =
|
|
3884
|
-
const
|
|
3885
|
-
const runningCount = runningTasks.length;
|
|
3990
|
+
const pendingCount = Number(taskPage.stats?.pendingCount ?? 0);
|
|
3991
|
+
const runningCount = Number(taskPage.stats?.runningCount ?? 0);
|
|
3886
3992
|
const activeTaskCount = pendingCount + runningCount;
|
|
3887
|
-
const runningProgress = runningCount
|
|
3888
|
-
|
|
3889
|
-
|
|
3993
|
+
const runningProgress = runningCount ? analysisTaskProgressValue(taskPage.stats?.runningProgress) : 0;
|
|
3994
|
+
const visibleTaskIds = new Set(tasks.map((item) => String(item.id)));
|
|
3995
|
+
for (const taskId of taskStatusSnapshots.keys()) {
|
|
3996
|
+
if (!visibleTaskIds.has(taskId)) taskStatusSnapshots.delete(taskId);
|
|
3997
|
+
}
|
|
3998
|
+
const pagination = tasks.length && (taskPage.page > 1 || taskPage.hasMore)
|
|
3999
|
+
? `<nav class="module-pagination" aria-label="AI 分析任务分页">
|
|
4000
|
+
<button type="button" data-task-page="${taskPage.page - 1}" ${taskPage.page <= 1 ? "disabled" : ""}>上一页</button>
|
|
4001
|
+
<span>第 ${taskPage.page}/${Math.max(1, Math.ceil(taskTotal / taskPage.limit))} 页 · 本页 ${tasks.length} 个任务 · 共 ${taskTotal} 个任务</span>
|
|
4002
|
+
<button type="button" data-task-page="${taskPage.nextPage ?? taskPage.page + 1}" ${taskPage.hasMore ? "" : "disabled"}>下一页</button>
|
|
4003
|
+
</nav>`
|
|
4004
|
+
: "";
|
|
3890
4005
|
$("#module-content").innerHTML = `
|
|
3891
4006
|
<section class="task-auto-run-panel ${canConfigureAutoRun ? "" : "hidden"}" aria-labelledby="task-auto-run-title">
|
|
3892
4007
|
<div class="task-auto-run-copy">
|
|
@@ -3904,21 +4019,27 @@ async function renderTasks() {
|
|
|
3904
4019
|
<p class="task-auto-run-meta">待执行队列 ${pendingCount} 个 · 正在运行 ${runningCount} 个</p>
|
|
3905
4020
|
<div class="task-auto-run-progress ${activeTaskCount ? "" : "hidden"}" aria-live="polite">
|
|
3906
4021
|
<div class="task-auto-run-progress-label"><span>${runningCount ? "运行中任务平均进度" : "等待任务开始"}</span><strong>${runningProgress}%</strong></div>
|
|
3907
|
-
<progress class="task-auto-run-progress-bar" max="100" value="${runningProgress}" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}">${runningProgress}%</progress>
|
|
4022
|
+
<progress class="task-auto-run-progress-bar ${runningCount ? "is-running" : "is-waiting"}" max="100" value="${runningProgress}" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}">${runningProgress}%</progress>
|
|
3908
4023
|
</div>
|
|
3909
4024
|
</section>
|
|
3910
4025
|
${tasks.length ? `<table class="table-list task-table"><thead><tr><th>分析类型</th><th>范围</th><th>状态</th><th>进度</th><th>操作</th></tr></thead><tbody>${tasks.map((item) => `
|
|
3911
4026
|
<tr>
|
|
3912
4027
|
<td>${esc(analysisTaskTypeLabel(item.taskType))}</td>
|
|
3913
4028
|
<td>${esc(item.scopeSummary || taskScopeLabel(item.scope?.type || "book"))}</td>
|
|
3914
|
-
<td>${
|
|
3915
|
-
<td>${
|
|
4029
|
+
<td class="task-status-cell">${renderAnalysisTaskStatus(item)}</td>
|
|
4030
|
+
<td class="task-progress-cell">${renderAnalysisTaskProgress(item)}</td>
|
|
3916
4031
|
<td class="task-row-actions">
|
|
3917
4032
|
<button class="ghost-button" type="button" data-task-detail="${esc(item.id)}">详情</button>
|
|
3918
4033
|
${item.status === "pending" ? `<button class="ghost-button" type="button" data-run-task="${esc(item.id)}">运行</button>` : ""}
|
|
3919
4034
|
${item.status === "pending" || item.status === "running" ? `<button class="ghost-button" type="button" data-cancel-task="${esc(item.id)}">取消</button>` : ""}
|
|
3920
4035
|
</td>
|
|
3921
|
-
</tr>`).join("")}</tbody></table
|
|
4036
|
+
</tr>`).join("")}</tbody></table>${pagination}` : emptyModule("还没有 AI 分析记录", "点击“开始 AI 分析”,可分析指定章节或整部作品。")}`;
|
|
4037
|
+
|
|
4038
|
+
$("#module-content").querySelectorAll("[data-task-page]").forEach((button) => button.addEventListener("click", async () => {
|
|
4039
|
+
if (button.disabled) return;
|
|
4040
|
+
$("#module-content").querySelectorAll("[data-task-page]").forEach((control) => { control.disabled = true; });
|
|
4041
|
+
await renderTasks(Number(button.dataset.taskPage));
|
|
4042
|
+
}));
|
|
3922
4043
|
|
|
3923
4044
|
$("#task-auto-run-save")?.addEventListener("click", async () => {
|
|
3924
4045
|
const button = $("#task-auto-run-save");
|
|
@@ -3957,8 +4078,15 @@ async function renderTasks() {
|
|
|
3957
4078
|
$("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
|
|
3958
4079
|
if (button.disabled) return;
|
|
3959
4080
|
button.disabled = true;
|
|
3960
|
-
|
|
3961
|
-
|
|
4081
|
+
const taskId = encodeURIComponent(button.dataset.taskDetail);
|
|
4082
|
+
Promise.all([
|
|
4083
|
+
api(`/api/tasks/${taskId}`),
|
|
4084
|
+
api(`/api/tasks/${taskId}/trace`).catch((error) => {
|
|
4085
|
+
if (error.code === "WORK_MODULE_READ_DENIED") return { restricted: true, captured: false, calls: [] };
|
|
4086
|
+
throw error;
|
|
4087
|
+
})
|
|
4088
|
+
])
|
|
4089
|
+
.then(([task, trace]) => openTaskDetailDialog(task, trace))
|
|
3962
4090
|
.catch((error) => toast(error.message, "error"))
|
|
3963
4091
|
.finally(() => { button.disabled = false; });
|
|
3964
4092
|
}));
|
|
@@ -3967,6 +4095,12 @@ async function renderTasks() {
|
|
|
3967
4095
|
try {
|
|
3968
4096
|
button.disabled = true;
|
|
3969
4097
|
button.textContent = "运行中";
|
|
4098
|
+
const row = button.closest("tr");
|
|
4099
|
+
const optimisticTask = { id: button.dataset.runTask, status: "running", progress: 5 };
|
|
4100
|
+
const statusCell = row?.querySelector(".task-status-cell");
|
|
4101
|
+
const progressCell = row?.querySelector(".task-progress-cell");
|
|
4102
|
+
if (statusCell) statusCell.innerHTML = renderAnalysisTaskStatus(optimisticTask);
|
|
4103
|
+
if (progressCell) progressCell.innerHTML = renderAnalysisTaskProgress(optimisticTask);
|
|
3970
4104
|
const cancel = button.parentElement.querySelector("[data-cancel-task]");
|
|
3971
4105
|
if (cancel) cancel.textContent = "取消运行";
|
|
3972
4106
|
scheduleTaskProgressRefresh(workId, 1);
|
|
@@ -4017,7 +4151,143 @@ function scheduleTaskProgressRefresh(workId, runningCount) {
|
|
|
4017
4151
|
}, taskProgressRefreshInterval);
|
|
4018
4152
|
}
|
|
4019
4153
|
|
|
4020
|
-
function
|
|
4154
|
+
function taskTraceRoleLabel(role) {
|
|
4155
|
+
if (role === "system") return "系统提示词";
|
|
4156
|
+
if (role === "assistant") return "Agent";
|
|
4157
|
+
if (role === "tool") return "工具结果";
|
|
4158
|
+
return "用户提示词";
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
function renderTaskTraceMessages(messages) {
|
|
4162
|
+
if (!Array.isArray(messages) || messages.length === 0) return '<p class="task-trace-empty">本轮没有消息。</p>';
|
|
4163
|
+
return `<div class="task-trace-messages">${messages.map((message, index) => {
|
|
4164
|
+
const role = String(message?.role || "user");
|
|
4165
|
+
const content = message?.content === null ? "" : String(message?.content ?? "");
|
|
4166
|
+
const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
4167
|
+
return `<article class="task-trace-message is-${esc(role)}">
|
|
4168
|
+
<header><span>${esc(taskTraceRoleLabel(role))}</span><small>#${index + 1} · ${content.length.toLocaleString("zh-CN")} 字符</small></header>
|
|
4169
|
+
${content ? `<pre>${esc(content)}</pre>` : '<p class="task-trace-empty">无文本正文</p>'}
|
|
4170
|
+
${toolCalls.length ? `<details><summary>Agent 请求的工具调用(${toolCalls.length})</summary><pre>${esc(JSON.stringify(toolCalls, null, 2))}</pre></details>` : ""}
|
|
4171
|
+
${message?.tool_call_id ? `<small>工具调用 ID:<code>${esc(message.tool_call_id)}</code></small>` : ""}
|
|
4172
|
+
</article>`;
|
|
4173
|
+
}).join("")}</div>`;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
function renderTaskTraceAttempt(attempt) {
|
|
4177
|
+
const response = attempt?.response && typeof attempt.response === "object" ? attempt.response : {};
|
|
4178
|
+
const choice = Array.isArray(response.choices) ? response.choices[0] : null;
|
|
4179
|
+
const message = choice?.message && typeof choice.message === "object" ? choice.message : {};
|
|
4180
|
+
const reasoning = String(message.reasoning_content ?? "");
|
|
4181
|
+
const content = String(message.content ?? "");
|
|
4182
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
4183
|
+
return `<article class="task-trace-attempt is-${esc(attempt?.status || "failed")}">
|
|
4184
|
+
<header>
|
|
4185
|
+
<strong>尝试 ${esc(String(attempt?.attempt ?? 1))}</strong>
|
|
4186
|
+
<span>${attempt?.status === "completed" ? "响应成功" : attempt?.status === "running" ? "等待响应" : "请求失败"}${attempt?.httpStatus ? ` · HTTP ${esc(String(attempt.httpStatus))}` : ""}</span>
|
|
4187
|
+
<small>${esc(formatDateTime(attempt?.startedAt))}${attempt?.completedAt ? ` → ${esc(formatDateTime(attempt.completedAt))}` : ""}</small>
|
|
4188
|
+
</header>
|
|
4189
|
+
${reasoning ? `<details class="task-trace-response"><summary>模型思考过程 · ${reasoning.length.toLocaleString("zh-CN")} 字符</summary><pre>${esc(reasoning)}</pre></details>` : ""}
|
|
4190
|
+
${content ? `<details class="task-trace-response" open><summary>Agent 响应 · ${content.length.toLocaleString("zh-CN")} 字符</summary><pre>${esc(content)}</pre></details>` : ""}
|
|
4191
|
+
${toolCalls.length ? `<details class="task-trace-response" open><summary>Agent 工具请求 · ${toolCalls.length} 项</summary><pre>${esc(JSON.stringify(toolCalls, null, 2))}</pre></details>` : ""}
|
|
4192
|
+
${attempt?.failure ? `<pre class="task-trace-failure">${esc(attempt.failure)}</pre>` : ""}
|
|
4193
|
+
${response.usage ? `<details class="task-trace-response"><summary>Token 用量</summary><pre>${esc(JSON.stringify(response.usage, null, 2))}</pre></details>` : ""}
|
|
4194
|
+
</article>`;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
function renderTaskTraceRound(round) {
|
|
4198
|
+
const messages = Array.isArray(round?.request?.messages) ? round.request.messages : [];
|
|
4199
|
+
const attempts = Array.isArray(round?.attempts) ? round.attempts : [];
|
|
4200
|
+
const executions = Array.isArray(round?.toolExecutions) ? round.toolExecutions : [];
|
|
4201
|
+
return `<section class="task-trace-round">
|
|
4202
|
+
<header class="task-trace-round-header">
|
|
4203
|
+
<span class="task-trace-round-index">${esc(String(round?.round ?? 1))}</span>
|
|
4204
|
+
<div><strong>Agent 轮次 ${esc(String(round?.round ?? 1))}</strong><small>${messages.length} 条消息 · ${attempts.length} 次请求尝试 · ${executions.length} 次工具执行</small></div>
|
|
4205
|
+
<time>${esc(formatDateTime(round?.requestedAt))}</time>
|
|
4206
|
+
</header>
|
|
4207
|
+
<div class="task-trace-flow" aria-label="本轮调用流程">
|
|
4208
|
+
<span>完整 Prompt</span><i aria-hidden="true">→</i><span>模型响应</span>${executions.length ? '<i aria-hidden="true">→</i><span>工具执行</span>' : ""}
|
|
4209
|
+
</div>
|
|
4210
|
+
<details class="task-trace-prompt">
|
|
4211
|
+
<summary>查看本轮发出的完整 Prompt(${messages.length} 条消息)</summary>
|
|
4212
|
+
${renderTaskTraceMessages(messages)}
|
|
4213
|
+
<details class="task-trace-request-meta"><summary>模型参数与工具定义</summary><pre>${esc(JSON.stringify({
|
|
4214
|
+
model: round?.request?.model,
|
|
4215
|
+
parameters: round?.request?.parameters ?? {},
|
|
4216
|
+
toolChoice: round?.request?.toolChoice,
|
|
4217
|
+
tools: round?.request?.tools ?? []
|
|
4218
|
+
}, null, 2))}</pre></details>
|
|
4219
|
+
</details>
|
|
4220
|
+
<div class="task-trace-attempts">${attempts.map(renderTaskTraceAttempt).join("") || '<p class="task-trace-empty">尚未记录模型响应。</p>'}</div>
|
|
4221
|
+
${executions.length ? `<div class="task-trace-tools"><strong>工具执行结果</strong>${executions.map((execution) => `<details>
|
|
4222
|
+
<summary>${esc(execution.name || "未知工具")} · ${execution.status === "completed" ? "成功" : "失败"}</summary>
|
|
4223
|
+
<div class="task-trace-tool-grid">
|
|
4224
|
+
<div><small>调用参数</small><pre>${esc(JSON.stringify(execution.arguments, null, 2))}</pre></div>
|
|
4225
|
+
<div><small>返回结果</small><pre>${esc(JSON.stringify(execution.result, null, 2))}</pre></div>
|
|
4226
|
+
</div>
|
|
4227
|
+
</details>`).join("")}</div>` : ""}
|
|
4228
|
+
</section>`;
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
function renderTaskTraceVisualization(trace) {
|
|
4232
|
+
if (trace?.restricted) {
|
|
4233
|
+
return `<section class="task-trace-section" aria-labelledby="task-trace-title">
|
|
4234
|
+
<header class="task-trace-heading"><div><span class="eyebrow">执行追踪</span><h3 id="task-trace-title">完整全流程上下文</h3></div></header>
|
|
4235
|
+
<div class="task-trace-unavailable"><strong>完整上下文受权限保护</strong><p>当前账号缺少正文或作品资料的读取权限,无法查看原始 Prompt、模型响应与工具结果。</p></div>
|
|
4236
|
+
</section>`;
|
|
4237
|
+
}
|
|
4238
|
+
const calls = Array.isArray(trace?.calls) ? trace.calls : [];
|
|
4239
|
+
const capturedCalls = calls.filter((call) => call.trace);
|
|
4240
|
+
const roundCount = capturedCalls.reduce((total, call) => total + (Array.isArray(call.trace?.rounds) ? call.trace.rounds.length : 0), 0);
|
|
4241
|
+
const toolCount = capturedCalls.reduce((total, call) => total + (Array.isArray(call.trace?.rounds)
|
|
4242
|
+
? call.trace.rounds.reduce((roundTotal, round) => roundTotal + (Array.isArray(round.toolExecutions) ? round.toolExecutions.length : 0), 0)
|
|
4243
|
+
: 0), 0);
|
|
4244
|
+
const promptChars = capturedCalls.reduce((total, call) => total + (Array.isArray(call.trace?.rounds)
|
|
4245
|
+
? call.trace.rounds.reduce((roundTotal, round) => roundTotal + JSON.stringify(round.request?.messages ?? []).length, 0)
|
|
4246
|
+
: 0), 0);
|
|
4247
|
+
const outputChars = capturedCalls.reduce((total, call) => total + Number(call.outputChars || 0), 0);
|
|
4248
|
+
if (!trace?.captured || capturedCalls.length === 0) {
|
|
4249
|
+
return `<section class="task-trace-section" aria-labelledby="task-trace-title">
|
|
4250
|
+
<header class="task-trace-heading"><div><span class="eyebrow">执行追踪</span><h3 id="task-trace-title">完整全流程上下文</h3></div></header>
|
|
4251
|
+
<div class="task-trace-unavailable"><strong>没有可用的全流程记录</strong><p>${calls.length ? "该任务仅保留了调用摘要,没有保存完整 Prompt 与 Agent 轮次。" : "这是追踪功能启用前创建的历史任务,或任务尚未发起任何模型调用。"}</p></div>
|
|
4252
|
+
</section>`;
|
|
4253
|
+
}
|
|
4254
|
+
return `<section class="task-trace-section" aria-labelledby="task-trace-title">
|
|
4255
|
+
<header class="task-trace-heading">
|
|
4256
|
+
<div><span class="eyebrow">执行追踪</span><h3 id="task-trace-title">完整全流程上下文</h3></div>
|
|
4257
|
+
<p>按模型调用与 Agent 轮次还原实际发送内容、模型响应和工具结果。</p>
|
|
4258
|
+
</header>
|
|
4259
|
+
<div class="task-trace-metrics" aria-label="执行追踪统计">
|
|
4260
|
+
<div><strong>${capturedCalls.length}</strong><span>模型调用</span></div>
|
|
4261
|
+
<div><strong>${roundCount}</strong><span>Agent 轮次</span></div>
|
|
4262
|
+
<div><strong>${toolCount}</strong><span>工具执行</span></div>
|
|
4263
|
+
<div><strong>${promptChars.toLocaleString("zh-CN")}</strong><span>Prompt 字符</span></div>
|
|
4264
|
+
<div><strong>${outputChars.toLocaleString("zh-CN")}</strong><span>输出字符</span></div>
|
|
4265
|
+
</div>
|
|
4266
|
+
<div class="task-trace-calls">${capturedCalls.map((call, index) => {
|
|
4267
|
+
const rounds = Array.isArray(call.trace?.rounds) ? call.trace.rounds : [];
|
|
4268
|
+
const modelName = call.model?.displayName || call.model?.modelId || "未知模型";
|
|
4269
|
+
const providerName = call.provider?.name || "未知供应商";
|
|
4270
|
+
return `<details class="task-trace-call is-${esc(call.status || "failed")}" ${index === 0 ? "open" : ""}>
|
|
4271
|
+
<summary>
|
|
4272
|
+
<span class="task-trace-call-index">${index + 1}</span>
|
|
4273
|
+
<span><strong>${esc(modelName)}</strong><small>${esc(providerName)} · ${rounds.length} 轮 · ${Number(call.inputChars || 0).toLocaleString("zh-CN")} → ${Number(call.outputChars || 0).toLocaleString("zh-CN")} 字符</small></span>
|
|
4274
|
+
<span class="task-trace-status">${call.status === "completed" ? "已完成" : call.status === "running" ? "运行中" : "失败"}</span>
|
|
4275
|
+
</summary>
|
|
4276
|
+
<div class="task-trace-call-body">
|
|
4277
|
+
<div class="task-trace-call-meta"><code>${esc(call.id)}</code><span>${esc(formatDateTime(call.createdAt))}</span></div>
|
|
4278
|
+
<details class="task-trace-initial">
|
|
4279
|
+
<summary>初始完整上下文(${Array.isArray(call.trace?.initialMessages) ? call.trace.initialMessages.length : 0} 条消息)</summary>
|
|
4280
|
+
${renderTaskTraceMessages(call.trace?.initialMessages)}
|
|
4281
|
+
</details>
|
|
4282
|
+
${call.failure ? `<pre class="task-trace-failure">${esc(call.failure)}</pre>` : ""}
|
|
4283
|
+
<div class="task-trace-rounds">${rounds.map(renderTaskTraceRound).join("")}</div>
|
|
4284
|
+
</div>
|
|
4285
|
+
</details>`;
|
|
4286
|
+
}).join("")}</div>
|
|
4287
|
+
</section>`;
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
function openTaskDetailDialog(task, trace) {
|
|
4021
4291
|
if (!task) return;
|
|
4022
4292
|
const details = Array.isArray(task.scopeDetails) ? task.scopeDetails : [];
|
|
4023
4293
|
const detailHtml = details.map((item) => {
|
|
@@ -4044,18 +4314,21 @@ function openTaskDetailDialog(task) {
|
|
|
4044
4314
|
: "<p>尚无结果</p>";
|
|
4045
4315
|
openDialog("任务详情",
|
|
4046
4316
|
`<div class="task-detail">
|
|
4047
|
-
<
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4317
|
+
<section class="task-detail-overview">
|
|
4318
|
+
<p><strong>任务 ID</strong><br><code>${esc(task.id)}</code></p>
|
|
4319
|
+
<p><strong>类型</strong> ${esc(analysisTaskTypeLabel(task.taskType))}</p>
|
|
4320
|
+
<p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
|
|
4321
|
+
<p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
|
|
4322
|
+
<div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
|
|
4323
|
+
<div><strong>失败信息</strong>${failureHtml}</div>
|
|
4324
|
+
<div><strong>结果摘要</strong>${resultPreview}</div>
|
|
4325
|
+
<p><small>创建于 ${esc(formatDateTime(task.createdAt))} · 更新于 ${esc(formatDateTime(task.updatedAt))}</small></p>
|
|
4326
|
+
</section>
|
|
4327
|
+
${renderTaskTraceVisualization(trace)}
|
|
4055
4328
|
</div>`,
|
|
4056
4329
|
async () => undefined,
|
|
4057
4330
|
"AI 分析详情",
|
|
4058
|
-
{ submitLabel: "关闭", wide: true });
|
|
4331
|
+
{ submitLabel: "关闭", wide: true, trace: true });
|
|
4059
4332
|
}
|
|
4060
4333
|
|
|
4061
4334
|
function renderProviderCards(providers, models) {
|
|
@@ -4558,41 +4831,78 @@ function commitRelationshipKeywordInputs(container) {
|
|
|
4558
4831
|
|
|
4559
4832
|
function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
4560
4833
|
void discardPendingMarkdownAttachments();
|
|
4834
|
+
const dialog = $("#form-dialog");
|
|
4835
|
+
const form = $("#dynamic-form");
|
|
4836
|
+
const submit = $("#dialog-submit");
|
|
4837
|
+
const submitStatus = $("#dialog-submit-status");
|
|
4838
|
+
const submitStatusMessage = $("#dialog-submit-status-message");
|
|
4839
|
+
const submitLabel = options.submitLabel ?? "保存";
|
|
4840
|
+
let submitting = false;
|
|
4841
|
+
let disabledStates = [];
|
|
4561
4842
|
$("#dialog-title").textContent = title;
|
|
4562
4843
|
$("#dialog-eyebrow").textContent = eyebrow;
|
|
4563
4844
|
$("#dialog-meta").textContent = options.meta ?? "";
|
|
4564
4845
|
$("#dialog-meta").classList.toggle("hidden", !options.meta);
|
|
4565
4846
|
$("#dialog-fields").innerHTML = fields;
|
|
4566
|
-
|
|
4847
|
+
submit.textContent = submitLabel;
|
|
4848
|
+
submitStatusMessage.textContent = options.pendingMessage ?? "正在提交,请稍候";
|
|
4849
|
+
submitStatus.classList.add("hidden");
|
|
4850
|
+
form.classList.remove("is-submitting");
|
|
4851
|
+
form.removeAttribute("aria-busy");
|
|
4567
4852
|
$("#dynamic-form .dialog-actions [value='cancel']").classList.toggle("hidden", Boolean(options.hideCancel));
|
|
4568
|
-
|
|
4853
|
+
dialog.classList.toggle("wide-dialog", Boolean(options.wide));
|
|
4854
|
+
dialog.classList.toggle("trace-dialog", Boolean(options.trace));
|
|
4569
4855
|
bindDynamicListControls($("#dialog-fields"));
|
|
4570
4856
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
4571
4857
|
bindVditorEditors($("#dialog-fields"));
|
|
4572
|
-
const form = $("#dynamic-form");
|
|
4573
4858
|
form.onclick = null;
|
|
4574
4859
|
form.onkeydown = null;
|
|
4860
|
+
dialog.oncancel = (event) => {
|
|
4861
|
+
if (submitting) event.preventDefault();
|
|
4862
|
+
};
|
|
4575
4863
|
form.onsubmit = async (event) => {
|
|
4864
|
+
if (submitting) {
|
|
4865
|
+
event.preventDefault();
|
|
4866
|
+
return;
|
|
4867
|
+
}
|
|
4576
4868
|
if (event.submitter?.value === "cancel") {
|
|
4577
4869
|
void discardPendingMarkdownAttachments();
|
|
4578
4870
|
return;
|
|
4579
4871
|
}
|
|
4580
4872
|
event.preventDefault();
|
|
4581
|
-
|
|
4582
|
-
|
|
4873
|
+
submitting = true;
|
|
4874
|
+
form.setAttribute("aria-busy", "true");
|
|
4875
|
+
form.classList.add("is-submitting");
|
|
4876
|
+
submitStatus.classList.remove("hidden");
|
|
4877
|
+
submit.textContent = options.pendingLabel ?? "处理中…";
|
|
4583
4878
|
try {
|
|
4584
4879
|
commitRelationshipKeywordInputs(form);
|
|
4585
|
-
|
|
4880
|
+
const formData = new FormData(form);
|
|
4881
|
+
disabledStates = [...form.elements].map((control) => [control, control.disabled]);
|
|
4882
|
+
disabledStates.forEach(([control]) => {
|
|
4883
|
+
control.disabled = true;
|
|
4884
|
+
});
|
|
4885
|
+
await onSubmit(formData);
|
|
4586
4886
|
const markdown = [...form.querySelectorAll("[data-vditor-value]")].map((textarea) => textarea.value).join("\n\n");
|
|
4587
4887
|
await cleanupPendingMarkdownAttachments(markdown);
|
|
4588
|
-
|
|
4888
|
+
dialog.close();
|
|
4589
4889
|
} catch (error) {
|
|
4590
|
-
|
|
4890
|
+
const message = error instanceof Error ? error.message : "未知错误";
|
|
4891
|
+
toast(`${options.errorPrefix ?? ""}${message}`, "error");
|
|
4591
4892
|
} finally {
|
|
4592
|
-
|
|
4893
|
+
disabledStates.forEach(([control, wasDisabled]) => {
|
|
4894
|
+
control.disabled = wasDisabled;
|
|
4895
|
+
});
|
|
4896
|
+
disabledStates = [];
|
|
4897
|
+
submitting = false;
|
|
4898
|
+
form.removeAttribute("aria-busy");
|
|
4899
|
+
form.classList.remove("is-submitting");
|
|
4900
|
+
submitStatus.classList.add("hidden");
|
|
4901
|
+
submit.textContent = submitLabel;
|
|
4593
4902
|
}
|
|
4594
4903
|
};
|
|
4595
|
-
|
|
4904
|
+
dialog.showModal();
|
|
4905
|
+
$("#dialog-fields").scrollTop = 0;
|
|
4596
4906
|
}
|
|
4597
4907
|
|
|
4598
4908
|
function openWorkDialog() {
|
|
@@ -6016,7 +6326,14 @@ async function openTaskDialog() {
|
|
|
6016
6326
|
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6017
6327
|
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
6018
6328
|
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope } });
|
|
6019
|
-
|
|
6329
|
+
taskListPage = 1;
|
|
6330
|
+
toast("分析任务已创建,已进入任务队列");
|
|
6331
|
+
void renderTasks(1).catch((error) => toast(`任务已创建,但列表刷新失败:${error.message}`, "error"));
|
|
6332
|
+
}, "AI 分析", {
|
|
6333
|
+
submitLabel: "创建任务",
|
|
6334
|
+
pendingLabel: "创建中…",
|
|
6335
|
+
pendingMessage: "正在创建分析任务,请稍候",
|
|
6336
|
+
errorPrefix: "任务创建失败:"
|
|
6020
6337
|
});
|
|
6021
6338
|
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
6022
6339
|
const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
|
|
@@ -6514,7 +6831,7 @@ async function loadImportHistoryPage(page) {
|
|
|
6514
6831
|
const workId = state.work?.id;
|
|
6515
6832
|
if (!workId || !page) return;
|
|
6516
6833
|
const requestId = ++importHistoryRequestId;
|
|
6517
|
-
const result = await apiPage(`/api/works/${encodeURIComponent(workId)}/file-versions`, page,
|
|
6834
|
+
const result = await apiPage(`/api/works/${encodeURIComponent(workId)}/file-versions`, page, pageSizeFor("fileVersions"));
|
|
6518
6835
|
if (requestId !== importHistoryRequestId || state.work?.id !== workId || !$("#import-history-dialog").open) return;
|
|
6519
6836
|
importHistoryRecords = page === 1 ? result.items : [...importHistoryRecords, ...result.items];
|
|
6520
6837
|
importHistoryNextPage = result.nextPage;
|
|
@@ -7115,11 +7432,20 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
7115
7432
|
try {
|
|
7116
7433
|
const settings = await api("/api/platform/ui-settings", {
|
|
7117
7434
|
method: "PATCH",
|
|
7118
|
-
body: {
|
|
7435
|
+
body: {
|
|
7436
|
+
toastPosition: $("#toast-position").value,
|
|
7437
|
+
pageSizes: {
|
|
7438
|
+
characters: Number($("#page-size-characters").value),
|
|
7439
|
+
analysisTasks: Number($("#page-size-analysis-tasks").value),
|
|
7440
|
+
fileVersions: Number($("#page-size-file-versions").value)
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7119
7443
|
});
|
|
7120
7444
|
applyPlatformUiSettings(settings);
|
|
7445
|
+
characterListPage = 1;
|
|
7446
|
+
taskListPage = 1;
|
|
7121
7447
|
$("#platform-ui-settings-dialog").close();
|
|
7122
|
-
toast("
|
|
7448
|
+
toast("界面与分页设置已保存");
|
|
7123
7449
|
} catch (error) {
|
|
7124
7450
|
toast(error.message, "error");
|
|
7125
7451
|
} finally {
|