@musnows/scriverse 0.4.11 → 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 +383 -20
- package/dist/ai.js.map +1 -1
- package/dist/app.js +69 -12
- 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/ai-message-meta.js +7 -2
- package/dist/public/app.js +575 -81
- package/dist/public/global-search.d.ts +12 -0
- package/dist/public/global-search.js +23 -0
- package/dist/public/index.html +18 -5
- 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 +247 -2
- package/dist/store.js +123 -30
- 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";
|
|
@@ -6,7 +6,7 @@ import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-a
|
|
|
6
6
|
import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
7
7
|
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
|
|
8
8
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
9
|
-
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=
|
|
9
|
+
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
10
10
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
11
11
|
import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=20260718-layered-context";
|
|
12
12
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
@@ -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,
|
|
@@ -104,6 +125,9 @@ let peerPageStale = false;
|
|
|
104
125
|
let collaborationAutoSaveDisabled = false;
|
|
105
126
|
|
|
106
127
|
let timelineMultiSelectEnabled = false;
|
|
128
|
+
let taskProgressRefreshTimer = null;
|
|
129
|
+
const taskProgressRefreshInterval = 2_500;
|
|
130
|
+
const taskStatusSnapshots = new Map();
|
|
107
131
|
|
|
108
132
|
const chapterTypes = ["正文", "设定", "作者的话", "其他"];
|
|
109
133
|
|
|
@@ -135,6 +159,41 @@ function analysisTaskStatusLabel(status) {
|
|
|
135
159
|
})[String(status)] ?? "未知状态";
|
|
136
160
|
}
|
|
137
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
|
+
|
|
138
197
|
function reviewSeverityLabel(severity) {
|
|
139
198
|
return levelLabel(severity);
|
|
140
199
|
}
|
|
@@ -774,8 +833,11 @@ let entityEditorDirty = false;
|
|
|
774
833
|
let entityEditorReadOnly = false;
|
|
775
834
|
let chapterEditorReadOnly = true;
|
|
776
835
|
let characterListPage = 1;
|
|
836
|
+
let taskListPage = 1;
|
|
777
837
|
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
778
838
|
let characterFiltersPanelOpen = false;
|
|
839
|
+
const relationshipFilters = { fromCharacterIds: [], toCharacterIds: [] };
|
|
840
|
+
let relationshipFiltersPanelOpen = false;
|
|
779
841
|
let settingEditorItem = null;
|
|
780
842
|
let characterEditorItem = null;
|
|
781
843
|
let knowledgeEditorItem = null;
|
|
@@ -1889,7 +1951,7 @@ async function api(path, options = {}) {
|
|
|
1889
1951
|
return payload.data;
|
|
1890
1952
|
}
|
|
1891
1953
|
|
|
1892
|
-
async function apiPage(path, page = 1, limit =
|
|
1954
|
+
async function apiPage(path, page = 1, limit = 30) {
|
|
1893
1955
|
const separator = path.includes("?") ? "&" : "?";
|
|
1894
1956
|
const result = await api(`${path}${separator}page=${page}&limit=${limit}`);
|
|
1895
1957
|
if (Array.isArray(result)) return { items: result, page, limit, hasMore: false, nextPage: null };
|
|
@@ -1998,9 +2060,14 @@ function applyAuthenticatedUser(session) {
|
|
|
1998
2060
|
|
|
1999
2061
|
function applyPlatformUiSettings(settings) {
|
|
2000
2062
|
const position = settings?.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2063
|
+
state.uiSettings = { toastPosition: position, pageSizes: normalizePageSizes(settings?.pageSizes) };
|
|
2001
2064
|
$("#toast-region").dataset.position = position;
|
|
2002
2065
|
}
|
|
2003
2066
|
|
|
2067
|
+
function pageSizeFor(module) {
|
|
2068
|
+
return normalizePageSize(state.uiSettings.pageSizes[module], defaultPageSizes[module] ?? 30);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2004
2071
|
async function loadPlatformUiSettings() {
|
|
2005
2072
|
try {
|
|
2006
2073
|
applyPlatformUiSettings(await api("/api/ui-settings"));
|
|
@@ -2493,6 +2560,10 @@ async function openPlatformUiSettingsDialog() {
|
|
|
2493
2560
|
try {
|
|
2494
2561
|
const settings = await api("/api/platform/ui-settings");
|
|
2495
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);
|
|
2496
2567
|
$("#platform-ui-settings-dialog").showModal();
|
|
2497
2568
|
} catch (error) {
|
|
2498
2569
|
toast(error.message, "error");
|
|
@@ -2643,36 +2714,22 @@ async function runWorkSearch() {
|
|
|
2643
2714
|
}
|
|
2644
2715
|
|
|
2645
2716
|
async function openSearchResult(result) {
|
|
2717
|
+
const target = resolveGlobalSearchTarget(result);
|
|
2718
|
+
if (!target) throw new Error("无法打开该搜索结果");
|
|
2646
2719
|
$("#search-dialog").close();
|
|
2647
2720
|
const inSettings = !$("#settings-hub-view").classList.contains("hidden") || !$("#platform-ai-view").classList.contains("hidden");
|
|
2648
2721
|
if (inSettings) await returnFromSettings();
|
|
2649
|
-
if (
|
|
2650
|
-
await selectChapter(
|
|
2722
|
+
if (target.kind === "chapter") {
|
|
2723
|
+
await selectChapter(target.id);
|
|
2651
2724
|
return;
|
|
2652
2725
|
}
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
}
|
|
2659
|
-
if (
|
|
2660
|
-
await showModule("settings");
|
|
2661
|
-
const setting = await api(`/api/settings/${encodeURIComponent(result.id)}`);
|
|
2662
|
-
openSettingEditor(setting);
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
if (result.type === "race") {
|
|
2666
|
-
await showModule("races");
|
|
2667
|
-
const race = state.races.find((item) => item.id === result.id);
|
|
2668
|
-
if (race) openRaceDialog(race);
|
|
2669
|
-
return;
|
|
2670
|
-
}
|
|
2671
|
-
if (result.type === "organization") {
|
|
2672
|
-
await showModule("organizations");
|
|
2673
|
-
const organization = state.organizations.find((item) => item.id === result.id);
|
|
2674
|
-
if (organization) openOrganizationDialog(organization);
|
|
2675
|
-
}
|
|
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 });
|
|
2676
2733
|
}
|
|
2677
2734
|
|
|
2678
2735
|
async function showSettingsHub() {
|
|
@@ -2770,6 +2827,9 @@ function resetWorkScopedUiCaches() {
|
|
|
2770
2827
|
state.characters = [];
|
|
2771
2828
|
state.settings = [];
|
|
2772
2829
|
characterListPage = 1;
|
|
2830
|
+
relationshipFilters.fromCharacterIds = [];
|
|
2831
|
+
relationshipFilters.toCharacterIds = [];
|
|
2832
|
+
taskListPage = 1;
|
|
2773
2833
|
state.collapsedVolumeIds.clear();
|
|
2774
2834
|
state.collapsedRaceIds.clear();
|
|
2775
2835
|
lastSavedChapterSnapshot = null;
|
|
@@ -2989,6 +3049,7 @@ async function showModule(module) {
|
|
|
2989
3049
|
if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
|
|
2990
3050
|
if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
|
|
2991
3051
|
state.module = module;
|
|
3052
|
+
if (module !== "tasks") stopTaskProgressRefresh();
|
|
2992
3053
|
applyWorkAccessMode();
|
|
2993
3054
|
markActiveModule(module);
|
|
2994
3055
|
if (module === "editor") {
|
|
@@ -3021,7 +3082,7 @@ async function showModule(module) {
|
|
|
3021
3082
|
if (module === "outlines") await renderOutlines();
|
|
3022
3083
|
if (module === "relationships") await renderRelationships();
|
|
3023
3084
|
if (module === "reviews") await renderReviews();
|
|
3024
|
-
if (module === "tasks") await renderTasks();
|
|
3085
|
+
if (module === "tasks") await renderTasks(taskListPage);
|
|
3025
3086
|
if (module === "ai-settings") await renderBookAiSettings();
|
|
3026
3087
|
} catch (error) {
|
|
3027
3088
|
$("#module-content").innerHTML = `<div class="empty-state"><b>载入失败</b>${esc(error.message)}</div>`;
|
|
@@ -3316,6 +3377,15 @@ function mountCharacterFilterToggle() {
|
|
|
3316
3377
|
});
|
|
3317
3378
|
}
|
|
3318
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
|
+
|
|
3319
3389
|
function bindRecordPreview(selector, open) {
|
|
3320
3390
|
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
3321
3391
|
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization ?? card.dataset.openReview;
|
|
@@ -3436,13 +3506,14 @@ async function renderSettings() {
|
|
|
3436
3506
|
|
|
3437
3507
|
async function renderCharacters(page = characterListPage) {
|
|
3438
3508
|
const hasCharacterFilters = characterFilters.raceIds.length > 0 || characterFilters.organizationIds.length > 0;
|
|
3509
|
+
const pageSize = pageSizeFor("characters");
|
|
3439
3510
|
const [characterSource, races, organizations] = await Promise.all([
|
|
3440
|
-
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),
|
|
3441
3512
|
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
3442
3513
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
3443
3514
|
]);
|
|
3444
3515
|
const characterPage = hasCharacterFilters
|
|
3445
|
-
? paginateCharacters(filterCharacters(characterSource, characterFilters), page,
|
|
3516
|
+
? paginateCharacters(filterCharacters(characterSource, characterFilters), page, pageSize)
|
|
3446
3517
|
: characterSource;
|
|
3447
3518
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3448
3519
|
characterListPage = characterPage.page;
|
|
@@ -3745,17 +3816,34 @@ async function renderOutlines() {
|
|
|
3745
3816
|
async function renderRelationships() {
|
|
3746
3817
|
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
3747
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;
|
|
3748
3821
|
const canEditRelationships = canEditModule("relationships");
|
|
3749
|
-
mountModuleCount(
|
|
3822
|
+
mountModuleCount(filteredRelationships.length);
|
|
3750
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();
|
|
3751
3838
|
state.galaxy?.destroy();
|
|
3752
3839
|
state.relationshipExpandedMap?.destroy?.();
|
|
3753
3840
|
if ($("#relationship-map-dialog").open) $("#relationship-map-dialog").close();
|
|
3754
3841
|
const graph = buildRelationshipGraph(state.characters, relationships);
|
|
3755
3842
|
state.relationshipGraph = graph;
|
|
3756
|
-
|
|
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) => `
|
|
3757
3844
|
<tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
|
|
3758
|
-
<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}`;
|
|
3759
3847
|
const openGalaxy = () => {
|
|
3760
3848
|
state.galaxy?.destroy();
|
|
3761
3849
|
state.galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, { workId: state.work.id });
|
|
@@ -3771,7 +3859,24 @@ async function renderRelationships() {
|
|
|
3771
3859
|
};
|
|
3772
3860
|
state.relationshipMindMap?.destroy?.();
|
|
3773
3861
|
state.relationshipMindMap = renderRelationshipMindMap($("#relationship-map-host"), graph, { onOpenGalaxy: openGalaxy, onOpenExpanded: openExpanded });
|
|
3774
|
-
|
|
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))));
|
|
3775
3880
|
bindEntityHistoryButtons(async () => { await renderRelationships(); await loadAiReferences(); });
|
|
3776
3881
|
}
|
|
3777
3882
|
|
|
@@ -3867,17 +3972,36 @@ async function renderReviews() {
|
|
|
3867
3972
|
}));
|
|
3868
3973
|
}
|
|
3869
3974
|
|
|
3870
|
-
async function renderTasks() {
|
|
3871
|
-
|
|
3872
|
-
|
|
3975
|
+
async function renderTasks(page = taskListPage) {
|
|
3976
|
+
stopTaskProgressRefresh();
|
|
3977
|
+
const pageSize = pageSizeFor("analysisTasks");
|
|
3978
|
+
const [taskPage, settings] = await Promise.all([
|
|
3979
|
+
apiPage(`/api/works/${state.work.id}/tasks`, page, pageSize),
|
|
3873
3980
|
canReadModule("ai-settings")
|
|
3874
3981
|
? api(`/api/works/${state.work.id}/ai-settings`)
|
|
3875
3982
|
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
|
|
3876
3983
|
]);
|
|
3877
|
-
|
|
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);
|
|
3878
3989
|
const canConfigureAutoRun = canEditModule("tasks") && canEditModule("ai-settings");
|
|
3879
|
-
const pendingCount =
|
|
3880
|
-
const runningCount =
|
|
3990
|
+
const pendingCount = Number(taskPage.stats?.pendingCount ?? 0);
|
|
3991
|
+
const runningCount = Number(taskPage.stats?.runningCount ?? 0);
|
|
3992
|
+
const activeTaskCount = pendingCount + runningCount;
|
|
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
|
+
: "";
|
|
3881
4005
|
$("#module-content").innerHTML = `
|
|
3882
4006
|
<section class="task-auto-run-panel ${canConfigureAutoRun ? "" : "hidden"}" aria-labelledby="task-auto-run-title">
|
|
3883
4007
|
<div class="task-auto-run-copy">
|
|
@@ -3893,19 +4017,29 @@ async function renderTasks() {
|
|
|
3893
4017
|
<button id="task-auto-run-continue" class="ghost-button" type="button" ${settings.autoRunEnabled ? "" : "disabled"}>开始下一轮</button>
|
|
3894
4018
|
</div>
|
|
3895
4019
|
<p class="task-auto-run-meta">待执行队列 ${pendingCount} 个 · 正在运行 ${runningCount} 个</p>
|
|
4020
|
+
<div class="task-auto-run-progress ${activeTaskCount ? "" : "hidden"}" aria-live="polite">
|
|
4021
|
+
<div class="task-auto-run-progress-label"><span>${runningCount ? "运行中任务平均进度" : "等待任务开始"}</span><strong>${runningProgress}%</strong></div>
|
|
4022
|
+
<progress class="task-auto-run-progress-bar ${runningCount ? "is-running" : "is-waiting"}" max="100" value="${runningProgress}" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}">${runningProgress}%</progress>
|
|
4023
|
+
</div>
|
|
3896
4024
|
</section>
|
|
3897
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) => `
|
|
3898
4026
|
<tr>
|
|
3899
4027
|
<td>${esc(analysisTaskTypeLabel(item.taskType))}</td>
|
|
3900
4028
|
<td>${esc(item.scopeSummary || taskScopeLabel(item.scope?.type || "book"))}</td>
|
|
3901
|
-
<td>${
|
|
3902
|
-
<td>${
|
|
4029
|
+
<td class="task-status-cell">${renderAnalysisTaskStatus(item)}</td>
|
|
4030
|
+
<td class="task-progress-cell">${renderAnalysisTaskProgress(item)}</td>
|
|
3903
4031
|
<td class="task-row-actions">
|
|
3904
4032
|
<button class="ghost-button" type="button" data-task-detail="${esc(item.id)}">详情</button>
|
|
3905
4033
|
${item.status === "pending" ? `<button class="ghost-button" type="button" data-run-task="${esc(item.id)}">运行</button>` : ""}
|
|
3906
4034
|
${item.status === "pending" || item.status === "running" ? `<button class="ghost-button" type="button" data-cancel-task="${esc(item.id)}">取消</button>` : ""}
|
|
3907
4035
|
</td>
|
|
3908
|
-
</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
|
+
}));
|
|
3909
4043
|
|
|
3910
4044
|
$("#task-auto-run-save")?.addEventListener("click", async () => {
|
|
3911
4045
|
const button = $("#task-auto-run-save");
|
|
@@ -3944,8 +4078,15 @@ async function renderTasks() {
|
|
|
3944
4078
|
$("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
|
|
3945
4079
|
if (button.disabled) return;
|
|
3946
4080
|
button.disabled = true;
|
|
3947
|
-
|
|
3948
|
-
|
|
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))
|
|
3949
4090
|
.catch((error) => toast(error.message, "error"))
|
|
3950
4091
|
.finally(() => { button.disabled = false; });
|
|
3951
4092
|
}));
|
|
@@ -3954,8 +4095,15 @@ async function renderTasks() {
|
|
|
3954
4095
|
try {
|
|
3955
4096
|
button.disabled = true;
|
|
3956
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);
|
|
3957
4104
|
const cancel = button.parentElement.querySelector("[data-cancel-task]");
|
|
3958
4105
|
if (cancel) cancel.textContent = "取消运行";
|
|
4106
|
+
scheduleTaskProgressRefresh(workId, 1);
|
|
3959
4107
|
const completed = await api(`/api/tasks/${button.dataset.runTask}/run`, { method: "POST", body: { modelId: $("#ai-model").value || undefined } });
|
|
3960
4108
|
toast(completed.status === "cancelled" ? "分析任务已取消" : completed.status === "expired" ? "正文已变化,本次分析已过期" : "分析已完成");
|
|
3961
4109
|
if (state.module === "tasks" && state.work?.id === workId) await renderTasks();
|
|
@@ -3975,9 +4123,171 @@ async function renderTasks() {
|
|
|
3975
4123
|
button.disabled = false;
|
|
3976
4124
|
}
|
|
3977
4125
|
}));
|
|
4126
|
+
scheduleTaskProgressRefresh(state.work.id, runningCount);
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
function stopTaskProgressRefresh() {
|
|
4130
|
+
if (taskProgressRefreshTimer === null) return;
|
|
4131
|
+
window.clearTimeout(taskProgressRefreshTimer);
|
|
4132
|
+
taskProgressRefreshTimer = null;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
function scheduleTaskProgressRefresh(workId, runningCount) {
|
|
4136
|
+
stopTaskProgressRefresh();
|
|
4137
|
+
if (runningCount === 0) return;
|
|
4138
|
+
taskProgressRefreshTimer = window.setTimeout(async () => {
|
|
4139
|
+
taskProgressRefreshTimer = null;
|
|
4140
|
+
if (state.module !== "tasks" || state.work?.id !== workId) return;
|
|
4141
|
+
if ($(".task-auto-run-controls")?.contains(document.activeElement)) {
|
|
4142
|
+
scheduleTaskProgressRefresh(workId, runningCount);
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
try {
|
|
4146
|
+
await renderTasks();
|
|
4147
|
+
} catch (error) {
|
|
4148
|
+
console.error("Failed to refresh task progress", error);
|
|
4149
|
+
scheduleTaskProgressRefresh(workId, runningCount);
|
|
4150
|
+
}
|
|
4151
|
+
}, taskProgressRefreshInterval);
|
|
4152
|
+
}
|
|
4153
|
+
|
|
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>`;
|
|
3978
4174
|
}
|
|
3979
4175
|
|
|
3980
|
-
function
|
|
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) {
|
|
3981
4291
|
if (!task) return;
|
|
3982
4292
|
const details = Array.isArray(task.scopeDetails) ? task.scopeDetails : [];
|
|
3983
4293
|
const detailHtml = details.map((item) => {
|
|
@@ -4004,18 +4314,21 @@ function openTaskDetailDialog(task) {
|
|
|
4004
4314
|
: "<p>尚无结果</p>";
|
|
4005
4315
|
openDialog("任务详情",
|
|
4006
4316
|
`<div class="task-detail">
|
|
4007
|
-
<
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
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)}
|
|
4015
4328
|
</div>`,
|
|
4016
4329
|
async () => undefined,
|
|
4017
4330
|
"AI 分析详情",
|
|
4018
|
-
{ submitLabel: "关闭", wide: true });
|
|
4331
|
+
{ submitLabel: "关闭", wide: true, trace: true });
|
|
4019
4332
|
}
|
|
4020
4333
|
|
|
4021
4334
|
function renderProviderCards(providers, models) {
|
|
@@ -4518,39 +4831,78 @@ function commitRelationshipKeywordInputs(container) {
|
|
|
4518
4831
|
|
|
4519
4832
|
function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
4520
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 = [];
|
|
4521
4842
|
$("#dialog-title").textContent = title;
|
|
4522
4843
|
$("#dialog-eyebrow").textContent = eyebrow;
|
|
4523
4844
|
$("#dialog-meta").textContent = options.meta ?? "";
|
|
4524
4845
|
$("#dialog-meta").classList.toggle("hidden", !options.meta);
|
|
4525
4846
|
$("#dialog-fields").innerHTML = fields;
|
|
4526
|
-
|
|
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");
|
|
4527
4852
|
$("#dynamic-form .dialog-actions [value='cancel']").classList.toggle("hidden", Boolean(options.hideCancel));
|
|
4528
|
-
|
|
4853
|
+
dialog.classList.toggle("wide-dialog", Boolean(options.wide));
|
|
4854
|
+
dialog.classList.toggle("trace-dialog", Boolean(options.trace));
|
|
4529
4855
|
bindDynamicListControls($("#dialog-fields"));
|
|
4530
4856
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
4531
4857
|
bindVditorEditors($("#dialog-fields"));
|
|
4532
|
-
|
|
4858
|
+
form.onclick = null;
|
|
4859
|
+
form.onkeydown = null;
|
|
4860
|
+
dialog.oncancel = (event) => {
|
|
4861
|
+
if (submitting) event.preventDefault();
|
|
4862
|
+
};
|
|
4533
4863
|
form.onsubmit = async (event) => {
|
|
4864
|
+
if (submitting) {
|
|
4865
|
+
event.preventDefault();
|
|
4866
|
+
return;
|
|
4867
|
+
}
|
|
4534
4868
|
if (event.submitter?.value === "cancel") {
|
|
4535
4869
|
void discardPendingMarkdownAttachments();
|
|
4536
4870
|
return;
|
|
4537
4871
|
}
|
|
4538
4872
|
event.preventDefault();
|
|
4539
|
-
|
|
4540
|
-
|
|
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 ?? "处理中…";
|
|
4541
4878
|
try {
|
|
4542
4879
|
commitRelationshipKeywordInputs(form);
|
|
4543
|
-
|
|
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);
|
|
4544
4886
|
const markdown = [...form.querySelectorAll("[data-vditor-value]")].map((textarea) => textarea.value).join("\n\n");
|
|
4545
4887
|
await cleanupPendingMarkdownAttachments(markdown);
|
|
4546
|
-
|
|
4888
|
+
dialog.close();
|
|
4547
4889
|
} catch (error) {
|
|
4548
|
-
|
|
4890
|
+
const message = error instanceof Error ? error.message : "未知错误";
|
|
4891
|
+
toast(`${options.errorPrefix ?? ""}${message}`, "error");
|
|
4549
4892
|
} finally {
|
|
4550
|
-
|
|
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;
|
|
4551
4902
|
}
|
|
4552
4903
|
};
|
|
4553
|
-
|
|
4904
|
+
dialog.showModal();
|
|
4905
|
+
$("#dialog-fields").scrollTop = 0;
|
|
4554
4906
|
}
|
|
4555
4907
|
|
|
4556
4908
|
function openWorkDialog() {
|
|
@@ -5922,32 +6274,165 @@ function openReviewDialog() {
|
|
|
5922
6274
|
});
|
|
5923
6275
|
}
|
|
5924
6276
|
|
|
5925
|
-
function openTaskDialog() {
|
|
6277
|
+
async function openTaskDialog() {
|
|
5926
6278
|
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]));
|
|
6279
|
+
let relationshipCharacters = [];
|
|
6280
|
+
try {
|
|
6281
|
+
relationshipCharacters = canReadModule("characters")
|
|
6282
|
+
? await apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
6283
|
+
: [];
|
|
6284
|
+
} catch (error) {
|
|
6285
|
+
toast(`角色列表加载失败:${error.message}`, "error");
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
|
|
6289
|
+
const relationshipCharacterPicker = `<div class="form-field relationship-character-field">
|
|
6290
|
+
<span id="relationship-character-label">被分析角色(可多选)</span>
|
|
6291
|
+
<div class="relationship-character-picker">
|
|
6292
|
+
<button class="relationship-character-trigger" type="button" aria-expanded="false" aria-controls="relationship-character-bubble" aria-labelledby="relationship-character-label relationship-character-summary">
|
|
6293
|
+
<span id="relationship-character-summary">选择需要定向分析的角色</span>
|
|
6294
|
+
<span class="relationship-character-trigger-meta"><span data-relationship-character-count>未选择</span><span class="relationship-character-chevron" aria-hidden="true">⌄</span></span>
|
|
6295
|
+
</button>
|
|
6296
|
+
<div id="relationship-character-bubble" class="relationship-character-bubble hidden">
|
|
6297
|
+
<label class="relationship-character-search">筛选角色<input type="search" data-relationship-character-search placeholder="输入角色名" autocomplete="off"></label>
|
|
6298
|
+
<div class="relationship-character-bubble-meta"><span>共 ${characterOptions.length} 个角色</span><button type="button" data-relationship-character-clear disabled>清空选择</button></div>
|
|
6299
|
+
<div class="relationship-character-options" role="group" aria-labelledby="relationship-character-label">
|
|
6300
|
+
${characterOptions.map(([characterId, characterName]) => `<label class="relationship-character-chip" data-character-search-name="${esc(String(characterName).toLocaleLowerCase())}"><input type="checkbox" name="characterIds" value="${esc(characterId)}" data-character-name="${esc(characterName)}"><span>${esc(characterName)}</span></label>`).join("")}
|
|
6301
|
+
</div>
|
|
6302
|
+
<p class="relationship-character-empty hidden" data-relationship-character-empty>没有匹配的角色</p>
|
|
6303
|
+
</div>
|
|
6304
|
+
</div>
|
|
6305
|
+
</div>`;
|
|
5927
6306
|
const defaultTaskType = ANALYSIS_TYPES[0].value;
|
|
5928
6307
|
const taskTypeField = `<div class="form-field analysis-type-field"><label>分析类型<select name="taskType" aria-describedby="analysis-type-description">${ANALYSIS_TYPES.map(({ value, label }) => `<option value="${esc(value)}" ${value === defaultTaskType ? "selected" : ""}>${esc(label)}</option>`).join("")}</select></label><p id="analysis-type-description" class="analysis-type-description" aria-live="polite">${esc(analysisTypeDescription(defaultTaskType))}</p></div>`;
|
|
5929
6308
|
const chapterField = `<label class="task-chapter-field">章节<select name="chapterId">${chapterOptions.map(([key, text], index) => `<option value="${esc(key)}" ${index === 0 ? "selected" : ""}>${esc(text)}</option>`).join("")}</select></label>`;
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
6309
|
+
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
6310
|
+
${relationshipCharacterPicker}
|
|
6311
|
+
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。</span></p>
|
|
6312
|
+
<div class="relationship-overwrite-card hidden">
|
|
6313
|
+
<label class="checkbox-field"><input name="replaceExistingRelationships" type="checkbox" disabled><span>用本次结果覆盖所选角色的已有关系</span></label>
|
|
6314
|
+
<p>任务成功后,会先删除所有涉及所选角色的旧关系,再写入本次分析结果。</p>
|
|
6315
|
+
</div>
|
|
6316
|
+
<label>额外分析提示<textarea name="additionalPrompt" maxlength="10000" placeholder="例如:重点识别权力继承、师承变化或隐秘亲缘关系"></textarea><small>将同时追加到证据收集和全局关系归纳提示词,仅影响本次任务。</small></label>
|
|
6317
|
+
</div>`;
|
|
6318
|
+
openDialog("开始 AI 分析", taskTypeField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
6319
|
+
const taskType = String(form.get("taskType"));
|
|
6320
|
+
const scopeType = String(form.get("scopeType"));
|
|
6321
|
+
const includeAllSettings = taskType === "relationship-analysis" && scopeType === "book-with-settings";
|
|
6322
|
+
const additionalPrompt = taskType === "relationship-analysis" ? String(form.get("additionalPrompt") ?? "").trim() : "";
|
|
6323
|
+
const characterIds = taskType === "relationship-analysis" ? form.getAll("characterIds").map(String).filter(Boolean) : [];
|
|
6324
|
+
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
6325
|
+
const scope = taskType === "character-identity-audit" || scopeType === "book" || includeAllSettings
|
|
6326
|
+
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6327
|
+
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
6328
|
+
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope } });
|
|
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: "任务创建失败:"
|
|
5934
6337
|
});
|
|
5935
6338
|
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
5936
6339
|
const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
|
|
5937
6340
|
const chapterSelect = $("#dialog-fields").querySelector('select[name="chapterId"]');
|
|
5938
6341
|
const chapterFieldElement = chapterSelect.closest(".task-chapter-field");
|
|
5939
6342
|
const description = $("#analysis-type-description");
|
|
6343
|
+
const relationshipOptions = $("#dialog-fields").querySelector(".relationship-analysis-options");
|
|
6344
|
+
const relationshipPrompt = relationshipOptions.querySelector('textarea[name="additionalPrompt"]');
|
|
6345
|
+
const relationshipCharacterPickerElement = relationshipOptions.querySelector(".relationship-character-picker");
|
|
6346
|
+
const relationshipCharacterTrigger = relationshipOptions.querySelector(".relationship-character-trigger");
|
|
6347
|
+
const relationshipCharacterBubble = relationshipOptions.querySelector(".relationship-character-bubble");
|
|
6348
|
+
const relationshipCharacterSearch = relationshipOptions.querySelector("[data-relationship-character-search]");
|
|
6349
|
+
const relationshipCharacterInputs = [...relationshipOptions.querySelectorAll('input[name="characterIds"]')];
|
|
6350
|
+
const relationshipCharacterSummary = relationshipOptions.querySelector("#relationship-character-summary");
|
|
6351
|
+
const relationshipCharacterCount = relationshipOptions.querySelector("[data-relationship-character-count]");
|
|
6352
|
+
const relationshipCharacterClear = relationshipOptions.querySelector("[data-relationship-character-clear]");
|
|
6353
|
+
const relationshipCharacterEmpty = relationshipOptions.querySelector("[data-relationship-character-empty]");
|
|
6354
|
+
const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
|
|
6355
|
+
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-overwrite-card");
|
|
6356
|
+
const allSettingsOption = document.createElement("option");
|
|
6357
|
+
allSettingsOption.value = "book-with-settings";
|
|
6358
|
+
allSettingsOption.textContent = "全书 + 所有设定";
|
|
5940
6359
|
const syncChapterField = () => {
|
|
5941
|
-
const disabled = scopeTypeSelect.value
|
|
6360
|
+
const disabled = scopeTypeSelect.value !== "chapter";
|
|
5942
6361
|
chapterSelect.disabled = disabled;
|
|
5943
6362
|
chapterFieldElement.classList.toggle("is-disabled", disabled);
|
|
5944
6363
|
chapterFieldElement.setAttribute("aria-disabled", String(disabled));
|
|
5945
6364
|
};
|
|
6365
|
+
const setRelationshipCharacterBubbleOpen = (open) => {
|
|
6366
|
+
relationshipCharacterBubble.classList.toggle("hidden", !open);
|
|
6367
|
+
relationshipCharacterTrigger.setAttribute("aria-expanded", String(open));
|
|
6368
|
+
if (open) relationshipCharacterSearch.focus();
|
|
6369
|
+
};
|
|
6370
|
+
const syncRelationshipCharacterPicker = () => {
|
|
6371
|
+
const selected = relationshipCharacterInputs.filter((input) => input.checked);
|
|
6372
|
+
const selectedNames = selected.map((input) => input.dataset.characterName);
|
|
6373
|
+
relationshipCharacterSummary.textContent = selectedNames.length
|
|
6374
|
+
? `${selectedNames.slice(0, 2).join("、")}${selectedNames.length > 2 ? ` 等 ${selectedNames.length} 人` : ""}`
|
|
6375
|
+
: "选择需要定向分析的角色";
|
|
6376
|
+
relationshipCharacterCount.textContent = selected.length ? `已选 ${selected.length}` : "未选择";
|
|
6377
|
+
relationshipCharacterClear.disabled = selected.length === 0;
|
|
6378
|
+
relationshipCharacterTrigger.setAttribute("aria-label", `筛选被分析角色,已选择 ${selected.length} 个`);
|
|
6379
|
+
};
|
|
6380
|
+
const filterRelationshipCharacters = () => {
|
|
6381
|
+
const query = relationshipCharacterSearch.value.trim().toLocaleLowerCase();
|
|
6382
|
+
let visibleCount = 0;
|
|
6383
|
+
for (const input of relationshipCharacterInputs) {
|
|
6384
|
+
const chip = input.closest(".relationship-character-chip");
|
|
6385
|
+
const visible = !query || chip.dataset.characterSearchName.includes(query);
|
|
6386
|
+
chip.classList.toggle("hidden", !visible);
|
|
6387
|
+
if (visible) visibleCount += 1;
|
|
6388
|
+
}
|
|
6389
|
+
relationshipCharacterEmpty.classList.toggle("hidden", visibleCount > 0);
|
|
6390
|
+
};
|
|
6391
|
+
const syncRelationshipOptions = () => {
|
|
6392
|
+
const enabled = taskTypeSelect.value === "relationship-analysis";
|
|
6393
|
+
if (enabled && !allSettingsOption.isConnected) scopeTypeSelect.append(allSettingsOption);
|
|
6394
|
+
if (!enabled && allSettingsOption.isConnected) {
|
|
6395
|
+
if (scopeTypeSelect.value === allSettingsOption.value) scopeTypeSelect.value = "book";
|
|
6396
|
+
allSettingsOption.remove();
|
|
6397
|
+
}
|
|
6398
|
+
relationshipOptions.classList.toggle("hidden", !enabled);
|
|
6399
|
+
relationshipPrompt.disabled = !enabled;
|
|
6400
|
+
relationshipCharacterTrigger.disabled = !enabled;
|
|
6401
|
+
relationshipCharacterSearch.disabled = !enabled;
|
|
6402
|
+
for (const input of relationshipCharacterInputs) input.disabled = !enabled;
|
|
6403
|
+
if (!enabled) setRelationshipCharacterBubbleOpen(false);
|
|
6404
|
+
const hasSelectedCharacters = enabled && relationshipCharacterInputs.some((input) => input.checked);
|
|
6405
|
+
replaceRelationships.disabled = !hasSelectedCharacters;
|
|
6406
|
+
relationshipOverwriteCard.classList.toggle("hidden", !hasSelectedCharacters);
|
|
6407
|
+
if (!hasSelectedCharacters) replaceRelationships.checked = false;
|
|
6408
|
+
syncRelationshipCharacterPicker();
|
|
6409
|
+
filterRelationshipCharacters();
|
|
6410
|
+
syncChapterField();
|
|
6411
|
+
};
|
|
5946
6412
|
taskTypeSelect.addEventListener("change", () => {
|
|
5947
6413
|
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
6414
|
+
syncRelationshipOptions();
|
|
5948
6415
|
});
|
|
6416
|
+
relationshipCharacterTrigger.addEventListener("click", () => {
|
|
6417
|
+
setRelationshipCharacterBubbleOpen(relationshipCharacterTrigger.getAttribute("aria-expanded") !== "true");
|
|
6418
|
+
});
|
|
6419
|
+
relationshipCharacterSearch.addEventListener("input", filterRelationshipCharacters);
|
|
6420
|
+
for (const input of relationshipCharacterInputs) input.addEventListener("change", syncRelationshipOptions);
|
|
6421
|
+
relationshipCharacterClear.addEventListener("click", () => {
|
|
6422
|
+
for (const input of relationshipCharacterInputs) input.checked = false;
|
|
6423
|
+
syncRelationshipOptions();
|
|
6424
|
+
});
|
|
6425
|
+
$("#dynamic-form").onclick = (event) => {
|
|
6426
|
+
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
6427
|
+
};
|
|
6428
|
+
$("#dynamic-form").onkeydown = (event) => {
|
|
6429
|
+
if (event.key !== "Escape" || relationshipCharacterBubble.classList.contains("hidden")) return;
|
|
6430
|
+
event.preventDefault();
|
|
6431
|
+
setRelationshipCharacterBubbleOpen(false);
|
|
6432
|
+
relationshipCharacterTrigger.focus();
|
|
6433
|
+
};
|
|
5949
6434
|
scopeTypeSelect.addEventListener("change", syncChapterField);
|
|
5950
|
-
|
|
6435
|
+
syncRelationshipOptions();
|
|
5951
6436
|
}
|
|
5952
6437
|
|
|
5953
6438
|
function openProviderDialog(item) {
|
|
@@ -6038,7 +6523,7 @@ async function sendAi() {
|
|
|
6038
6523
|
} else {
|
|
6039
6524
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
6040
6525
|
assistantContent = suggestion.content;
|
|
6041
|
-
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens };
|
|
6526
|
+
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
|
|
6042
6527
|
}
|
|
6043
6528
|
try {
|
|
6044
6529
|
const persistedAssistantMessage = await persistAiConversationMessage("assistant", assistantContent, [], assistantMetadata);
|
|
@@ -6134,9 +6619,9 @@ async function streamChat(body) {
|
|
|
6134
6619
|
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : toolCalls;
|
|
6135
6620
|
processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : processSteps;
|
|
6136
6621
|
const processDurationMs = elapsedProcessTime();
|
|
6137
|
-
generatedMetadata = { modelDisplayName: payload.model?.displayName, outputTokens: payload.outputTokens, toolCalls, processSteps, processDurationMs };
|
|
6622
|
+
generatedMetadata = { modelDisplayName: payload.model?.displayName, outputTokens: payload.outputTokens, cacheHitPercent: payload.cacheHitPercent, toolCalls, processSteps, processDurationMs };
|
|
6138
6623
|
renderAiProcessSteps(message, processSteps, true, processDurationMs);
|
|
6139
|
-
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens);
|
|
6624
|
+
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent);
|
|
6140
6625
|
attachAssistantCopyAction(message, streamedText);
|
|
6141
6626
|
scrollAiFeedToBottom();
|
|
6142
6627
|
} else if (eventName === "error") {
|
|
@@ -6191,7 +6676,7 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
6191
6676
|
const outputTokens = Number.isFinite(metadata?.outputTokens) ? metadata.outputTokens : estimateAiMessageTokens(text);
|
|
6192
6677
|
const meta = document.createElement("div");
|
|
6193
6678
|
meta.className = "message-meta";
|
|
6194
|
-
meta.textContent = formatAiMessageMeta(modelDisplayName, outputTokens);
|
|
6679
|
+
meta.textContent = formatAiMessageMeta(modelDisplayName, outputTokens, metadata?.cacheHitPercent);
|
|
6195
6680
|
message.append(meta);
|
|
6196
6681
|
attachAssistantCopyAction(message, text);
|
|
6197
6682
|
}
|
|
@@ -6206,7 +6691,7 @@ function appendSuggestion(suggestion, createdAt = null, messageId = null) {
|
|
|
6206
6691
|
const applicable = suggestion.action !== "note";
|
|
6207
6692
|
const guard = suggestion.guard;
|
|
6208
6693
|
const guardHtml = guard ? `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${guard.issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<p>${esc(guard.failure || "无法完成检查,请谨慎采纳")}</p>` : guard.issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>` : "";
|
|
6209
|
-
message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, `基于 v${suggestion.chapterVersion ?? "-"}`))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
|
|
6694
|
+
message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, suggestion.cacheHitPercent, `基于 v${suggestion.chapterVersion ?? "-"}`))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
|
|
6210
6695
|
attachMessageHeading(message, "助手建议", createdAt ?? undefined);
|
|
6211
6696
|
attachAssistantCopyAction(message, suggestion.content);
|
|
6212
6697
|
attachMessageIdentity(message, messageId);
|
|
@@ -6346,7 +6831,7 @@ async function loadImportHistoryPage(page) {
|
|
|
6346
6831
|
const workId = state.work?.id;
|
|
6347
6832
|
if (!workId || !page) return;
|
|
6348
6833
|
const requestId = ++importHistoryRequestId;
|
|
6349
|
-
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"));
|
|
6350
6835
|
if (requestId !== importHistoryRequestId || state.work?.id !== workId || !$("#import-history-dialog").open) return;
|
|
6351
6836
|
importHistoryRecords = page === 1 ? result.items : [...importHistoryRecords, ...result.items];
|
|
6352
6837
|
importHistoryNextPage = result.nextPage;
|
|
@@ -6947,11 +7432,20 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
6947
7432
|
try {
|
|
6948
7433
|
const settings = await api("/api/platform/ui-settings", {
|
|
6949
7434
|
method: "PATCH",
|
|
6950
|
-
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
|
+
}
|
|
6951
7443
|
});
|
|
6952
7444
|
applyPlatformUiSettings(settings);
|
|
7445
|
+
characterListPage = 1;
|
|
7446
|
+
taskListPage = 1;
|
|
6953
7447
|
$("#platform-ui-settings-dialog").close();
|
|
6954
|
-
toast("
|
|
7448
|
+
toast("界面与分页设置已保存");
|
|
6955
7449
|
} catch (error) {
|
|
6956
7450
|
toast(error.message, "error");
|
|
6957
7451
|
} finally {
|