@musnows/scriverse 0.5.11 → 0.5.12
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 +23 -1
- package/dist/ai.js.map +1 -1
- package/dist/app.js +19 -1
- package/dist/app.js.map +1 -1
- package/dist/database.js +18 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +280 -63
- package/dist/public/index.html +2 -2
- package/dist/public/module-request-cache.d.ts +17 -0
- package/dist/public/module-request-cache.js +35 -0
- package/dist/public/stream-typewriter.d.ts +1 -1
- package/dist/public/stream-typewriter.js +16 -12
- package/dist/public/styles.css +10 -5
- package/dist/store.js +56 -16
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/writing-progress-time.js +71 -0
- package/dist/writing-progress-time.js.map +1 -0
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -8,7 +8,7 @@ import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260
|
|
|
8
8
|
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
|
|
9
9
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
10
10
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
11
|
-
import { createStreamTypewriter } from "/stream-typewriter.js?v=
|
|
11
|
+
import { createStreamTypewriter } from "/stream-typewriter.js?v=20260730-ai-stream-typewriter-v3";
|
|
12
12
|
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
13
13
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
14
14
|
import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=20260718-layered-context";
|
|
@@ -48,6 +48,7 @@ import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-s
|
|
|
48
48
|
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
49
49
|
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
50
50
|
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
|
|
51
|
+
import { createModuleRequestCache } from "/module-request-cache.js?v=20260730-module-request-cache-v1";
|
|
51
52
|
import {
|
|
52
53
|
clampCropRect,
|
|
53
54
|
containImageRect,
|
|
@@ -117,6 +118,21 @@ const state = {
|
|
|
117
118
|
contextChapterId: null
|
|
118
119
|
};
|
|
119
120
|
|
|
121
|
+
const moduleRequestCache = createModuleRequestCache();
|
|
122
|
+
const cachedWorkModules = new Set([
|
|
123
|
+
"drafts",
|
|
124
|
+
"settings",
|
|
125
|
+
"characters",
|
|
126
|
+
"races",
|
|
127
|
+
"organizations",
|
|
128
|
+
"timeline",
|
|
129
|
+
"outlines",
|
|
130
|
+
"relationships",
|
|
131
|
+
"reviews",
|
|
132
|
+
"tasks",
|
|
133
|
+
"ai-settings"
|
|
134
|
+
]);
|
|
135
|
+
|
|
120
136
|
function createPresenceClientId() {
|
|
121
137
|
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
122
138
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
@@ -371,6 +387,9 @@ let aiReferencesLoadWorkId = null;
|
|
|
371
387
|
let aiConversationsLoadPromise = null;
|
|
372
388
|
let aiConversationsLoadWorkId = null;
|
|
373
389
|
let workScopedUiGeneration = 0;
|
|
390
|
+
const loadedVolumeChapterIds = new Set();
|
|
391
|
+
const volumeChapterLoadingIds = new Set();
|
|
392
|
+
const volumeChapterRequests = new Map();
|
|
374
393
|
let raceHierarchyLoadPromise = null;
|
|
375
394
|
let raceHierarchyLoadWorkId = null;
|
|
376
395
|
let loadedRaceHierarchyWorkId = null;
|
|
@@ -874,6 +893,7 @@ let chapterEditorReadOnly = true;
|
|
|
874
893
|
let characterListPage = 1;
|
|
875
894
|
let taskListPage = 1;
|
|
876
895
|
let draftTypeFilter = "all";
|
|
896
|
+
let draftFiltersPanelOpen = false;
|
|
877
897
|
const moduleListPages = {
|
|
878
898
|
drafts: 1,
|
|
879
899
|
settings: 1,
|
|
@@ -2126,14 +2146,19 @@ async function api(path, options = {}) {
|
|
|
2126
2146
|
if (response.status === 401 && !path.startsWith("/api/auth/") && !path.includes("/presence")) {
|
|
2127
2147
|
state.user = null;
|
|
2128
2148
|
state.csrfToken = null;
|
|
2149
|
+
moduleRequestCache.clear();
|
|
2129
2150
|
showAuth(false);
|
|
2130
2151
|
}
|
|
2131
2152
|
const error = new Error(payload.error?.message ?? `请求失败:${response.status}`);
|
|
2132
2153
|
error.code = payload.error?.code;
|
|
2133
2154
|
throw error;
|
|
2134
2155
|
}
|
|
2135
|
-
if (response.status === 204)
|
|
2156
|
+
if (response.status === 204) {
|
|
2157
|
+
invalidateModuleRequestsAfterMutation(path, method);
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2136
2160
|
const payload = await response.json();
|
|
2161
|
+
invalidateModuleRequestsAfterMutation(path, method);
|
|
2137
2162
|
return payload.data;
|
|
2138
2163
|
}
|
|
2139
2164
|
|
|
@@ -2155,6 +2180,63 @@ async function apiAllPages(path, limit = 100) {
|
|
|
2155
2180
|
}
|
|
2156
2181
|
}
|
|
2157
2182
|
|
|
2183
|
+
function cachedModuleRequest(module, requestKey, loader, options = {}) {
|
|
2184
|
+
const workId = state.work?.id;
|
|
2185
|
+
if (!workId) return Promise.resolve().then(loader);
|
|
2186
|
+
return moduleRequestCache.request(workId, module, requestKey, loader, options);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function moduleApi(module, path, options = {}) {
|
|
2190
|
+
return cachedModuleRequest(module, `api:${path}`, () => api(path), options);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function moduleApiPage(module, path, page = 1, limit = 30, options = {}) {
|
|
2194
|
+
return cachedModuleRequest(module, `page:${path}:${page}:${limit}`, () => apiPage(path, page, limit), options);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function moduleApiAllPages(module, path, limit = 100, options = {}) {
|
|
2198
|
+
return cachedModuleRequest(module, `all:${path}:${limit}`, () => apiAllPages(path, limit), options);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function invalidateModuleRequestsAfterMutation(path, method) {
|
|
2202
|
+
if (["GET", "HEAD", "OPTIONS"].includes(method) || !state.work) return;
|
|
2203
|
+
if (
|
|
2204
|
+
path.startsWith("/api/auth/")
|
|
2205
|
+
|| path.includes("/presence")
|
|
2206
|
+
|| path.includes("/context/prepare")
|
|
2207
|
+
|| path.includes("/ai-context-usage")
|
|
2208
|
+
|| path.includes("/chat/stream")
|
|
2209
|
+
) return;
|
|
2210
|
+
|
|
2211
|
+
const affected = new Set();
|
|
2212
|
+
if (path.includes("/ai-settings") || path.includes("/task-defaults") || path.includes("/providers") || path.includes("/models")) {
|
|
2213
|
+
affected.add("ai-settings");
|
|
2214
|
+
}
|
|
2215
|
+
if (path.includes("/tasks")) affected.add("tasks");
|
|
2216
|
+
if (path.includes("/drafts")) affected.add("drafts");
|
|
2217
|
+
if (path.includes("/settings") && !path.includes("/ai-settings")) affected.add("settings");
|
|
2218
|
+
if (path.includes("/characters") || path.includes("/character-sections")) affected.add("characters");
|
|
2219
|
+
if (path.includes("/races")) affected.add("races");
|
|
2220
|
+
if (path.includes("/organizations")) affected.add("organizations");
|
|
2221
|
+
if (path.includes("/timeline")) affected.add("timeline");
|
|
2222
|
+
if (path.includes("/outlines") || path.includes("/foreshadows")) affected.add("outlines");
|
|
2223
|
+
if (path.includes("/relationships")) affected.add("relationships");
|
|
2224
|
+
if (path.includes("/reviews")) affected.add("reviews");
|
|
2225
|
+
if (path.includes("/entity-versions/")) {
|
|
2226
|
+
if (path.includes("/draft/")) affected.add("drafts");
|
|
2227
|
+
if (path.includes("/setting/")) affected.add("settings");
|
|
2228
|
+
if (path.includes("/character/")) affected.add("characters");
|
|
2229
|
+
if (path.includes("/race/")) affected.add("races");
|
|
2230
|
+
if (path.includes("/organization/")) affected.add("organizations");
|
|
2231
|
+
if (path.includes("/timeline-event/") || path.includes("/timeline-track/")) affected.add("timeline");
|
|
2232
|
+
if (path.includes("/chapter-outline/") || path.includes("/foreshadow/")) affected.add("outlines");
|
|
2233
|
+
if (path.includes("/relationship/")) affected.add("relationships");
|
|
2234
|
+
if (path.includes("/review/")) affected.add("reviews");
|
|
2235
|
+
}
|
|
2236
|
+
if (cachedWorkModules.has(state.module)) affected.add(state.module);
|
|
2237
|
+
affected.forEach((module) => moduleRequestCache.invalidate(state.work.id, module));
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2158
2240
|
async function initializeProductFooters() {
|
|
2159
2241
|
const year = String(new Date().getFullYear());
|
|
2160
2242
|
document.querySelectorAll("[data-product-footer-year]").forEach((element) => { element.textContent = year; });
|
|
@@ -3345,11 +3427,15 @@ function resetWorkScopedUiCaches() {
|
|
|
3345
3427
|
state.races = [];
|
|
3346
3428
|
characterListPage = 1;
|
|
3347
3429
|
draftTypeFilter = "all";
|
|
3430
|
+
draftFiltersPanelOpen = false;
|
|
3348
3431
|
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
3349
3432
|
relationshipFilters.fromCharacterIds = [];
|
|
3350
3433
|
relationshipFilters.toCharacterIds = [];
|
|
3351
3434
|
taskListPage = 1;
|
|
3352
3435
|
state.collapsedVolumeIds.clear();
|
|
3436
|
+
loadedVolumeChapterIds.clear();
|
|
3437
|
+
volumeChapterLoadingIds.clear();
|
|
3438
|
+
volumeChapterRequests.clear();
|
|
3353
3439
|
state.collapsedRaceIds.clear();
|
|
3354
3440
|
lastSavedChapterSnapshot = null;
|
|
3355
3441
|
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
@@ -3373,7 +3459,13 @@ function resetWorkScopedUiCaches() {
|
|
|
3373
3459
|
async function selectWork(workId, preferredChapterId = null) {
|
|
3374
3460
|
const discarding = state.work?.id !== workId && state.dirty;
|
|
3375
3461
|
if (discarding && !(await confirmDiscardChanges())) return false;
|
|
3376
|
-
|
|
3462
|
+
if (state.work?.id === workId) {
|
|
3463
|
+
workScopedUiGeneration += 1;
|
|
3464
|
+
loadedVolumeChapterIds.clear();
|
|
3465
|
+
volumeChapterLoadingIds.clear();
|
|
3466
|
+
volumeChapterRequests.clear();
|
|
3467
|
+
}
|
|
3468
|
+
const nextWork = await api(`/api/works/${workId}?directory=volumes`);
|
|
3377
3469
|
if (state.work?.id !== nextWork.id) resetWorkScopedUiCaches();
|
|
3378
3470
|
if (discarding) setSaveState("就绪");
|
|
3379
3471
|
$("#app").classList.remove("shelf-mode");
|
|
@@ -3384,6 +3476,8 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
3384
3476
|
$("#settings-button").removeAttribute("aria-current");
|
|
3385
3477
|
settingsReturnContext = null;
|
|
3386
3478
|
state.work = nextWork;
|
|
3479
|
+
state.work.volumes = state.work.volumes.map((volume) => ({ ...volume, chapters: Array.isArray(volume.chapters) ? volume.chapters : [] }));
|
|
3480
|
+
state.collapsedVolumeIds = new Set(state.work.volumes.map((volume) => volume.id));
|
|
3387
3481
|
state.chapter = null;
|
|
3388
3482
|
chapterEditorReadOnly = true;
|
|
3389
3483
|
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
@@ -3393,40 +3487,121 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
3393
3487
|
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
3394
3488
|
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
3395
3489
|
renderTree();
|
|
3396
|
-
|
|
3397
|
-
const targetChapter = chapters.find((chapter) => chapter.id === preferredChapterId) ?? chapters[0];
|
|
3490
|
+
void loadAllVolumeChapters(nextWork.id);
|
|
3398
3491
|
if (state.module === "editor" && preferredChapterId) await selectChapter(preferredChapterId);
|
|
3399
|
-
else if (state.module === "editor" && targetChapter) await selectChapter(targetChapter.id);
|
|
3400
3492
|
else if (state.module === "editor" && canReadModule("editor")) showWelcome(true);
|
|
3401
3493
|
else if (!canReadModule(state.module)) showWelcome(true);
|
|
3402
3494
|
else await showModule(state.module);
|
|
3403
3495
|
return true;
|
|
3404
3496
|
}
|
|
3405
3497
|
|
|
3498
|
+
async function loadVolumeChapters(volumeId) {
|
|
3499
|
+
if (!state.work || loadedVolumeChapterIds.has(volumeId)) return;
|
|
3500
|
+
const existingRequest = volumeChapterRequests.get(volumeId);
|
|
3501
|
+
if (existingRequest) return existingRequest;
|
|
3502
|
+
const workId = state.work.id;
|
|
3503
|
+
const generation = workScopedUiGeneration;
|
|
3504
|
+
volumeChapterLoadingIds.add(volumeId);
|
|
3505
|
+
renderTree();
|
|
3506
|
+
const request = (async () => {
|
|
3507
|
+
try {
|
|
3508
|
+
const chapters = await apiAllPages(`/api/volumes/${encodeURIComponent(volumeId)}/chapters`, 100);
|
|
3509
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
3510
|
+
const volume = state.work.volumes.find((item) => item.id === volumeId);
|
|
3511
|
+
if (!volume) return;
|
|
3512
|
+
volume.chapters = chapters;
|
|
3513
|
+
volume.chapterCount = chapters.length;
|
|
3514
|
+
loadedVolumeChapterIds.add(volumeId);
|
|
3515
|
+
renderTree();
|
|
3516
|
+
} catch (error) {
|
|
3517
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration) {
|
|
3518
|
+
toast(`加载分卷章节失败:${error.message}`, "error");
|
|
3519
|
+
renderTree();
|
|
3520
|
+
}
|
|
3521
|
+
} finally {
|
|
3522
|
+
volumeChapterLoadingIds.delete(volumeId);
|
|
3523
|
+
volumeChapterRequests.delete(volumeId);
|
|
3524
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration) renderTree();
|
|
3525
|
+
}
|
|
3526
|
+
})();
|
|
3527
|
+
volumeChapterRequests.set(volumeId, request);
|
|
3528
|
+
return request;
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
async function loadAllVolumeChapters(workId) {
|
|
3532
|
+
const volumeIds = state.work?.id === workId ? state.work.volumes.map((volume) => volume.id) : [];
|
|
3533
|
+
for (const volumeId of volumeIds) {
|
|
3534
|
+
if (state.work?.id !== workId) return;
|
|
3535
|
+
await loadVolumeChapters(volumeId);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
function mergeChapterDirectoryEntry(chapter) {
|
|
3540
|
+
if (!state.work || !chapter?.volumeId) return;
|
|
3541
|
+
const volume = state.work.volumes.find((item) => item.id === chapter.volumeId);
|
|
3542
|
+
if (!volume) return;
|
|
3543
|
+
const directoryEntry = {
|
|
3544
|
+
id: chapter.id,
|
|
3545
|
+
workId: chapter.workId,
|
|
3546
|
+
volumeId: chapter.volumeId,
|
|
3547
|
+
title: chapter.title,
|
|
3548
|
+
chapterType: chapter.chapterType,
|
|
3549
|
+
sortOrder: chapter.sortOrder,
|
|
3550
|
+
wordCount: chapter.wordCount,
|
|
3551
|
+
versionNo: chapter.versionNo,
|
|
3552
|
+
analysisStatus: chapter.analysisStatus,
|
|
3553
|
+
excludedFromAnalysis: chapter.excludedFromAnalysis,
|
|
3554
|
+
createdAt: chapter.createdAt,
|
|
3555
|
+
updatedAt: chapter.updatedAt
|
|
3556
|
+
};
|
|
3557
|
+
const chapters = Array.isArray(volume.chapters) ? volume.chapters : [];
|
|
3558
|
+
const existingIndex = chapters.findIndex((item) => item.id === chapter.id);
|
|
3559
|
+
if (existingIndex >= 0) chapters[existingIndex] = directoryEntry;
|
|
3560
|
+
else chapters.push(directoryEntry);
|
|
3561
|
+
chapters.sort((left, right) => Number(left.sortOrder) - Number(right.sortOrder));
|
|
3562
|
+
volume.chapters = chapters;
|
|
3563
|
+
volume.chapterCount = Math.max(Number(volume.chapterCount ?? 0), chapters.length);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3406
3566
|
function renderTree() {
|
|
3407
3567
|
if (!state.work) return;
|
|
3408
|
-
const count = state.work.volumes.reduce((total, volume) => total + volume.chapters
|
|
3568
|
+
const count = state.work.volumes.reduce((total, volume) => total + Number(volume.chapterCount ?? volume.chapters?.length ?? 0), 0);
|
|
3409
3569
|
const proseEditable = canEditProse();
|
|
3410
3570
|
$("#chapter-count").textContent = `${count} 章`;
|
|
3411
3571
|
$("#novel-tree").classList.remove("empty-copy");
|
|
3412
|
-
$("#novel-tree").innerHTML = state.work.volumes.map((volume) =>
|
|
3413
|
-
|
|
3572
|
+
$("#novel-tree").innerHTML = state.work.volumes.map((volume) => {
|
|
3573
|
+
const collapsed = state.collapsedVolumeIds.has(volume.id);
|
|
3574
|
+
const chapters = Array.isArray(volume.chapters) ? volume.chapters : [];
|
|
3575
|
+
const chapterContent = collapsed
|
|
3576
|
+
? ""
|
|
3577
|
+
: volumeChapterLoadingIds.has(volume.id)
|
|
3578
|
+
? '<p class="entity-history-empty">正在加载章节……</p>'
|
|
3579
|
+
: !loadedVolumeChapterIds.has(volume.id)
|
|
3580
|
+
? '<p class="entity-history-empty">展开后加载章节。</p>'
|
|
3581
|
+
: chapters.length
|
|
3582
|
+
? chapters.map((chapter) => `
|
|
3583
|
+
<button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}" draggable="${proseEditable ? "true" : "false"}" title="${proseEditable ? "拖拽排序;Alt+方向键排序,Alt+Shift+方向键跨卷" : ""}">
|
|
3584
|
+
<span>${esc(chapter.title)}</span><span class="chapter-node-meta">${chapter.chapterType && chapter.chapterType !== "正文" ? `<em class="chapter-type-badge">${esc(chapter.chapterType)}</em>` : ""}<small>${Number(chapter.wordCount ?? 0).toLocaleString("zh-CN")}</small></span>
|
|
3585
|
+
</button>`).join("")
|
|
3586
|
+
: '<p class="entity-history-empty">本卷还没有章节。</p>';
|
|
3587
|
+
return `
|
|
3588
|
+
<div class="volume-node ${collapsed ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
|
|
3414
3589
|
<div class="volume-title">
|
|
3415
|
-
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${
|
|
3590
|
+
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${collapsed ? "false" : "true"}" title="左键展开或折叠;右键设置分卷;可将章节拖到这里追加"><span>${esc(volume.title)}</span><span>${Number(volume.chapterCount ?? chapters.length)} 章</span></button>
|
|
3416
3591
|
${proseEditable ? `<button class="add-button chapter-add-button" type="button" data-new-chapter-volume="${esc(volume.id)}" aria-label="在“${esc(volume.title)}”中新建章节" title="在“${esc(volume.title)}”中新建章节">+</button>` : ""}
|
|
3417
3592
|
</div>
|
|
3418
3593
|
<div class="volume-chapters">
|
|
3419
|
-
${
|
|
3420
|
-
<button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}" draggable="${proseEditable ? "true" : "false"}" title="${proseEditable ? "拖拽排序;Alt+方向键排序,Alt+Shift+方向键跨卷" : ""}">
|
|
3421
|
-
<span>${esc(chapter.title)}</span><span class="chapter-node-meta">${chapter.chapterType && chapter.chapterType !== "正文" ? `<em class="chapter-type-badge">${esc(chapter.chapterType)}</em>` : ""}<small>${Number(chapter.wordCount ?? 0).toLocaleString("zh-CN")}</small></span>
|
|
3422
|
-
</button>`).join("")}
|
|
3594
|
+
${chapterContent}
|
|
3423
3595
|
</div>
|
|
3424
|
-
</div
|
|
3596
|
+
</div>`;
|
|
3597
|
+
}).join("");
|
|
3425
3598
|
$("#novel-tree").querySelectorAll("[data-volume-toggle]").forEach((button) => {
|
|
3426
3599
|
button.addEventListener("click", () => {
|
|
3427
3600
|
const volumeId = button.dataset.volumeToggle;
|
|
3428
|
-
if (state.collapsedVolumeIds.has(volumeId))
|
|
3429
|
-
|
|
3601
|
+
if (state.collapsedVolumeIds.has(volumeId)) {
|
|
3602
|
+
state.collapsedVolumeIds.delete(volumeId);
|
|
3603
|
+
void loadVolumeChapters(volumeId);
|
|
3604
|
+
} else state.collapsedVolumeIds.add(volumeId);
|
|
3430
3605
|
renderTree();
|
|
3431
3606
|
});
|
|
3432
3607
|
button.addEventListener("contextmenu", (event) => {
|
|
@@ -3446,7 +3621,7 @@ function renderTree() {
|
|
|
3446
3621
|
button.closest(".volume-node")?.classList.remove("is-drag-target");
|
|
3447
3622
|
const chapterId = event.dataTransfer?.getData("text/plain");
|
|
3448
3623
|
const volume = state.work?.volumes.find((item) => item.id === button.dataset.volumeToggle);
|
|
3449
|
-
if (chapterId && volume) await moveChapterInTree(chapterId, volume.id, volume.chapters.length);
|
|
3624
|
+
if (chapterId && volume) await moveChapterInTree(chapterId, volume.id, Number(volume.chapterCount ?? volume.chapters.length));
|
|
3450
3625
|
});
|
|
3451
3626
|
}
|
|
3452
3627
|
});
|
|
@@ -3715,7 +3890,11 @@ async function deleteChapter(chapterId) {
|
|
|
3715
3890
|
async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
3716
3891
|
if (state.chapter?.id !== chapterId && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) return;
|
|
3717
3892
|
cancelChapterAutoSave();
|
|
3718
|
-
state.chapter
|
|
3893
|
+
if (state.chapter?.id !== chapterId) {
|
|
3894
|
+
state.chapter = await api(`/api/chapters/${chapterId}`);
|
|
3895
|
+
mergeChapterDirectoryEntry(state.chapter);
|
|
3896
|
+
}
|
|
3897
|
+
state.collapsedVolumeIds.delete(state.chapter.volumeId);
|
|
3719
3898
|
lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
|
|
3720
3899
|
chapterEditorReadOnly = !canEditProse() || !editMode;
|
|
3721
3900
|
state.module = "editor";
|
|
@@ -4152,6 +4331,15 @@ function mountRelationshipFilterToggle() {
|
|
|
4152
4331
|
});
|
|
4153
4332
|
}
|
|
4154
4333
|
|
|
4334
|
+
function mountDraftFilterToggle() {
|
|
4335
|
+
$("#module-header-actions").querySelector('[data-module-header-action="draft-filter-toggle"]')?.remove();
|
|
4336
|
+
$("#module-header-actions").insertAdjacentHTML("afterbegin", `<button type="button" class="module-filter-toggle" data-module-header-action="draft-filter-toggle" aria-label="筛选草稿" aria-controls="draft-filter-panel" aria-expanded="${draftFiltersPanelOpen}" 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>`);
|
|
4337
|
+
$("#module-header-actions").querySelector('[data-module-header-action="draft-filter-toggle"]')?.addEventListener("click", async () => {
|
|
4338
|
+
draftFiltersPanelOpen = !draftFiltersPanelOpen;
|
|
4339
|
+
await renderDrafts(moduleListPages.drafts);
|
|
4340
|
+
});
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4155
4343
|
function bindRecordPreview(selector, open) {
|
|
4156
4344
|
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
4157
4345
|
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization ?? card.dataset.openReview;
|
|
@@ -4246,28 +4434,42 @@ function draftTypeLabel(draftType) {
|
|
|
4246
4434
|
|
|
4247
4435
|
async function deleteDraft(item) {
|
|
4248
4436
|
if (!item || !canEditModule("drafts")) return;
|
|
4437
|
+
const dialog = $("#form-dialog");
|
|
4438
|
+
dialog.close();
|
|
4249
4439
|
if (!await confirmToast(`确认删除草稿“${item.title}”吗?草稿将从当前列表移除。`, {
|
|
4250
4440
|
title: "删除草稿",
|
|
4251
4441
|
confirmLabel: "确认删除"
|
|
4252
|
-
}))
|
|
4442
|
+
})) {
|
|
4443
|
+
openDraftDialog(item);
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4253
4446
|
try {
|
|
4254
4447
|
await api(`/api/drafts/${encodeURIComponent(item.id)}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
4255
4448
|
await renderDrafts(moduleListPages.drafts);
|
|
4256
4449
|
toast("草稿已删除");
|
|
4257
4450
|
} catch (error) {
|
|
4258
4451
|
toast(error.message, "error");
|
|
4452
|
+
try {
|
|
4453
|
+
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(item.id)}`));
|
|
4454
|
+
} catch (reloadError) {
|
|
4455
|
+
toast(reloadError.message, "error");
|
|
4456
|
+
}
|
|
4259
4457
|
}
|
|
4260
4458
|
}
|
|
4261
4459
|
|
|
4262
4460
|
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
4263
4461
|
const viewOnly = readOnly || !canEditModule("drafts");
|
|
4462
|
+
const management = item && !viewOnly ? `<section class="entity-dialog-management" aria-label="草稿操作">
|
|
4463
|
+
<div><strong>草稿操作</strong><small>删除后将从草稿列表移除,版本历史仍会保留。</small></div>
|
|
4464
|
+
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-draft-delete>删除草稿</button></div>
|
|
4465
|
+
</section>` : "";
|
|
4264
4466
|
const fields = `<p class="form-field-note">草稿只记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。</p>`
|
|
4265
4467
|
+ field("draftType", "草稿类型", "select", item?.draftType ?? "prose", [["prose", "正文草稿"], ["setting", "设定草稿"]])
|
|
4266
4468
|
+ field("title", "标题", "text", item?.title ?? "")
|
|
4267
4469
|
+ field("content", "内容", "markdown", item?.content ?? "", {
|
|
4268
4470
|
placeholder: "记录尚未定稿的片段、方向或设定想法……",
|
|
4269
4471
|
readOnly: viewOnly
|
|
4270
|
-
});
|
|
4472
|
+
}) + management;
|
|
4271
4473
|
openDialog(item ? viewOnly ? "查看草稿" : "编辑草稿" : "新建草稿", fields, async (form) => {
|
|
4272
4474
|
if (viewOnly) return;
|
|
4273
4475
|
const title = String(form.get("title") ?? "").trim();
|
|
@@ -4287,7 +4489,7 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
4287
4489
|
}, item ? draftTypeLabel(item.draftType) : "未确认想法", {
|
|
4288
4490
|
submitLabel: viewOnly ? "关闭" : "保存草稿",
|
|
4289
4491
|
hideCancel: viewOnly,
|
|
4290
|
-
|
|
4492
|
+
editor: true,
|
|
4291
4493
|
errorPrefix: "草稿保存失败:"
|
|
4292
4494
|
});
|
|
4293
4495
|
if (viewOnly) {
|
|
@@ -4296,10 +4498,13 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
4296
4498
|
else control.readOnly = true;
|
|
4297
4499
|
});
|
|
4298
4500
|
}
|
|
4501
|
+
$("#dialog-fields").querySelector("[data-dialog-draft-delete]")?.addEventListener("click", () => {
|
|
4502
|
+
void deleteDraft(item);
|
|
4503
|
+
});
|
|
4299
4504
|
}
|
|
4300
4505
|
|
|
4301
4506
|
async function renderDrafts(page = moduleListPages.drafts) {
|
|
4302
|
-
const allDrafts = await
|
|
4507
|
+
const allDrafts = await moduleApiAllPages("drafts", `/api/works/${state.work.id}/drafts`);
|
|
4303
4508
|
const drafts = draftTypeFilter === "all"
|
|
4304
4509
|
? allDrafts
|
|
4305
4510
|
: allDrafts.filter((draft) => draft.draftType === draftTypeFilter);
|
|
@@ -4309,7 +4514,8 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
4309
4514
|
const layout = readModuleLayout();
|
|
4310
4515
|
if (drafts.length) mountModuleLayoutToggle(layout, "草稿列表样式");
|
|
4311
4516
|
else $("#module-header-actions").querySelector('[data-module-header-action="layout-toggle"]')?.remove();
|
|
4312
|
-
|
|
4517
|
+
mountDraftFilterToggle();
|
|
4518
|
+
const filterToolbar = `<section id="draft-filter-panel" class="draft-filter-toolbar${draftFiltersPanelOpen ? "" : " hidden"}" aria-label="草稿筛选">
|
|
4313
4519
|
<label for="draft-type-filter">草稿类型</label>
|
|
4314
4520
|
<select id="draft-type-filter" aria-label="按草稿类型筛选">
|
|
4315
4521
|
<option value="all" ${draftTypeFilter === "all" ? "selected" : ""}>全部草稿</option>
|
|
@@ -4319,7 +4525,7 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
4319
4525
|
${draftTypeFilter === "all" ? "" : `<span aria-live="polite">筛选后剩余 ${drafts.length} 篇草稿</span>`}
|
|
4320
4526
|
</section>`;
|
|
4321
4527
|
const actions = (item) => canEditModule("drafts")
|
|
4322
|
-
? `${recordCardEditButton("edit-draft", item.id, `草稿“${item.title}”`)}
|
|
4528
|
+
? `${recordCardEditButton("edit-draft", item.id, `草稿“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
|
|
4323
4529
|
: recordHistoryButton("draft", item.id, item.title);
|
|
4324
4530
|
const cards = `<div class="card-grid">${pageResult.items.map((item) => `
|
|
4325
4531
|
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看草稿 ${esc(item.title)}">
|
|
@@ -4344,12 +4550,12 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
4344
4550
|
: emptyDrafts);
|
|
4345
4551
|
$("#draft-type-filter").addEventListener("change", async (event) => {
|
|
4346
4552
|
draftTypeFilter = ["prose", "setting"].includes(event.currentTarget.value) ? event.currentTarget.value : "all";
|
|
4553
|
+
draftFiltersPanelOpen = true;
|
|
4347
4554
|
moduleListPages.drafts = 1;
|
|
4348
4555
|
await renderDrafts(1);
|
|
4349
4556
|
});
|
|
4350
4557
|
bindModuleLayoutToggle(() => renderDrafts(pageResult.page));
|
|
4351
4558
|
bindModulePagination("drafts", renderDrafts);
|
|
4352
|
-
const draftById = (draftId) => drafts.find((draft) => draft.id === draftId);
|
|
4353
4559
|
$("#module-content").querySelectorAll("[data-open-draft]").forEach((card) => {
|
|
4354
4560
|
const open = async () => openDraftDialog(await api(`/api/drafts/${encodeURIComponent(card.dataset.openDraft)}`), { readOnly: true });
|
|
4355
4561
|
card.addEventListener("click", (event) => { if (!event.target.closest("button, a")) void open(); });
|
|
@@ -4363,9 +4569,6 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
4363
4569
|
$("#module-content").querySelectorAll("[data-edit-draft]").forEach((button) => button.addEventListener("click", async () => {
|
|
4364
4570
|
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(button.dataset.editDraft)}`));
|
|
4365
4571
|
}));
|
|
4366
|
-
$("#module-content").querySelectorAll("[data-delete-draft]").forEach((button) => button.addEventListener("click", () => {
|
|
4367
|
-
void deleteDraft(draftById(button.dataset.deleteDraft));
|
|
4368
|
-
}));
|
|
4369
4572
|
bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
|
|
4370
4573
|
}
|
|
4371
4574
|
|
|
@@ -4387,7 +4590,7 @@ function renderSettingRows(records) {
|
|
|
4387
4590
|
}
|
|
4388
4591
|
|
|
4389
4592
|
async function renderSettings(page = moduleListPages.settings) {
|
|
4390
|
-
const records = await
|
|
4593
|
+
const records = await moduleApiAllPages("settings", `/api/works/${state.work.id}/settings`);
|
|
4391
4594
|
state.settings = records;
|
|
4392
4595
|
mountModuleCount(records.length);
|
|
4393
4596
|
const pageResult = paginateModuleItems(records, page, "settings");
|
|
@@ -4408,9 +4611,11 @@ async function renderCharacters(page = characterListPage) {
|
|
|
4408
4611
|
const hasCharacterFilters = characterFilters.raceIds.length > 0 || characterFilters.organizationIds.length > 0;
|
|
4409
4612
|
const pageSize = pageSizeFor("characters");
|
|
4410
4613
|
const [characterSource, races, organizations] = await Promise.all([
|
|
4411
|
-
hasCharacterFilters
|
|
4412
|
-
|
|
4413
|
-
|
|
4614
|
+
hasCharacterFilters
|
|
4615
|
+
? moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`)
|
|
4616
|
+
: moduleApiPage("characters", `/api/works/${state.work.id}/characters`, page, pageSize),
|
|
4617
|
+
canReadModule("races") ? moduleApi("characters", `/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
4618
|
+
canReadModule("organizations") ? moduleApiAllPages("characters", `/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
4414
4619
|
]);
|
|
4415
4620
|
const characterPage = hasCharacterFilters
|
|
4416
4621
|
? paginateCharacters(filterCharacters(characterSource, characterFilters), page, pageSize)
|
|
@@ -4586,7 +4791,7 @@ async function renderRaces() {
|
|
|
4586
4791
|
const workId = state.work.id;
|
|
4587
4792
|
const generation = workScopedUiGeneration;
|
|
4588
4793
|
const requestId = ++raceListRequestId;
|
|
4589
|
-
const roots = await
|
|
4794
|
+
const roots = await moduleApi("races", `/api/works/${workId}/races?scope=roots`);
|
|
4590
4795
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
|
|
4591
4796
|
state.races = roots.items;
|
|
4592
4797
|
loadedRaceHierarchyWorkId = roots.items.length === roots.total ? workId : null;
|
|
@@ -4594,7 +4799,7 @@ async function renderRaces() {
|
|
|
4594
4799
|
if (loadedRaceHierarchyWorkId === workId) return;
|
|
4595
4800
|
|
|
4596
4801
|
const dismissLoadingToast = persistentToast("正在加载子种族……");
|
|
4597
|
-
const loadPromise =
|
|
4802
|
+
const loadPromise = moduleApi("races", `/api/works/${workId}/races?scope=descendants`).then((descendants) => {
|
|
4598
4803
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
|
|
4599
4804
|
state.races = [...roots.items, ...descendants];
|
|
4600
4805
|
loadedRaceHierarchyWorkId = workId;
|
|
@@ -4618,8 +4823,8 @@ async function renderRaces() {
|
|
|
4618
4823
|
|
|
4619
4824
|
async function renderOrganizations(page = moduleListPages.organizations) {
|
|
4620
4825
|
[state.organizations, state.characters] = await Promise.all([
|
|
4621
|
-
|
|
4622
|
-
canReadModule("characters") ?
|
|
4826
|
+
moduleApiAllPages("organizations", `/api/works/${state.work.id}/organizations`),
|
|
4827
|
+
canReadModule("characters") ? moduleApiAllPages("organizations", `/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
4623
4828
|
]);
|
|
4624
4829
|
mountModuleCount(state.organizations.length);
|
|
4625
4830
|
const pageResult = paginateModuleItems(state.organizations, page, "organizations");
|
|
@@ -4685,8 +4890,8 @@ function setTimelineMultiSelectMode(enabled) {
|
|
|
4685
4890
|
|
|
4686
4891
|
async function renderTimeline(page = moduleListPages.timeline) {
|
|
4687
4892
|
const [events, tracks] = await Promise.all([
|
|
4688
|
-
|
|
4689
|
-
|
|
4893
|
+
moduleApiAllPages("timeline", `/api/works/${state.work.id}/timeline`),
|
|
4894
|
+
moduleApiAllPages("timeline", `/api/works/${state.work.id}/timeline-tracks`)
|
|
4690
4895
|
]);
|
|
4691
4896
|
mountModuleCount(events.length);
|
|
4692
4897
|
const pageResult = paginateModuleItems(events, page, "timeline");
|
|
@@ -4729,8 +4934,8 @@ async function renderTimeline(page = moduleListPages.timeline) {
|
|
|
4729
4934
|
async function renderOutlines(outlinePage = moduleListPages.outlinePlans, foreshadowPage = moduleListPages.foreshadows) {
|
|
4730
4935
|
const currentChapterId = state.chapter?.id;
|
|
4731
4936
|
const [outlines, foreshadows] = await Promise.all([
|
|
4732
|
-
|
|
4733
|
-
|
|
4937
|
+
moduleApiAllPages("outlines", `/api/works/${state.work.id}/outlines`),
|
|
4938
|
+
moduleApiAllPages("outlines", `/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `¤tChapterId=${encodeURIComponent(currentChapterId)}` : ""}`)
|
|
4734
4939
|
]);
|
|
4735
4940
|
mountModuleCount(outlines.length + foreshadows.length);
|
|
4736
4941
|
const outlinePageResult = paginateModuleItems(outlines, outlinePage, "outlines");
|
|
@@ -4783,8 +4988,8 @@ async function renderOutlines(outlinePage = moduleListPages.outlinePlans, foresh
|
|
|
4783
4988
|
}
|
|
4784
4989
|
|
|
4785
4990
|
async function renderRelationships(page = moduleListPages.relationships) {
|
|
4786
|
-
state.characters = canReadModule("characters") ? await
|
|
4787
|
-
const relationships = await
|
|
4991
|
+
state.characters = canReadModule("characters") ? await moduleApiAllPages("relationships", `/api/works/${state.work.id}/characters`) : [];
|
|
4992
|
+
const relationships = await moduleApiAllPages("relationships", `/api/works/${state.work.id}/relationships`);
|
|
4788
4993
|
const filteredRelationships = filterRelationships(relationships, relationshipFilters);
|
|
4789
4994
|
const pageResult = paginateModuleItems(filteredRelationships, page, "relationships");
|
|
4790
4995
|
moduleListPages.relationships = pageResult.page;
|
|
@@ -4858,8 +5063,8 @@ async function renderReviews(page = moduleListPages.reviews) {
|
|
|
4858
5063
|
const canMergeCharacters = canResolveReview
|
|
4859
5064
|
&& ["characters", "races", "organizations", "timeline", "relationships"].every((module) => canEditModule(module));
|
|
4860
5065
|
const [reviews, characters] = await Promise.all([
|
|
4861
|
-
|
|
4862
|
-
canReadCharacters ?
|
|
5066
|
+
moduleApiAllPages("reviews", `/api/works/${state.work.id}/reviews`),
|
|
5067
|
+
canReadCharacters ? moduleApiAllPages("reviews", `/api/works/${state.work.id}/characters?includeMerged=1`) : Promise.resolve([])
|
|
4863
5068
|
]);
|
|
4864
5069
|
mountModuleCount(reviews.length);
|
|
4865
5070
|
const pageResult = paginateModuleItems(reviews, page, "reviews");
|
|
@@ -4947,16 +5152,16 @@ async function renderReviews(page = moduleListPages.reviews) {
|
|
|
4947
5152
|
}));
|
|
4948
5153
|
}
|
|
4949
5154
|
|
|
4950
|
-
async function renderTasks(page = taskListPage) {
|
|
5155
|
+
async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
4951
5156
|
stopTaskProgressRefresh();
|
|
4952
5157
|
const pageSize = pageSizeFor("analysisTasks");
|
|
4953
5158
|
const [taskPage, settings] = await Promise.all([
|
|
4954
|
-
|
|
5159
|
+
moduleApiPage("tasks", `/api/works/${state.work.id}/tasks`, page, pageSize, { refresh }),
|
|
4955
5160
|
canReadModule("ai-settings")
|
|
4956
|
-
?
|
|
5161
|
+
? moduleApi("tasks", `/api/works/${state.work.id}/ai-settings`, { refresh })
|
|
4957
5162
|
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunDailyTaskLimit: 0, autoRunFailureThreshold: 3, autoRunPaused: false })
|
|
4958
5163
|
]);
|
|
4959
|
-
if (!taskPage.items.length && page > 1) return renderTasks(page - 1);
|
|
5164
|
+
if (!taskPage.items.length && page > 1) return renderTasks(page - 1, { refresh });
|
|
4960
5165
|
taskListPage = taskPage.page;
|
|
4961
5166
|
const tasks = taskPage.items;
|
|
4962
5167
|
const taskTotal = Number(taskPage.total ?? taskPage.stats?.total ?? tasks.length);
|
|
@@ -5067,7 +5272,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
5067
5272
|
: "自动执行已关闭");
|
|
5068
5273
|
taskAutoRunEditing = false;
|
|
5069
5274
|
taskAutoRunEditingWorkId = null;
|
|
5070
|
-
await renderTasks();
|
|
5275
|
+
await renderTasks(taskListPage, { refresh: true });
|
|
5071
5276
|
} catch (error) {
|
|
5072
5277
|
toast(error.message, "error");
|
|
5073
5278
|
button.disabled = false;
|
|
@@ -5226,7 +5431,7 @@ function scheduleTaskProgressRefresh(workId, runningCount) {
|
|
|
5226
5431
|
return;
|
|
5227
5432
|
}
|
|
5228
5433
|
try {
|
|
5229
|
-
await renderTasks();
|
|
5434
|
+
await renderTasks(taskListPage, { refresh: true });
|
|
5230
5435
|
} catch (error) {
|
|
5231
5436
|
console.error("Failed to refresh task progress", error);
|
|
5232
5437
|
scheduleTaskProgressRefresh(workId, runningCount);
|
|
@@ -6106,12 +6311,12 @@ async function renderBookAiSettings() {
|
|
|
6106
6311
|
relationshipSearchIndexRefreshTimer = null;
|
|
6107
6312
|
}
|
|
6108
6313
|
const [settings, providers, models, taskDefaults, relationshipIndex, usage] = await Promise.all([
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6314
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings`),
|
|
6315
|
+
moduleApi("ai-settings", "/api/platform/ai/providers"),
|
|
6316
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/models`),
|
|
6317
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/task-defaults`),
|
|
6318
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/relationship-search-index`),
|
|
6319
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/usage?timezoneOffset=${-new Date().getTimezoneOffset()}`)
|
|
6115
6320
|
]);
|
|
6116
6321
|
const host = $("#module-content");
|
|
6117
6322
|
const workId = String(state.work.id);
|
|
@@ -6641,6 +6846,7 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
6641
6846
|
dialog.classList.toggle("wide-dialog", Boolean(options.wide));
|
|
6642
6847
|
dialog.classList.toggle("trace-dialog", Boolean(options.trace));
|
|
6643
6848
|
dialog.classList.toggle("large-dialog", Boolean(options.large));
|
|
6849
|
+
dialog.classList.toggle("editor-dialog", Boolean(options.editor));
|
|
6644
6850
|
bindDynamicListControls($("#dialog-fields"));
|
|
6645
6851
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
6646
6852
|
formDialogVditors = bindVditorEditors($("#dialog-fields"));
|
|
@@ -8758,23 +8964,30 @@ async function sendAi() {
|
|
|
8758
8964
|
let assistantContent = "";
|
|
8759
8965
|
let assistantMessage;
|
|
8760
8966
|
let assistantMetadata = {};
|
|
8967
|
+
let persistedStreamMessage = null;
|
|
8761
8968
|
let suggestion = null;
|
|
8762
8969
|
if (taskType === "chat") {
|
|
8763
8970
|
const streamed = await streamChat({ instruction, scope, modelId, citations, conversationId: state.aiConversationId, currentMessageId: persistedUserMessage.id });
|
|
8764
8971
|
assistantContent = streamed.content;
|
|
8765
8972
|
assistantMessage = streamed.message;
|
|
8766
8973
|
assistantMetadata = streamed.metadata;
|
|
8974
|
+
persistedStreamMessage = streamed.messageId ? { id: streamed.messageId, createdAt: streamed.createdAt } : null;
|
|
8767
8975
|
} else {
|
|
8768
8976
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
8769
8977
|
assistantContent = suggestion.content;
|
|
8770
8978
|
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
|
|
8771
8979
|
}
|
|
8772
8980
|
try {
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8981
|
+
if (persistedStreamMessage) {
|
|
8982
|
+
updateMessageCreatedAt(assistantMessage, persistedStreamMessage.createdAt);
|
|
8983
|
+
attachMessageIdentity(assistantMessage, persistedStreamMessage.id);
|
|
8984
|
+
} else {
|
|
8985
|
+
const persistedAssistantMessage = await persistAiConversationMessage("assistant", assistantContent, [], assistantMetadata);
|
|
8986
|
+
if (assistantMessage) {
|
|
8987
|
+
updateMessageCreatedAt(assistantMessage, persistedAssistantMessage.createdAt);
|
|
8988
|
+
attachMessageIdentity(assistantMessage, persistedAssistantMessage.id);
|
|
8989
|
+
} else if (suggestion) appendSuggestion(suggestion, persistedAssistantMessage.createdAt, persistedAssistantMessage.id);
|
|
8990
|
+
}
|
|
8778
8991
|
} catch (error) {
|
|
8779
8992
|
if (suggestion) appendSuggestion(suggestion);
|
|
8780
8993
|
toast(`AI 回复已生成,但历史记录保存失败:${error.message}`, "error");
|
|
@@ -8814,6 +9027,8 @@ async function streamChat(body) {
|
|
|
8814
9027
|
let generatedMetadata = {};
|
|
8815
9028
|
let toolCalls = [];
|
|
8816
9029
|
let processSteps = [];
|
|
9030
|
+
let persistedMessageId = null;
|
|
9031
|
+
let persistedMessageCreatedAt = null;
|
|
8817
9032
|
let finalAnswerStarted = false;
|
|
8818
9033
|
const processStartedAt = Date.now();
|
|
8819
9034
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
@@ -8868,6 +9083,8 @@ async function streamChat(body) {
|
|
|
8868
9083
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
8869
9084
|
scrollAiFeedToBottom();
|
|
8870
9085
|
} else if (eventName === "complete") {
|
|
9086
|
+
persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
|
|
9087
|
+
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
8871
9088
|
await typewriter.finish();
|
|
8872
9089
|
message.classList.remove("is-streaming");
|
|
8873
9090
|
content.setAttribute("aria-busy", "false");
|
|
@@ -8895,7 +9112,7 @@ async function streamChat(body) {
|
|
|
8895
9112
|
if (buffer.trim()) await consume(buffer);
|
|
8896
9113
|
await typewriter.finish();
|
|
8897
9114
|
if (streamError) throw streamError;
|
|
8898
|
-
return { content: streamedText, message, metadata: generatedMetadata };
|
|
9115
|
+
return { content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt };
|
|
8899
9116
|
} catch (error) {
|
|
8900
9117
|
typewriter.reveal();
|
|
8901
9118
|
message.classList.remove("is-streaming");
|