@musnows/scriverse 0.4.12 → 0.5.1
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 +279 -4
- package/dist/ai.js.map +1 -1
- package/dist/app.js +34 -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 +470 -75
- 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 +244 -5
- 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);
|
|
2722
|
+
if (target.kind === "chapter") {
|
|
2723
|
+
await selectChapter(target.id);
|
|
2665
2724
|
return;
|
|
2666
2725
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
}
|
|
2673
|
-
if (
|
|
2674
|
-
await showModule("organizations");
|
|
2675
|
-
const organization = state.organizations.find((item) => item.id === result.id);
|
|
2676
|
-
if (organization) openOrganizationDialog(organization);
|
|
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">
|
|
@@ -3903,22 +4018,37 @@ async function renderTasks() {
|
|
|
3903
4018
|
</div>
|
|
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
|
-
<div class="task-auto-run-progress-label"
|
|
3907
|
-
|
|
4021
|
+
<div class="task-auto-run-progress-ring ${runningCount ? "is-running" : "is-waiting"}" role="progressbar" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${runningProgress}">
|
|
4022
|
+
<svg viewBox="0 0 120 120" aria-hidden="true" focusable="false">
|
|
4023
|
+
<circle class="task-auto-run-progress-ring-track" cx="60" cy="60" r="52"></circle>
|
|
4024
|
+
<circle class="task-auto-run-progress-ring-value" cx="60" cy="60" r="52" pathLength="100" stroke-dasharray="${runningProgress} 100"></circle>
|
|
4025
|
+
</svg>
|
|
4026
|
+
<div class="task-auto-run-progress-ring-label"><strong>${runningProgress}%</strong><span>${runningCount ? "运行中平均进度" : "等待任务开始"}</span></div>
|
|
4027
|
+
</div>
|
|
4028
|
+
<div class="task-auto-run-progress-bar-layout">
|
|
4029
|
+
<div class="task-auto-run-progress-label"><span>${runningCount ? "运行中任务平均进度" : "等待任务开始"}</span><strong>${runningProgress}%</strong></div>
|
|
4030
|
+
<progress class="task-auto-run-progress-bar ${runningCount ? "is-running" : "is-waiting"}" max="100" value="${runningProgress}" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}">${runningProgress}%</progress>
|
|
4031
|
+
</div>
|
|
3908
4032
|
</div>
|
|
3909
4033
|
</section>
|
|
3910
4034
|
${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
4035
|
<tr>
|
|
3912
4036
|
<td>${esc(analysisTaskTypeLabel(item.taskType))}</td>
|
|
3913
4037
|
<td>${esc(item.scopeSummary || taskScopeLabel(item.scope?.type || "book"))}</td>
|
|
3914
|
-
<td>${
|
|
3915
|
-
<td>${
|
|
4038
|
+
<td class="task-status-cell">${renderAnalysisTaskStatus(item)}</td>
|
|
4039
|
+
<td class="task-progress-cell">${renderAnalysisTaskProgress(item)}</td>
|
|
3916
4040
|
<td class="task-row-actions">
|
|
3917
4041
|
<button class="ghost-button" type="button" data-task-detail="${esc(item.id)}">详情</button>
|
|
3918
4042
|
${item.status === "pending" ? `<button class="ghost-button" type="button" data-run-task="${esc(item.id)}">运行</button>` : ""}
|
|
3919
4043
|
${item.status === "pending" || item.status === "running" ? `<button class="ghost-button" type="button" data-cancel-task="${esc(item.id)}">取消</button>` : ""}
|
|
3920
4044
|
</td>
|
|
3921
|
-
</tr>`).join("")}</tbody></table
|
|
4045
|
+
</tr>`).join("")}</tbody></table>${pagination}` : emptyModule("还没有 AI 分析记录", "点击“开始 AI 分析”,可分析指定章节或整部作品。")}`;
|
|
4046
|
+
|
|
4047
|
+
$("#module-content").querySelectorAll("[data-task-page]").forEach((button) => button.addEventListener("click", async () => {
|
|
4048
|
+
if (button.disabled) return;
|
|
4049
|
+
$("#module-content").querySelectorAll("[data-task-page]").forEach((control) => { control.disabled = true; });
|
|
4050
|
+
await renderTasks(Number(button.dataset.taskPage));
|
|
4051
|
+
}));
|
|
3922
4052
|
|
|
3923
4053
|
$("#task-auto-run-save")?.addEventListener("click", async () => {
|
|
3924
4054
|
const button = $("#task-auto-run-save");
|
|
@@ -3957,8 +4087,15 @@ async function renderTasks() {
|
|
|
3957
4087
|
$("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
|
|
3958
4088
|
if (button.disabled) return;
|
|
3959
4089
|
button.disabled = true;
|
|
3960
|
-
|
|
3961
|
-
|
|
4090
|
+
const taskId = encodeURIComponent(button.dataset.taskDetail);
|
|
4091
|
+
Promise.all([
|
|
4092
|
+
api(`/api/tasks/${taskId}`),
|
|
4093
|
+
api(`/api/tasks/${taskId}/trace`).catch((error) => {
|
|
4094
|
+
if (error.code === "WORK_MODULE_READ_DENIED") return { restricted: true, captured: false, calls: [] };
|
|
4095
|
+
throw error;
|
|
4096
|
+
})
|
|
4097
|
+
])
|
|
4098
|
+
.then(([task, trace]) => openTaskDetailDialog(task, trace))
|
|
3962
4099
|
.catch((error) => toast(error.message, "error"))
|
|
3963
4100
|
.finally(() => { button.disabled = false; });
|
|
3964
4101
|
}));
|
|
@@ -3967,6 +4104,12 @@ async function renderTasks() {
|
|
|
3967
4104
|
try {
|
|
3968
4105
|
button.disabled = true;
|
|
3969
4106
|
button.textContent = "运行中";
|
|
4107
|
+
const row = button.closest("tr");
|
|
4108
|
+
const optimisticTask = { id: button.dataset.runTask, status: "running", progress: 5 };
|
|
4109
|
+
const statusCell = row?.querySelector(".task-status-cell");
|
|
4110
|
+
const progressCell = row?.querySelector(".task-progress-cell");
|
|
4111
|
+
if (statusCell) statusCell.innerHTML = renderAnalysisTaskStatus(optimisticTask);
|
|
4112
|
+
if (progressCell) progressCell.innerHTML = renderAnalysisTaskProgress(optimisticTask);
|
|
3970
4113
|
const cancel = button.parentElement.querySelector("[data-cancel-task]");
|
|
3971
4114
|
if (cancel) cancel.textContent = "取消运行";
|
|
3972
4115
|
scheduleTaskProgressRefresh(workId, 1);
|
|
@@ -4017,7 +4160,202 @@ function scheduleTaskProgressRefresh(workId, runningCount) {
|
|
|
4017
4160
|
}, taskProgressRefreshInterval);
|
|
4018
4161
|
}
|
|
4019
4162
|
|
|
4020
|
-
function
|
|
4163
|
+
function taskTraceRoleLabel(role) {
|
|
4164
|
+
if (role === "system") return "系统提示词";
|
|
4165
|
+
if (role === "assistant") return "Agent";
|
|
4166
|
+
if (role === "tool") return "工具结果";
|
|
4167
|
+
return "用户提示词";
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
function renderTaskTraceMessages(messages) {
|
|
4171
|
+
if (!Array.isArray(messages) || messages.length === 0) return '<p class="task-trace-empty">本轮没有消息。</p>';
|
|
4172
|
+
return `<div class="task-trace-messages">${messages.map((message, index) => {
|
|
4173
|
+
const role = String(message?.role || "user");
|
|
4174
|
+
const content = message?.content === null ? "" : String(message?.content ?? "");
|
|
4175
|
+
const contentChars = Number.isFinite(Number(message?.contentChars)) ? Number(message.contentChars) : content.length;
|
|
4176
|
+
const contentTruncated = Boolean(message?.contentTruncated);
|
|
4177
|
+
const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
4178
|
+
const toolCallCount = Number.isFinite(Number(message?.toolCallCount)) ? Number(message.toolCallCount) : toolCalls.length;
|
|
4179
|
+
return `<article class="task-trace-message is-${esc(role)}">
|
|
4180
|
+
<header><span>${esc(taskTraceRoleLabel(role))}</span><small>#${index + 1} · ${contentChars.toLocaleString("zh-CN")} 字符${contentTruncated ? " · 已截断" : ""}</small></header>
|
|
4181
|
+
${content ? `<pre>${esc(content)}${contentTruncated ? "\n\n……预览已截断" : ""}</pre>` : '<p class="task-trace-empty">无文本正文</p>'}
|
|
4182
|
+
${toolCalls.length ? `<details><summary>Agent 请求的工具调用(${toolCalls.length})</summary><pre>${esc(JSON.stringify(toolCalls, null, 2))}</pre></details>` : ""}
|
|
4183
|
+
${!toolCalls.length && toolCallCount ? `<small>包含 ${toolCallCount} 个工具调用,加载完整内容后显示。</small>` : ""}
|
|
4184
|
+
${message?.tool_call_id ? `<small>工具调用 ID:<code>${esc(message.tool_call_id)}</code></small>` : ""}
|
|
4185
|
+
</article>`;
|
|
4186
|
+
}).join("")}</div>`;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
function renderTaskTraceAttempt(attempt) {
|
|
4190
|
+
const response = attempt?.response && typeof attempt.response === "object" ? attempt.response : {};
|
|
4191
|
+
const choice = Array.isArray(response.choices) ? response.choices[0] : null;
|
|
4192
|
+
const message = choice?.message && typeof choice.message === "object" ? choice.message : {};
|
|
4193
|
+
const reasoning = String(message.reasoning_content ?? "");
|
|
4194
|
+
const content = String(message.content ?? "");
|
|
4195
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
4196
|
+
return `<article class="task-trace-attempt is-${esc(attempt?.status || "failed")}">
|
|
4197
|
+
<header>
|
|
4198
|
+
<strong>尝试 ${esc(String(attempt?.attempt ?? 1))}</strong>
|
|
4199
|
+
<span>${attempt?.status === "completed" ? "响应成功" : attempt?.status === "running" ? "等待响应" : "请求失败"}${attempt?.httpStatus ? ` · HTTP ${esc(String(attempt.httpStatus))}` : ""}</span>
|
|
4200
|
+
<small>${esc(formatDateTime(attempt?.startedAt))}${attempt?.completedAt ? ` → ${esc(formatDateTime(attempt.completedAt))}` : ""}</small>
|
|
4201
|
+
</header>
|
|
4202
|
+
${reasoning ? `<details class="task-trace-response"><summary>模型思考过程 · ${reasoning.length.toLocaleString("zh-CN")} 字符</summary><pre>${esc(reasoning)}</pre></details>` : ""}
|
|
4203
|
+
${content ? `<details class="task-trace-response" open><summary>Agent 响应 · ${content.length.toLocaleString("zh-CN")} 字符</summary><pre>${esc(content)}</pre></details>` : ""}
|
|
4204
|
+
${toolCalls.length ? `<details class="task-trace-response" open><summary>Agent 工具请求 · ${toolCalls.length} 项</summary><pre>${esc(JSON.stringify(toolCalls, null, 2))}</pre></details>` : ""}
|
|
4205
|
+
${attempt?.failure ? `<pre class="task-trace-failure">${esc(attempt.failure)}</pre>` : ""}
|
|
4206
|
+
${response.usage ? `<details class="task-trace-response"><summary>Token 用量</summary><pre>${esc(JSON.stringify(response.usage, null, 2))}</pre></details>` : ""}
|
|
4207
|
+
</article>`;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
function renderTaskTraceRound(round) {
|
|
4211
|
+
const messages = Array.isArray(round?.request?.messages) ? round.request.messages : [];
|
|
4212
|
+
const attempts = Array.isArray(round?.attempts) ? round.attempts : [];
|
|
4213
|
+
const executions = Array.isArray(round?.toolExecutions) ? round.toolExecutions : [];
|
|
4214
|
+
return `<section class="task-trace-round">
|
|
4215
|
+
<header class="task-trace-round-header">
|
|
4216
|
+
<span class="task-trace-round-index">${esc(String(round?.round ?? 1))}</span>
|
|
4217
|
+
<div><strong>Agent 轮次 ${esc(String(round?.round ?? 1))}</strong><small>${messages.length} 条消息 · ${attempts.length} 次请求尝试 · ${executions.length} 次工具执行</small></div>
|
|
4218
|
+
<time>${esc(formatDateTime(round?.requestedAt))}</time>
|
|
4219
|
+
</header>
|
|
4220
|
+
<div class="task-trace-flow" aria-label="本轮调用流程">
|
|
4221
|
+
<span>完整 Prompt</span><i aria-hidden="true">→</i><span>模型响应</span>${executions.length ? '<i aria-hidden="true">→</i><span>工具执行</span>' : ""}
|
|
4222
|
+
</div>
|
|
4223
|
+
<details class="task-trace-prompt">
|
|
4224
|
+
<summary>查看本轮发出的完整 Prompt(${messages.length} 条消息)</summary>
|
|
4225
|
+
${renderTaskTraceMessages(messages)}
|
|
4226
|
+
<details class="task-trace-request-meta"><summary>模型参数与工具定义</summary><pre>${esc(JSON.stringify({
|
|
4227
|
+
model: round?.request?.model,
|
|
4228
|
+
parameters: round?.request?.parameters ?? {},
|
|
4229
|
+
toolChoice: round?.request?.toolChoice,
|
|
4230
|
+
tools: round?.request?.tools ?? []
|
|
4231
|
+
}, null, 2))}</pre></details>
|
|
4232
|
+
</details>
|
|
4233
|
+
<div class="task-trace-attempts">${attempts.map(renderTaskTraceAttempt).join("") || '<p class="task-trace-empty">尚未记录模型响应。</p>'}</div>
|
|
4234
|
+
${executions.length ? `<div class="task-trace-tools"><strong>工具执行结果</strong>${executions.map((execution) => `<details>
|
|
4235
|
+
<summary>${esc(execution.name || "未知工具")} · ${execution.status === "completed" ? "成功" : "失败"}</summary>
|
|
4236
|
+
<div class="task-trace-tool-grid">
|
|
4237
|
+
<div><small>调用参数</small><pre>${esc(JSON.stringify(execution.arguments, null, 2))}</pre></div>
|
|
4238
|
+
<div><small>返回结果</small><pre>${esc(JSON.stringify(execution.result, null, 2))}</pre></div>
|
|
4239
|
+
</div>
|
|
4240
|
+
</details>`).join("")}</div>` : ""}
|
|
4241
|
+
</section>`;
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
function renderTaskTraceRoundSummary(round) {
|
|
4245
|
+
return `<section class="task-trace-round task-trace-round-summary">
|
|
4246
|
+
<header class="task-trace-round-header">
|
|
4247
|
+
<span class="task-trace-round-index">${esc(String(round?.round ?? 1))}</span>
|
|
4248
|
+
<div><strong>Agent 轮次 ${esc(String(round?.round ?? 1))}</strong><small>${Number(round?.messageCount ?? 0)} 条消息 · ${Number(round?.attemptCount ?? 0)} 次请求尝试 · ${Number(round?.toolExecutionCount ?? 0)} 次工具执行</small></div>
|
|
4249
|
+
<time>${esc(formatDateTime(round?.requestedAt))}</time>
|
|
4250
|
+
</header>
|
|
4251
|
+
<p>本轮 Prompt 共 ${Number(round?.promptChars ?? 0).toLocaleString("zh-CN")} 字符,正文、模型响应和工具结果尚未传输。</p>
|
|
4252
|
+
</section>`;
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
function renderTaskTraceCallPreview(detail) {
|
|
4256
|
+
const trace = detail?.trace && typeof detail.trace === "object" ? detail.trace : {};
|
|
4257
|
+
const messages = Array.isArray(trace.initialMessages) ? trace.initialMessages : [];
|
|
4258
|
+
const rounds = Array.isArray(trace.rounds) ? trace.rounds : [];
|
|
4259
|
+
return `<div class="task-trace-load-state is-loaded">
|
|
4260
|
+
<div><strong>Prompt 预览</strong><p>全部消息合计最多传输 ${Number(detail?.previewLimit ?? 3000).toLocaleString("zh-CN")} 个字符;模型响应和工具结果仍未传输。</p></div>
|
|
4261
|
+
<details class="task-trace-initial" open>
|
|
4262
|
+
<summary>初始上下文预览(${messages.length} 条消息 · 原文 ${Number(detail?.totalPromptChars ?? 0).toLocaleString("zh-CN")} 字符)</summary>
|
|
4263
|
+
${renderTaskTraceMessages(messages)}
|
|
4264
|
+
</details>
|
|
4265
|
+
<div class="task-trace-rounds">${rounds.map(renderTaskTraceRoundSummary).join("") || '<p class="task-trace-empty">尚未记录 Agent 轮次。</p>'}</div>
|
|
4266
|
+
<div class="card-actions"><button class="ghost-button" type="button" data-load-task-trace-call="full">查看完整调用</button></div>
|
|
4267
|
+
</div>`;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
function renderTaskTraceCallFull(detail) {
|
|
4271
|
+
const trace = detail?.trace && typeof detail.trace === "object" ? detail.trace : {};
|
|
4272
|
+
const messages = Array.isArray(trace.initialMessages) ? trace.initialMessages : [];
|
|
4273
|
+
const rounds = Array.isArray(trace.rounds) ? trace.rounds : [];
|
|
4274
|
+
return `<div class="task-trace-load-state is-loaded">
|
|
4275
|
+
<details class="task-trace-initial" open>
|
|
4276
|
+
<summary>初始完整上下文(${messages.length} 条消息)</summary>
|
|
4277
|
+
${renderTaskTraceMessages(messages)}
|
|
4278
|
+
</details>
|
|
4279
|
+
<div class="task-trace-rounds">${rounds.map(renderTaskTraceRound).join("") || '<p class="task-trace-empty">尚未记录 Agent 轮次。</p>'}</div>
|
|
4280
|
+
</div>`;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
function bindTaskTraceCallActions(container) {
|
|
4284
|
+
container.querySelectorAll("[data-load-task-trace-call]").forEach((button) => button.addEventListener("click", async () => {
|
|
4285
|
+
const call = button.closest("[data-task-trace-call]");
|
|
4286
|
+
const content = call?.querySelector("[data-task-trace-call-content]");
|
|
4287
|
+
if (!call || !content || button.disabled) return;
|
|
4288
|
+
const mode = button.dataset.loadTaskTraceCall;
|
|
4289
|
+
button.disabled = true;
|
|
4290
|
+
button.textContent = mode === "full" ? "正在加载完整内容" : "正在加载预览";
|
|
4291
|
+
try {
|
|
4292
|
+
const taskId = encodeURIComponent(call.dataset.taskTraceTask);
|
|
4293
|
+
const callId = encodeURIComponent(call.dataset.taskTraceCall);
|
|
4294
|
+
const detail = await api(`/api/tasks/${taskId}/trace/calls/${callId}${mode === "full" ? "?full=true" : ""}`);
|
|
4295
|
+
content.innerHTML = mode === "full" ? renderTaskTraceCallFull(detail) : renderTaskTraceCallPreview(detail);
|
|
4296
|
+
bindTaskTraceCallActions(content);
|
|
4297
|
+
} catch (error) {
|
|
4298
|
+
toast(error.message, "error");
|
|
4299
|
+
button.disabled = false;
|
|
4300
|
+
button.textContent = mode === "full" ? "查看完整调用" : "加载内容预览";
|
|
4301
|
+
}
|
|
4302
|
+
}));
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
function renderTaskTraceVisualization(trace, taskId) {
|
|
4306
|
+
if (trace?.restricted) {
|
|
4307
|
+
return `<section class="task-trace-section" aria-labelledby="task-trace-title">
|
|
4308
|
+
<header class="task-trace-heading"><div><span class="eyebrow">执行追踪</span><h3 id="task-trace-title">完整全流程上下文</h3></div></header>
|
|
4309
|
+
<div class="task-trace-unavailable"><strong>完整上下文受权限保护</strong><p>当前账号缺少正文或作品资料的读取权限,无法查看原始 Prompt、模型响应与工具结果。</p></div>
|
|
4310
|
+
</section>`;
|
|
4311
|
+
}
|
|
4312
|
+
const calls = Array.isArray(trace?.calls) ? trace.calls : [];
|
|
4313
|
+
const capturedCalls = calls.filter((call) => call.trace);
|
|
4314
|
+
const roundCount = capturedCalls.reduce((total, call) => total + Number(call.trace?.roundCount || 0), 0);
|
|
4315
|
+
const promptChars = capturedCalls.reduce((total, call) => total + Number(call.inputChars || 0), 0);
|
|
4316
|
+
const outputChars = capturedCalls.reduce((total, call) => total + Number(call.outputChars || 0), 0);
|
|
4317
|
+
if (!trace?.captured || capturedCalls.length === 0) {
|
|
4318
|
+
return `<section class="task-trace-section" aria-labelledby="task-trace-title">
|
|
4319
|
+
<header class="task-trace-heading"><div><span class="eyebrow">执行追踪</span><h3 id="task-trace-title">完整全流程上下文</h3></div></header>
|
|
4320
|
+
<div class="task-trace-unavailable"><strong>没有可用的全流程记录</strong><p>${calls.length ? "该任务仅保留了调用摘要,没有保存完整 Prompt 与 Agent 轮次。" : "这是追踪功能启用前创建的历史任务,或任务尚未发起任何模型调用。"}</p></div>
|
|
4321
|
+
</section>`;
|
|
4322
|
+
}
|
|
4323
|
+
return `<section class="task-trace-section" aria-labelledby="task-trace-title">
|
|
4324
|
+
<header class="task-trace-heading">
|
|
4325
|
+
<div><span class="eyebrow">执行追踪</span><h3 id="task-trace-title">完整全流程上下文</h3></div>
|
|
4326
|
+
<p>首次只加载调用摘要;Prompt 预览与完整调用内容均按需单独请求。</p>
|
|
4327
|
+
</header>
|
|
4328
|
+
<div class="task-trace-metrics" aria-label="执行追踪统计">
|
|
4329
|
+
<div><strong>${capturedCalls.length}</strong><span>模型调用</span></div>
|
|
4330
|
+
<div><strong>${roundCount}</strong><span>Agent 轮次</span></div>
|
|
4331
|
+
<div><strong>${promptChars.toLocaleString("zh-CN")}</strong><span>Prompt 字符</span></div>
|
|
4332
|
+
<div><strong>${outputChars.toLocaleString("zh-CN")}</strong><span>输出字符</span></div>
|
|
4333
|
+
</div>
|
|
4334
|
+
<div class="task-trace-calls">${capturedCalls.map((call, index) => {
|
|
4335
|
+
const modelName = call.model?.displayName || call.model?.modelId || "未知模型";
|
|
4336
|
+
const providerName = call.provider?.name || "未知供应商";
|
|
4337
|
+
return `<details class="task-trace-call is-${esc(call.status || "failed")}" data-task-trace-call="${esc(call.id)}" data-task-trace-task="${esc(taskId)}" ${index === 0 ? "open" : ""}>
|
|
4338
|
+
<summary>
|
|
4339
|
+
<span class="task-trace-call-index">${index + 1}</span>
|
|
4340
|
+
<span><strong>${esc(modelName)}</strong><small>${esc(providerName)} · ${Number(call.trace?.roundCount || 0)} 轮 · ${Number(call.inputChars || 0).toLocaleString("zh-CN")} → ${Number(call.outputChars || 0).toLocaleString("zh-CN")} 字符</small></span>
|
|
4341
|
+
<span class="task-trace-status">${call.status === "completed" ? "已完成" : call.status === "running" ? "运行中" : "失败"}</span>
|
|
4342
|
+
</summary>
|
|
4343
|
+
<div class="task-trace-call-body">
|
|
4344
|
+
<div class="task-trace-call-meta"><code>${esc(call.id)}</code><span>${esc(formatDateTime(call.createdAt))}</span></div>
|
|
4345
|
+
${call.failure ? `<pre class="task-trace-failure">${esc(call.failure)}${call.failureTruncated ? "\n……失败信息已截断" : ""}</pre>` : ""}
|
|
4346
|
+
<div data-task-trace-call-content>
|
|
4347
|
+
<div class="task-trace-load-state">
|
|
4348
|
+
<div><strong>调用内容尚未加载</strong><p>这次调用的 Prompt、模型响应和工具结果不会随任务详情传输。</p></div>
|
|
4349
|
+
<button class="ghost-button" type="button" data-load-task-trace-call="preview">加载内容预览</button>
|
|
4350
|
+
</div>
|
|
4351
|
+
</div>
|
|
4352
|
+
</div>
|
|
4353
|
+
</details>`;
|
|
4354
|
+
}).join("")}</div>
|
|
4355
|
+
</section>`;
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
function openTaskDetailDialog(task, trace) {
|
|
4021
4359
|
if (!task) return;
|
|
4022
4360
|
const details = Array.isArray(task.scopeDetails) ? task.scopeDetails : [];
|
|
4023
4361
|
const detailHtml = details.map((item) => {
|
|
@@ -4044,18 +4382,22 @@ function openTaskDetailDialog(task) {
|
|
|
4044
4382
|
: "<p>尚无结果</p>";
|
|
4045
4383
|
openDialog("任务详情",
|
|
4046
4384
|
`<div class="task-detail">
|
|
4047
|
-
<
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4385
|
+
<section class="task-detail-overview">
|
|
4386
|
+
<p><strong>任务 ID</strong><br><code>${esc(task.id)}</code></p>
|
|
4387
|
+
<p><strong>类型</strong> ${esc(analysisTaskTypeLabel(task.taskType))}</p>
|
|
4388
|
+
<p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
|
|
4389
|
+
<p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
|
|
4390
|
+
<div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
|
|
4391
|
+
<div><strong>失败信息</strong>${failureHtml}</div>
|
|
4392
|
+
<div><strong>结果摘要</strong>${resultPreview}</div>
|
|
4393
|
+
<p><small>创建于 ${esc(formatDateTime(task.createdAt))} · 更新于 ${esc(formatDateTime(task.updatedAt))}</small></p>
|
|
4394
|
+
</section>
|
|
4395
|
+
${renderTaskTraceVisualization(trace, task.id)}
|
|
4055
4396
|
</div>`,
|
|
4056
4397
|
async () => undefined,
|
|
4057
4398
|
"AI 分析详情",
|
|
4058
|
-
{ submitLabel: "关闭", wide: true });
|
|
4399
|
+
{ submitLabel: "关闭", wide: true, trace: true });
|
|
4400
|
+
bindTaskTraceCallActions($("#dialog-fields"));
|
|
4059
4401
|
}
|
|
4060
4402
|
|
|
4061
4403
|
function renderProviderCards(providers, models) {
|
|
@@ -4558,41 +4900,78 @@ function commitRelationshipKeywordInputs(container) {
|
|
|
4558
4900
|
|
|
4559
4901
|
function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
4560
4902
|
void discardPendingMarkdownAttachments();
|
|
4903
|
+
const dialog = $("#form-dialog");
|
|
4904
|
+
const form = $("#dynamic-form");
|
|
4905
|
+
const submit = $("#dialog-submit");
|
|
4906
|
+
const submitStatus = $("#dialog-submit-status");
|
|
4907
|
+
const submitStatusMessage = $("#dialog-submit-status-message");
|
|
4908
|
+
const submitLabel = options.submitLabel ?? "保存";
|
|
4909
|
+
let submitting = false;
|
|
4910
|
+
let disabledStates = [];
|
|
4561
4911
|
$("#dialog-title").textContent = title;
|
|
4562
4912
|
$("#dialog-eyebrow").textContent = eyebrow;
|
|
4563
4913
|
$("#dialog-meta").textContent = options.meta ?? "";
|
|
4564
4914
|
$("#dialog-meta").classList.toggle("hidden", !options.meta);
|
|
4565
4915
|
$("#dialog-fields").innerHTML = fields;
|
|
4566
|
-
|
|
4916
|
+
submit.textContent = submitLabel;
|
|
4917
|
+
submitStatusMessage.textContent = options.pendingMessage ?? "正在提交,请稍候";
|
|
4918
|
+
submitStatus.classList.add("hidden");
|
|
4919
|
+
form.classList.remove("is-submitting");
|
|
4920
|
+
form.removeAttribute("aria-busy");
|
|
4567
4921
|
$("#dynamic-form .dialog-actions [value='cancel']").classList.toggle("hidden", Boolean(options.hideCancel));
|
|
4568
|
-
|
|
4922
|
+
dialog.classList.toggle("wide-dialog", Boolean(options.wide));
|
|
4923
|
+
dialog.classList.toggle("trace-dialog", Boolean(options.trace));
|
|
4569
4924
|
bindDynamicListControls($("#dialog-fields"));
|
|
4570
4925
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
4571
4926
|
bindVditorEditors($("#dialog-fields"));
|
|
4572
|
-
const form = $("#dynamic-form");
|
|
4573
4927
|
form.onclick = null;
|
|
4574
4928
|
form.onkeydown = null;
|
|
4929
|
+
dialog.oncancel = (event) => {
|
|
4930
|
+
if (submitting) event.preventDefault();
|
|
4931
|
+
};
|
|
4575
4932
|
form.onsubmit = async (event) => {
|
|
4933
|
+
if (submitting) {
|
|
4934
|
+
event.preventDefault();
|
|
4935
|
+
return;
|
|
4936
|
+
}
|
|
4576
4937
|
if (event.submitter?.value === "cancel") {
|
|
4577
4938
|
void discardPendingMarkdownAttachments();
|
|
4578
4939
|
return;
|
|
4579
4940
|
}
|
|
4580
4941
|
event.preventDefault();
|
|
4581
|
-
|
|
4582
|
-
|
|
4942
|
+
submitting = true;
|
|
4943
|
+
form.setAttribute("aria-busy", "true");
|
|
4944
|
+
form.classList.add("is-submitting");
|
|
4945
|
+
submitStatus.classList.remove("hidden");
|
|
4946
|
+
submit.textContent = options.pendingLabel ?? "处理中…";
|
|
4583
4947
|
try {
|
|
4584
4948
|
commitRelationshipKeywordInputs(form);
|
|
4585
|
-
|
|
4949
|
+
const formData = new FormData(form);
|
|
4950
|
+
disabledStates = [...form.elements].map((control) => [control, control.disabled]);
|
|
4951
|
+
disabledStates.forEach(([control]) => {
|
|
4952
|
+
control.disabled = true;
|
|
4953
|
+
});
|
|
4954
|
+
await onSubmit(formData);
|
|
4586
4955
|
const markdown = [...form.querySelectorAll("[data-vditor-value]")].map((textarea) => textarea.value).join("\n\n");
|
|
4587
4956
|
await cleanupPendingMarkdownAttachments(markdown);
|
|
4588
|
-
|
|
4957
|
+
dialog.close();
|
|
4589
4958
|
} catch (error) {
|
|
4590
|
-
|
|
4959
|
+
const message = error instanceof Error ? error.message : "未知错误";
|
|
4960
|
+
toast(`${options.errorPrefix ?? ""}${message}`, "error");
|
|
4591
4961
|
} finally {
|
|
4592
|
-
|
|
4962
|
+
disabledStates.forEach(([control, wasDisabled]) => {
|
|
4963
|
+
control.disabled = wasDisabled;
|
|
4964
|
+
});
|
|
4965
|
+
disabledStates = [];
|
|
4966
|
+
submitting = false;
|
|
4967
|
+
form.removeAttribute("aria-busy");
|
|
4968
|
+
form.classList.remove("is-submitting");
|
|
4969
|
+
submitStatus.classList.add("hidden");
|
|
4970
|
+
submit.textContent = submitLabel;
|
|
4593
4971
|
}
|
|
4594
4972
|
};
|
|
4595
|
-
|
|
4973
|
+
dialog.showModal();
|
|
4974
|
+
$("#dialog-fields").scrollTop = 0;
|
|
4596
4975
|
}
|
|
4597
4976
|
|
|
4598
4977
|
function openWorkDialog() {
|
|
@@ -6016,7 +6395,14 @@ async function openTaskDialog() {
|
|
|
6016
6395
|
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6017
6396
|
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
6018
6397
|
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope } });
|
|
6019
|
-
|
|
6398
|
+
taskListPage = 1;
|
|
6399
|
+
toast("分析任务已创建,已进入任务队列");
|
|
6400
|
+
void renderTasks(1).catch((error) => toast(`任务已创建,但列表刷新失败:${error.message}`, "error"));
|
|
6401
|
+
}, "AI 分析", {
|
|
6402
|
+
submitLabel: "创建任务",
|
|
6403
|
+
pendingLabel: "创建中…",
|
|
6404
|
+
pendingMessage: "正在创建分析任务,请稍候",
|
|
6405
|
+
errorPrefix: "任务创建失败:"
|
|
6020
6406
|
});
|
|
6021
6407
|
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
6022
6408
|
const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
|
|
@@ -6514,7 +6900,7 @@ async function loadImportHistoryPage(page) {
|
|
|
6514
6900
|
const workId = state.work?.id;
|
|
6515
6901
|
if (!workId || !page) return;
|
|
6516
6902
|
const requestId = ++importHistoryRequestId;
|
|
6517
|
-
const result = await apiPage(`/api/works/${encodeURIComponent(workId)}/file-versions`, page,
|
|
6903
|
+
const result = await apiPage(`/api/works/${encodeURIComponent(workId)}/file-versions`, page, pageSizeFor("fileVersions"));
|
|
6518
6904
|
if (requestId !== importHistoryRequestId || state.work?.id !== workId || !$("#import-history-dialog").open) return;
|
|
6519
6905
|
importHistoryRecords = page === 1 ? result.items : [...importHistoryRecords, ...result.items];
|
|
6520
6906
|
importHistoryNextPage = result.nextPage;
|
|
@@ -7115,11 +7501,20 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
7115
7501
|
try {
|
|
7116
7502
|
const settings = await api("/api/platform/ui-settings", {
|
|
7117
7503
|
method: "PATCH",
|
|
7118
|
-
body: {
|
|
7504
|
+
body: {
|
|
7505
|
+
toastPosition: $("#toast-position").value,
|
|
7506
|
+
pageSizes: {
|
|
7507
|
+
characters: Number($("#page-size-characters").value),
|
|
7508
|
+
analysisTasks: Number($("#page-size-analysis-tasks").value),
|
|
7509
|
+
fileVersions: Number($("#page-size-file-versions").value)
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7119
7512
|
});
|
|
7120
7513
|
applyPlatformUiSettings(settings);
|
|
7514
|
+
characterListPage = 1;
|
|
7515
|
+
taskListPage = 1;
|
|
7121
7516
|
$("#platform-ui-settings-dialog").close();
|
|
7122
|
-
toast("
|
|
7517
|
+
toast("界面与分页设置已保存");
|
|
7123
7518
|
} catch (error) {
|
|
7124
7519
|
toast(error.message, "error");
|
|
7125
7520
|
} finally {
|