@musnows/scriverse 0.6.2 → 0.6.3
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-tool-results.js +71 -0
- package/dist/ai-tool-results.js.map +1 -1
- package/dist/ai.js +163 -38
- package/dist/ai.js.map +1 -1
- package/dist/app.js +155 -36
- package/dist/app.js.map +1 -1
- package/dist/cli-core.js +23 -13
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +123 -1
- package/dist/database.js.map +1 -1
- package/dist/docx-export.js +89 -0
- package/dist/docx-export.js.map +1 -0
- package/dist/image-captcha.js +8 -5
- package/dist/image-captcha.js.map +1 -1
- package/dist/public/ai-context-meter.js +4 -0
- package/dist/public/ai-message-time.js +3 -9
- package/dist/public/ai-tool-call.js +4 -0
- package/dist/public/app.js +659 -89
- package/dist/public/index.html +47 -16
- package/dist/public/page-route.js +2 -2
- package/dist/public/styles.css +77 -19
- package/dist/public/system-status.d.ts +12 -0
- package/dist/public/system-status.js +16 -0
- package/dist/public/theme-init.js +2 -2
- package/dist/security.js +101 -9
- package/dist/security.js.map +1 -1
- package/dist/store.js +111 -20
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +41 -22
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/writing-progress-time.js +15 -0
- package/dist/writing-progress-time.js.map +1 -1
- package/package.json +2 -1
package/dist/public/app.js
CHANGED
|
@@ -10,8 +10,9 @@ import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-s
|
|
|
10
10
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
11
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
|
-
import { formatAiMessageTime } from "/ai-message-time.js?v=
|
|
14
|
-
import { formatAiContextUsagePercent, formatAiContextUsageTooltip, normalizeAiContextTokenDistribution } from "/ai-context-meter.js?v=
|
|
13
|
+
import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
|
|
14
|
+
import { formatAiContextUsagePercent, formatAiContextUsageTooltip, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260801-retain-usage-v1";
|
|
15
|
+
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
15
16
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
16
17
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
17
18
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
@@ -49,6 +50,7 @@ import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20
|
|
|
49
50
|
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
50
51
|
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
|
|
51
52
|
import { createModuleRequestCache } from "/module-request-cache.js?v=20260730-module-request-cache-v1";
|
|
53
|
+
import { systemStatusPresentation } from "/system-status.js?v=20260801-system-health-v1";
|
|
52
54
|
import {
|
|
53
55
|
clampCropRect,
|
|
54
56
|
containImageRect,
|
|
@@ -152,6 +154,7 @@ function createPresenceClientId() {
|
|
|
152
154
|
|
|
153
155
|
const presenceClientId = createPresenceClientId();
|
|
154
156
|
const presenceHeartbeatInterval = 12_000;
|
|
157
|
+
const systemBootCheckInterval = 8_000;
|
|
155
158
|
let presenceParticipants = [];
|
|
156
159
|
let presenceHeartbeatTimer = null;
|
|
157
160
|
let presenceHeartbeatQueued = null;
|
|
@@ -160,6 +163,10 @@ const acknowledgedCollaborativeChangeIds = new Set();
|
|
|
160
163
|
let collaborativeChangePromptOpen = false;
|
|
161
164
|
let relationshipPresenceId = null;
|
|
162
165
|
let collaborationAutoSaveDisabled = false;
|
|
166
|
+
let systemBootId = null;
|
|
167
|
+
let systemBootCheckTimer = null;
|
|
168
|
+
let systemBootCheckPromise = null;
|
|
169
|
+
let systemRestartDetected = false;
|
|
163
170
|
let chapterAnnotations = [];
|
|
164
171
|
let workAuditRecords = [];
|
|
165
172
|
let workAuditNextPage = null;
|
|
@@ -177,6 +184,10 @@ let backgroundTaskCenterWorkId = null;
|
|
|
177
184
|
let backgroundTaskCenterTasksInitialized = false;
|
|
178
185
|
let backgroundTaskCenterTaskSnapshots = new Map();
|
|
179
186
|
let backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
187
|
+
const systemHealthPollInterval = 30_000;
|
|
188
|
+
let systemHealthTimer = null;
|
|
189
|
+
let systemHealthSnapshot = { status: "checking", version: "" };
|
|
190
|
+
let topbarStatusSource = "view";
|
|
180
191
|
const taskProgressRefreshInterval = 2_500;
|
|
181
192
|
const taskStatusSnapshots = new Map();
|
|
182
193
|
|
|
@@ -338,6 +349,15 @@ const $ = (selector) => document.querySelector(selector);
|
|
|
338
349
|
const esc = (value) => String(value ?? "").replace(/[&<>'"]/g, (character) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ })[character]);
|
|
339
350
|
const maximumAvatarFileSize = 5 * 1024 * 1024;
|
|
340
351
|
|
|
352
|
+
function setAiAssistantStatus(status) {
|
|
353
|
+
const failed = status === "error";
|
|
354
|
+
const label = failed ? "创作助手状态:执行失败" : "创作助手状态:正常";
|
|
355
|
+
const dot = $("#ai-status-dot");
|
|
356
|
+
dot.classList.toggle("is-error", failed);
|
|
357
|
+
dot.setAttribute("aria-label", label);
|
|
358
|
+
dot.title = label;
|
|
359
|
+
}
|
|
360
|
+
|
|
341
361
|
function userAvatarInitial(user) {
|
|
342
362
|
return Array.from(String(user?.displayName || user?.username || "作"))[0] ?? "作";
|
|
343
363
|
}
|
|
@@ -395,6 +415,7 @@ let aiReferencesLoadPromise = null;
|
|
|
395
415
|
let aiReferencesLoadWorkId = null;
|
|
396
416
|
let aiConversationsLoadPromise = null;
|
|
397
417
|
let aiConversationsLoadWorkId = null;
|
|
418
|
+
let latestAiContextUsage = null;
|
|
398
419
|
const aiConversationHistoryPageLimit = 20;
|
|
399
420
|
let aiConversationHistoryPage = { page: 1, limit: aiConversationHistoryPageLimit, hasMore: false, nextPage: null };
|
|
400
421
|
let workScopedUiGeneration = 0;
|
|
@@ -614,7 +635,7 @@ function presencePageForRoute(route = currentPageRoute()) {
|
|
|
614
635
|
if (route.view === "editor") return { kind: "editor", resourceId: String(route.chapterId ?? "") || undefined };
|
|
615
636
|
if (route.view === "entity-editor") return { kind: "entity-editor", module: route.entity, resourceId: String(route.entityId ?? "") || undefined };
|
|
616
637
|
if (route.view === "module") return { kind: "module", module: route.module };
|
|
617
|
-
if (route.view === "settings" || route.view === "platform-ai" || route.view === "platform-usage") return { kind: "settings" };
|
|
638
|
+
if (route.view === "settings" || route.view === "platform-ai" || route.view === "platform-usage" || route.view === "work-audit") return { kind: "settings" };
|
|
618
639
|
return { kind: "welcome" };
|
|
619
640
|
}
|
|
620
641
|
|
|
@@ -780,6 +801,7 @@ function currentPageRoute() {
|
|
|
780
801
|
if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
|
|
781
802
|
if (!$("#platform-ai-view").classList.contains("hidden")) return { view: "platform-ai", workId, ...settingsRouteContext() };
|
|
782
803
|
if (!$("#platform-usage-view").classList.contains("hidden")) return { view: "platform-usage", workId, ...settingsRouteContext() };
|
|
804
|
+
if (!$("#work-audit-view").classList.contains("hidden")) return { view: "work-audit", workId, ...settingsRouteContext() };
|
|
783
805
|
if (!$("#shelf-view").classList.contains("hidden")) return { view: "shelf" };
|
|
784
806
|
if (!workId) return { view: "shelf" };
|
|
785
807
|
if (!$("#editor-view").classList.contains("hidden")) return { view: "editor", workId, chapterId: state.chapter?.id ?? null };
|
|
@@ -1314,7 +1336,7 @@ function attachMessageHeading(message, label, createdAt = new Date().toISOString
|
|
|
1314
1336
|
role.textContent = label;
|
|
1315
1337
|
const time = document.createElement("time");
|
|
1316
1338
|
time.dateTime = timestamp;
|
|
1317
|
-
time.textContent = formatAiMessageTime(timestamp
|
|
1339
|
+
time.textContent = formatAiMessageTime(timestamp);
|
|
1318
1340
|
heading.append(role, time);
|
|
1319
1341
|
message.prepend(heading);
|
|
1320
1342
|
message.dataset.createdAt = timestamp;
|
|
@@ -1328,7 +1350,7 @@ function updateMessageCreatedAt(message, createdAt) {
|
|
|
1328
1350
|
const time = message.querySelector(".message-heading time");
|
|
1329
1351
|
if (!time) return;
|
|
1330
1352
|
time.dateTime = createdAt;
|
|
1331
|
-
time.textContent = formatAiMessageTime(createdAt
|
|
1353
|
+
time.textContent = formatAiMessageTime(createdAt);
|
|
1332
1354
|
message.dataset.createdAt = createdAt;
|
|
1333
1355
|
if (message === $("#ai-feed").lastElementChild) state.aiLastMessageAt = createdAt;
|
|
1334
1356
|
}
|
|
@@ -1338,14 +1360,20 @@ function resetAiFeed() {
|
|
|
1338
1360
|
$("#ai-feed").innerHTML = '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
1339
1361
|
}
|
|
1340
1362
|
|
|
1341
|
-
function
|
|
1363
|
+
function createAiContextCompactionDivider({ kind = "conversation", ariaLabel = "已压缩上下文", title = "" } = {}) {
|
|
1342
1364
|
const divider = document.createElement("div");
|
|
1343
1365
|
divider.className = "ai-context-compaction-divider";
|
|
1344
1366
|
divider.dataset.contextCompaction = kind;
|
|
1345
1367
|
divider.dataset.testid = "ai-context-compaction-divider";
|
|
1346
1368
|
divider.setAttribute("role", "separator");
|
|
1347
|
-
divider.setAttribute("aria-label",
|
|
1369
|
+
divider.setAttribute("aria-label", ariaLabel);
|
|
1370
|
+
if (title) divider.title = title;
|
|
1348
1371
|
divider.innerHTML = "<span>已压缩上下文</span>";
|
|
1372
|
+
return divider;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function appendAiContextCompactionDivider(kind, before = null) {
|
|
1376
|
+
const divider = createAiContextCompactionDivider({ kind });
|
|
1349
1377
|
const feed = $("#ai-feed");
|
|
1350
1378
|
if (before?.parentElement === feed) feed.insertBefore(divider, before);
|
|
1351
1379
|
else feed.append(divider);
|
|
@@ -1524,8 +1552,10 @@ function openAiToolCallDetail(toolCall) {
|
|
|
1524
1552
|
time.textContent = formatAiToolCallTime(calledAt);
|
|
1525
1553
|
if (calledAt && !Number.isNaN(new Date(calledAt).getTime())) time.dateTime = new Date(calledAt).toISOString();
|
|
1526
1554
|
else time.removeAttribute("datetime");
|
|
1555
|
+
const resultDetails = formatAiToolCallResult(toolCall?.result);
|
|
1527
1556
|
$("#ai-tool-call-arguments").textContent = JSON.stringify(toolCall?.arguments ?? {}, null, 2);
|
|
1528
|
-
$("#ai-tool-call-result").textContent =
|
|
1557
|
+
$("#ai-tool-call-result-length").textContent = `${resultDetails.characterCount.toLocaleString("zh-CN")} 字符`;
|
|
1558
|
+
$("#ai-tool-call-result").textContent = resultDetails.text;
|
|
1529
1559
|
$("#ai-tool-call-dialog").showModal();
|
|
1530
1560
|
}
|
|
1531
1561
|
|
|
@@ -1595,14 +1625,11 @@ function renderAiProcessSteps(message, steps, completed, durationMs = null) {
|
|
|
1595
1625
|
list.className = "ai-process-list";
|
|
1596
1626
|
for (const step of steps) {
|
|
1597
1627
|
if (step?.type === "context_compaction") {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
compaction.title = `已将 ${Number(step.sourceMessageCount) || 0} 条工具上下文压缩为摘要`;
|
|
1604
|
-
compaction.innerHTML = "<span>已压缩上下文</span>";
|
|
1605
|
-
list.append(compaction);
|
|
1628
|
+
list.append(createAiContextCompactionDivider({
|
|
1629
|
+
kind: "tool",
|
|
1630
|
+
ariaLabel: `第 ${Number(step.round) || 1} 轮已压缩上下文`,
|
|
1631
|
+
title: `已将 ${Number(step.sourceMessageCount) || 0} 条工具上下文压缩为摘要`
|
|
1632
|
+
}));
|
|
1606
1633
|
continue;
|
|
1607
1634
|
}
|
|
1608
1635
|
if (step?.type === "tool" && step.toolCall) {
|
|
@@ -1759,6 +1786,7 @@ async function openAiConversation(conversationId, hideHistory = true) {
|
|
|
1759
1786
|
upsertAiConversationSummary(conversation);
|
|
1760
1787
|
state.aiConversationId = conversation.id;
|
|
1761
1788
|
state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
|
|
1789
|
+
resetAiContextMeter();
|
|
1762
1790
|
$("#ai-conversation-title").textContent = conversation.title;
|
|
1763
1791
|
resetAiFeed();
|
|
1764
1792
|
for (const message of conversation.messages) appendMessage(message.role, message.content, message.citations, message.createdAt, message.metadata, message.id);
|
|
@@ -1786,7 +1814,7 @@ async function createNewAiConversation() {
|
|
|
1786
1814
|
$("#ai-conversation-title").textContent = conversation.title;
|
|
1787
1815
|
resetAiFeed();
|
|
1788
1816
|
hideAiContextWarning();
|
|
1789
|
-
|
|
1817
|
+
resetAiContextMeter();
|
|
1790
1818
|
renderAiQuickActions();
|
|
1791
1819
|
setAiHistoryVisible(false);
|
|
1792
1820
|
}
|
|
@@ -2296,19 +2324,29 @@ async function api(path, options = {}) {
|
|
|
2296
2324
|
const headers = { ...(options.headers ?? {}) };
|
|
2297
2325
|
if (state.csrfToken && !["GET", "HEAD", "OPTIONS"].includes(method)) headers["X-CSRF-Token"] = state.csrfToken;
|
|
2298
2326
|
if (!(body instanceof FormData)) headers["Content-Type"] = "application/json";
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
headers
|
|
2302
|
-
|
|
2303
|
-
|
|
2327
|
+
let response;
|
|
2328
|
+
try {
|
|
2329
|
+
response = await fetch(path, body instanceof FormData ? { ...options, body, headers } : {
|
|
2330
|
+
...options,
|
|
2331
|
+
headers,
|
|
2332
|
+
body: body && typeof body !== "string" ? JSON.stringify(body) : body
|
|
2333
|
+
});
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
updateSystemHealth({ status: "offline" });
|
|
2336
|
+
throw error;
|
|
2337
|
+
}
|
|
2338
|
+
updateSystemHealth({ status: response.status >= 500 ? "degraded" : "ready" });
|
|
2304
2339
|
if (!response.ok) {
|
|
2305
2340
|
const payload = await response.json().catch(() => ({ error: { message: `请求失败:${response.status}` } }));
|
|
2306
2341
|
// Presence is best-effort; a heartbeat 401 must not force the login wall.
|
|
2307
2342
|
if (response.status === 401 && !path.startsWith("/api/auth/") && !path.includes("/presence")) {
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2343
|
+
const restarted = await checkSystemBoot(true);
|
|
2344
|
+
if (!restarted) {
|
|
2345
|
+
state.user = null;
|
|
2346
|
+
state.csrfToken = null;
|
|
2347
|
+
moduleRequestCache.clear();
|
|
2348
|
+
showAuth(false);
|
|
2349
|
+
}
|
|
2312
2350
|
}
|
|
2313
2351
|
throw createClientError(payload.error, `请求失败:${response.status}`, response.status);
|
|
2314
2352
|
}
|
|
@@ -2317,6 +2355,12 @@ async function api(path, options = {}) {
|
|
|
2317
2355
|
return null;
|
|
2318
2356
|
}
|
|
2319
2357
|
const payload = await response.json();
|
|
2358
|
+
if (path === "/api/health") {
|
|
2359
|
+
updateSystemHealth({
|
|
2360
|
+
status: payload.data?.status === "ok" ? "ready" : "degraded",
|
|
2361
|
+
version: payload.data?.version
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2320
2364
|
invalidateModuleRequestsAfterMutation(path, method);
|
|
2321
2365
|
return payload.data;
|
|
2322
2366
|
}
|
|
@@ -2351,9 +2395,9 @@ function formatAiFailureMessage(error) {
|
|
|
2351
2395
|
if (status) lines.push(`服务端状态:HTTP ${status}`);
|
|
2352
2396
|
if (providerName || providerId) lines.push(`模型供应商:${providerName || providerId}`);
|
|
2353
2397
|
if (modelId) lines.push(`模型 ID:${modelId}`);
|
|
2354
|
-
if (failure && failure !== message) lines.push(`详细原因:${failure}`);
|
|
2355
2398
|
if (callId) lines.push(`调用 ID:${callId}`);
|
|
2356
|
-
|
|
2399
|
+
if (failure && failure !== message) lines.push(`详细原因:${failure}`);
|
|
2400
|
+
return lines.join("\n");
|
|
2357
2401
|
}
|
|
2358
2402
|
|
|
2359
2403
|
async function apiPage(path, page = 1, limit = 30) {
|
|
@@ -2431,23 +2475,97 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
2431
2475
|
affected.forEach((module) => moduleRequestCache.invalidate(state.work.id, module));
|
|
2432
2476
|
}
|
|
2433
2477
|
|
|
2478
|
+
function applyProductHealthMetadata(health) {
|
|
2479
|
+
const version = String(health?.version ?? "").trim();
|
|
2480
|
+
document.querySelectorAll("[data-product-footer-version]").forEach((element) => {
|
|
2481
|
+
element.textContent = version ? `v${version}` : "v—";
|
|
2482
|
+
});
|
|
2483
|
+
document.querySelectorAll("[data-product-footer-development]").forEach((element) => {
|
|
2484
|
+
element.classList.toggle("hidden", health?.development !== true);
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function scheduleSystemHealthCheck() {
|
|
2489
|
+
if (systemHealthTimer !== null) window.clearTimeout(systemHealthTimer);
|
|
2490
|
+
systemHealthTimer = window.setTimeout(() => {
|
|
2491
|
+
systemHealthTimer = null;
|
|
2492
|
+
void refreshSystemHealth();
|
|
2493
|
+
}, systemHealthPollInterval);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
async function refreshSystemHealth() {
|
|
2497
|
+
try {
|
|
2498
|
+
const health = await api("/api/health");
|
|
2499
|
+
applyProductHealthMetadata(health);
|
|
2500
|
+
} catch {
|
|
2501
|
+
// 系统状态已由 api 记录;健康检查失败不弹出重复提示
|
|
2502
|
+
} finally {
|
|
2503
|
+
scheduleSystemHealthCheck();
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2434
2507
|
async function initializeProductFooters() {
|
|
2435
2508
|
const year = String(new Date().getFullYear());
|
|
2436
2509
|
document.querySelectorAll("[data-product-footer-year]").forEach((element) => { element.textContent = year; });
|
|
2510
|
+
applyProductHealthMetadata(null);
|
|
2511
|
+
await refreshSystemHealth();
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
function observeSystemBootId(value) {
|
|
2515
|
+
const nextBootId = typeof value === "string" ? value.trim() : "";
|
|
2516
|
+
if (!nextBootId) return false;
|
|
2517
|
+
if (!systemBootId) {
|
|
2518
|
+
systemBootId = nextBootId;
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
if (nextBootId === systemBootId) return false;
|
|
2522
|
+
systemRestartDetected = true;
|
|
2523
|
+
if (systemBootCheckTimer !== null) clearTimeout(systemBootCheckTimer);
|
|
2524
|
+
systemBootCheckTimer = null;
|
|
2525
|
+
const dialog = $("#system-restart-dialog");
|
|
2526
|
+
if (!dialog.open) dialog.showModal();
|
|
2527
|
+
window.requestAnimationFrame(() => $("#system-restart-dialog-title").focus());
|
|
2528
|
+
return true;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
async function checkSystemBoot(forceFresh = false) {
|
|
2532
|
+
if (!state.user || systemRestartDetected) return systemRestartDetected;
|
|
2533
|
+
if (systemBootCheckPromise) {
|
|
2534
|
+
if (!forceFresh) return systemBootCheckPromise;
|
|
2535
|
+
await systemBootCheckPromise;
|
|
2536
|
+
if (systemRestartDetected) return true;
|
|
2537
|
+
}
|
|
2538
|
+
systemBootCheckPromise = (async () => {
|
|
2539
|
+
try {
|
|
2540
|
+
const response = await fetch("/api/health", {
|
|
2541
|
+
cache: "no-store",
|
|
2542
|
+
headers: { Accept: "application/json" }
|
|
2543
|
+
});
|
|
2544
|
+
if (!response.ok) return false;
|
|
2545
|
+
const payload = await response.json();
|
|
2546
|
+
return observeSystemBootId(payload?.data?.bootId);
|
|
2547
|
+
} catch {
|
|
2548
|
+
return false;
|
|
2549
|
+
}
|
|
2550
|
+
})();
|
|
2437
2551
|
try {
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
element.textContent = version ? `v${version}` : "v—";
|
|
2442
|
-
});
|
|
2443
|
-
document.querySelectorAll("[data-product-footer-development]").forEach((element) => {
|
|
2444
|
-
element.classList.toggle("hidden", health.development !== true);
|
|
2445
|
-
});
|
|
2446
|
-
} catch {
|
|
2447
|
-
document.querySelectorAll("[data-product-footer-version]").forEach((element) => { element.textContent = "v—"; });
|
|
2552
|
+
return await systemBootCheckPromise;
|
|
2553
|
+
} finally {
|
|
2554
|
+
systemBootCheckPromise = null;
|
|
2448
2555
|
}
|
|
2449
2556
|
}
|
|
2450
2557
|
|
|
2558
|
+
function scheduleSystemBootCheck(delay = systemBootCheckInterval) {
|
|
2559
|
+
if (systemBootCheckTimer !== null) clearTimeout(systemBootCheckTimer);
|
|
2560
|
+
systemBootCheckTimer = null;
|
|
2561
|
+
if (!state.user || systemRestartDetected) return;
|
|
2562
|
+
systemBootCheckTimer = setTimeout(async () => {
|
|
2563
|
+
systemBootCheckTimer = null;
|
|
2564
|
+
await checkSystemBoot();
|
|
2565
|
+
scheduleSystemBootCheck();
|
|
2566
|
+
}, delay);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2451
2569
|
function selectAuthMode(mode) {
|
|
2452
2570
|
const registerTab = $("#auth-register-tab");
|
|
2453
2571
|
const login = mode === "login" || registerTab.disabled;
|
|
@@ -2589,7 +2707,9 @@ async function initializeAuthentication() {
|
|
|
2589
2707
|
}
|
|
2590
2708
|
// 已登录却停在登录页路由时,回到书架首页
|
|
2591
2709
|
if (route.view === "login") window.history.replaceState(null, "", serializePageRoute({ view: "shelf" }));
|
|
2710
|
+
observeSystemBootId(session.bootId);
|
|
2592
2711
|
applyAuthenticatedUser(session);
|
|
2712
|
+
scheduleSystemBootCheck();
|
|
2593
2713
|
await loadPlatformUiSettings();
|
|
2594
2714
|
return true;
|
|
2595
2715
|
}
|
|
@@ -2745,10 +2865,53 @@ document.addEventListener("toggle", (event) => {
|
|
|
2745
2865
|
}
|
|
2746
2866
|
}, true);
|
|
2747
2867
|
|
|
2868
|
+
function paintTopbarStatus(text, { source, tone, title }) {
|
|
2869
|
+
const element = $("#save-state");
|
|
2870
|
+
const colors = { ok: "var(--green)", pending: "var(--muted)", error: "var(--accent)" };
|
|
2871
|
+
topbarStatusSource = source;
|
|
2872
|
+
element.textContent = text;
|
|
2873
|
+
element.style.color = colors[tone] ?? colors.ok;
|
|
2874
|
+
element.dataset.statusSource = source;
|
|
2875
|
+
element.setAttribute("aria-label", `${source === "system" ? "系统" : "工作台"}状态:${text}`);
|
|
2876
|
+
element.title = title || text;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function setTopbarViewState(text) {
|
|
2880
|
+
state.dirty = false;
|
|
2881
|
+
paintTopbarStatus(text, { source: "view", tone: "ok", title: text });
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
function updateSystemHealth(next) {
|
|
2885
|
+
systemHealthSnapshot = {
|
|
2886
|
+
...systemHealthSnapshot,
|
|
2887
|
+
...next,
|
|
2888
|
+
version: next.version === undefined ? systemHealthSnapshot.version : String(next.version ?? "")
|
|
2889
|
+
};
|
|
2890
|
+
if (topbarStatusSource === "system") renderSystemHealth();
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
function renderSystemHealth() {
|
|
2894
|
+
const presentation = systemStatusPresentation(systemHealthSnapshot);
|
|
2895
|
+
paintTopbarStatus(presentation.label, {
|
|
2896
|
+
source: "system",
|
|
2897
|
+
tone: presentation.tone,
|
|
2898
|
+
title: presentation.title
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
function showSystemStatus() {
|
|
2903
|
+
state.dirty = false;
|
|
2904
|
+
topbarStatusSource = "system";
|
|
2905
|
+
renderSystemHealth();
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2748
2908
|
function setSaveState(text, dirty = false) {
|
|
2749
2909
|
state.dirty = dirty;
|
|
2750
|
-
|
|
2751
|
-
|
|
2910
|
+
paintTopbarStatus(text, {
|
|
2911
|
+
source: "save",
|
|
2912
|
+
tone: dirty ? "error" : "ok",
|
|
2913
|
+
title: text
|
|
2914
|
+
});
|
|
2752
2915
|
}
|
|
2753
2916
|
|
|
2754
2917
|
function chapterDraftSnapshot() {
|
|
@@ -2990,6 +3153,11 @@ async function initializePage() {
|
|
|
2990
3153
|
if (route.view === "platform-usage") {
|
|
2991
3154
|
await showPlatformUsage();
|
|
2992
3155
|
settingsReturnContext = restoredSettingsReturnContext(route);
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
if (route.view === "work-audit") {
|
|
3159
|
+
await showWorkAudit();
|
|
3160
|
+
settingsReturnContext = restoredSettingsReturnContext(route);
|
|
2993
3161
|
}
|
|
2994
3162
|
} finally {
|
|
2995
3163
|
document.body.classList.remove("auth-pending");
|
|
@@ -3009,6 +3177,7 @@ function showShelf() {
|
|
|
3009
3177
|
$("#shelf-view").classList.remove("hidden");
|
|
3010
3178
|
$("#platform-ai-view").classList.add("hidden");
|
|
3011
3179
|
$("#platform-usage-view").classList.add("hidden");
|
|
3180
|
+
$("#work-audit-view").classList.add("hidden");
|
|
3012
3181
|
$("#settings-hub-view").classList.add("hidden");
|
|
3013
3182
|
$("#welcome-view").classList.add("hidden");
|
|
3014
3183
|
$("#editor-view").classList.add("hidden");
|
|
@@ -3016,7 +3185,7 @@ function showShelf() {
|
|
|
3016
3185
|
$("#work-meta").textContent = `${state.works.length} 部作品`;
|
|
3017
3186
|
$("#settings-button").removeAttribute("aria-current");
|
|
3018
3187
|
$("#top-search-button").disabled = true;
|
|
3019
|
-
|
|
3188
|
+
setTopbarViewState("书架");
|
|
3020
3189
|
renderShelf();
|
|
3021
3190
|
replacePageRoute({ view: "shelf" });
|
|
3022
3191
|
}
|
|
@@ -3033,7 +3202,7 @@ function renderSettingsHub() {
|
|
|
3033
3202
|
const hasWork = Boolean(state.work);
|
|
3034
3203
|
const canManageWork = hasWork && ["admin", "owner"].includes(String(state.work.accessRole));
|
|
3035
3204
|
const canReadAggregate = hasWork && canReadAggregateContent();
|
|
3036
|
-
const
|
|
3205
|
+
const canExportManuscript = hasWork && canReadModule("editor");
|
|
3037
3206
|
const isAdmin = state.user?.role === "admin";
|
|
3038
3207
|
$("#platform-ai-button").classList.toggle("hidden", !isAdmin);
|
|
3039
3208
|
$("#platform-usage-button").classList.toggle("hidden", !isAdmin);
|
|
@@ -3043,10 +3212,11 @@ function renderSettingsHub() {
|
|
|
3043
3212
|
$("#writing-progress-button").disabled = !hasWork || !canReadModule("editor");
|
|
3044
3213
|
$("#work-audit-button").disabled = !canManageWork;
|
|
3045
3214
|
$("#top-search-button").disabled = !canReadAggregate;
|
|
3046
|
-
$("#export-button").disabled = !
|
|
3215
|
+
$("#export-button").disabled = !canExportManuscript;
|
|
3216
|
+
$("#export-button").setAttribute("aria-expanded", "false");
|
|
3047
3217
|
$("#settings-return").textContent = settingsReturnContext?.view === "shelf" || !hasWork ? "返回书架" : "返回当前作品";
|
|
3048
3218
|
$("#settings-work-note").textContent = hasWork
|
|
3049
|
-
? `当前作品:《${state.work.title}
|
|
3219
|
+
? `当前作品:《${state.work.title}》。导出正文时可选择 Markdown ZIP 或 DOCX;DOCX 在有封面时会嵌入为首页。`
|
|
3050
3220
|
: "当前未选择作品;打开作品后可使用导出。";
|
|
3051
3221
|
}
|
|
3052
3222
|
|
|
@@ -3115,36 +3285,173 @@ async function openWritingProgressDialog() {
|
|
|
3115
3285
|
const workAuditActionLabels = {
|
|
3116
3286
|
"work.created": "创建作品",
|
|
3117
3287
|
"work.updated": "更新作品",
|
|
3288
|
+
"work.cover.updated": "更新作品封面",
|
|
3289
|
+
"work.cover.deleted": "删除作品封面",
|
|
3290
|
+
"work.member-added": "添加作品成员",
|
|
3291
|
+
"work.member-role-updated": "更新成员权限",
|
|
3292
|
+
"work.member-removed": "移除作品成员",
|
|
3293
|
+
"work.writing_goal.updated": "更新写作目标",
|
|
3118
3294
|
"volume.created": "创建分卷",
|
|
3119
3295
|
"volume.updated": "更新分卷",
|
|
3120
3296
|
"volume.deleted": "删除分卷",
|
|
3297
|
+
"volume.restored": "恢复分卷",
|
|
3121
3298
|
"chapter.created": "创建章节",
|
|
3122
3299
|
"chapter.saved": "保存章节",
|
|
3123
3300
|
"chapter.moved": "移动章节",
|
|
3124
3301
|
"chapter.deleted": "删除章节",
|
|
3125
3302
|
"chapter.purged": "彻底删除章节",
|
|
3126
3303
|
"chapter.restored": "恢复章节",
|
|
3304
|
+
"chapter.annotation.created": "添加正文评论",
|
|
3305
|
+
"chapter.annotation.updated": "更新正文评论",
|
|
3306
|
+
"chapter.annotation.deleted": "删除正文评论",
|
|
3127
3307
|
"draft.created": "创建想法",
|
|
3128
3308
|
"draft.updated": "更新想法",
|
|
3129
3309
|
"draft.deleted": "删除想法",
|
|
3130
3310
|
"draft.restored": "恢复想法",
|
|
3311
|
+
"setting.created": "创建设定",
|
|
3312
|
+
"setting.updated": "更新设定",
|
|
3313
|
+
"setting.deleted": "删除设定",
|
|
3314
|
+
"setting.restored": "恢复设定",
|
|
3315
|
+
"character.created": "创建角色",
|
|
3316
|
+
"character.updated": "更新角色",
|
|
3317
|
+
"character.deleted": "删除角色",
|
|
3318
|
+
"character.restored": "恢复角色",
|
|
3319
|
+
"race.created": "创建种族",
|
|
3320
|
+
"race.updated": "更新种族",
|
|
3321
|
+
"race.deleted": "删除种族",
|
|
3322
|
+
"race.restored": "恢复种族",
|
|
3323
|
+
"organization.created": "创建组织",
|
|
3324
|
+
"organization.updated": "更新组织",
|
|
3325
|
+
"organization.deleted": "删除组织",
|
|
3326
|
+
"organization.restored": "恢复组织",
|
|
3327
|
+
"timeline-track.created": "创建时间轴",
|
|
3328
|
+
"timeline-track.updated": "更新时间轴",
|
|
3329
|
+
"timeline-track.deleted": "删除时间轴",
|
|
3330
|
+
"timeline-track.restored": "恢复时间轴",
|
|
3331
|
+
"timeline.created": "创建时间事件",
|
|
3332
|
+
"timeline.updated": "更新时间事件",
|
|
3333
|
+
"timeline.deleted": "删除时间事件",
|
|
3334
|
+
"timeline.restored": "恢复时间事件",
|
|
3335
|
+
"relationship.created": "创建人物关系",
|
|
3336
|
+
"relationship.updated": "更新人物关系",
|
|
3337
|
+
"relationship.deleted": "删除人物关系",
|
|
3338
|
+
"relationship.restored": "恢复人物关系",
|
|
3339
|
+
"outline.created": "创建章节大纲",
|
|
3340
|
+
"outline.updated": "更新章节大纲",
|
|
3341
|
+
"outline.deleted": "删除章节大纲",
|
|
3342
|
+
"foreshadow.created": "创建伏笔",
|
|
3343
|
+
"foreshadow.updated": "更新伏笔",
|
|
3344
|
+
"foreshadow.deleted": "删除伏笔",
|
|
3345
|
+
"foreshadow.restored": "恢复伏笔",
|
|
3346
|
+
"task.created": "创建 AI 分析任务",
|
|
3347
|
+
"task.cancelled": "取消 AI 分析任务",
|
|
3348
|
+
"attachment.created": "创建附件",
|
|
3349
|
+
"attachment.deleted": "删除附件",
|
|
3350
|
+
"attachment.garbage-collected": "清理未引用附件",
|
|
3351
|
+
"file.restored": "恢复导入快照",
|
|
3131
3352
|
"work.imported": "导入正文"
|
|
3132
3353
|
};
|
|
3133
3354
|
|
|
3134
3355
|
function workAuditEntityLabel(type) {
|
|
3135
|
-
return ({
|
|
3356
|
+
return ({
|
|
3357
|
+
work: "作品",
|
|
3358
|
+
volume: "分卷",
|
|
3359
|
+
chapter: "章节",
|
|
3360
|
+
draft: "想法",
|
|
3361
|
+
user: "用户",
|
|
3362
|
+
setting: "设定",
|
|
3363
|
+
character: "角色",
|
|
3364
|
+
race: "种族",
|
|
3365
|
+
organization: "组织",
|
|
3366
|
+
"timeline-track": "时间轴",
|
|
3367
|
+
"timeline-event": "时间事件",
|
|
3368
|
+
relationship: "人物关系",
|
|
3369
|
+
"chapter-outline": "章节大纲",
|
|
3370
|
+
foreshadow: "伏笔",
|
|
3371
|
+
"chapter-annotation": "正文评论",
|
|
3372
|
+
attachment: "附件",
|
|
3373
|
+
"file-version": "导入快照",
|
|
3374
|
+
"analysis-task": "AI 分析任务",
|
|
3375
|
+
review: "审核"
|
|
3376
|
+
})[type] ?? type;
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
const workAuditDetailLabels = {
|
|
3380
|
+
fields: "变更字段",
|
|
3381
|
+
versionNo: "版本号",
|
|
3382
|
+
fromVersion: "来源版本",
|
|
3383
|
+
source: "操作来源",
|
|
3384
|
+
sourceRef: "来源引用",
|
|
3385
|
+
changeNote: "变更说明",
|
|
3386
|
+
name: "名称",
|
|
3387
|
+
description: "说明",
|
|
3388
|
+
chapterType: "章节类型",
|
|
3389
|
+
volumeId: "所属分卷",
|
|
3390
|
+
previousVolumeId: "原分卷",
|
|
3391
|
+
sortOrder: "排序位置",
|
|
3392
|
+
timeLabel: "时间标签",
|
|
3393
|
+
location: "地点",
|
|
3394
|
+
eventType: "事件类型",
|
|
3395
|
+
batch: "批量操作",
|
|
3396
|
+
recoverable: "可恢复",
|
|
3397
|
+
excludedFromAnalysis: "排除 AI 分析",
|
|
3398
|
+
startLine: "起始行",
|
|
3399
|
+
endLine: "结束行",
|
|
3400
|
+
characterId: "角色 ID",
|
|
3401
|
+
restorePointId: "恢复点 ID",
|
|
3402
|
+
storageKey: "存储位置",
|
|
3403
|
+
byteLength: "文件字节数",
|
|
3404
|
+
mimeType: "文件类型",
|
|
3405
|
+
role: "成员角色",
|
|
3406
|
+
status: "状态",
|
|
3407
|
+
previousStatus: "原状态",
|
|
3408
|
+
dailyGoal: "每日目标",
|
|
3409
|
+
targetTotal: "总字数目标",
|
|
3410
|
+
deadline: "计划完成日期",
|
|
3411
|
+
title: "标题",
|
|
3412
|
+
reason: "原因"
|
|
3413
|
+
};
|
|
3414
|
+
|
|
3415
|
+
function workAuditDetailValue(value) {
|
|
3416
|
+
if (typeof value === "boolean") return value ? "是" : "否";
|
|
3417
|
+
if (Array.isArray(value)) return value.map((item) => typeof item === "object" ? JSON.stringify(item) : String(item)).join("、");
|
|
3418
|
+
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
3419
|
+
return String(value);
|
|
3136
3420
|
}
|
|
3137
3421
|
|
|
3138
|
-
function
|
|
3139
|
-
|
|
3140
|
-
return entries
|
|
3422
|
+
function workAuditDetailEntries(detail) {
|
|
3423
|
+
if (!detail || typeof detail !== "object" || Array.isArray(detail)) return [];
|
|
3424
|
+
return Object.entries(detail)
|
|
3425
|
+
.filter(([, value]) => value !== null && value !== undefined && value !== "")
|
|
3426
|
+
.map(([key, value]) => ({ label: workAuditDetailLabels[key] ?? key, value: workAuditDetailValue(value) }));
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
function workAuditTimestamp(createdAt) {
|
|
3430
|
+
const formatted = formatDateTime(createdAt);
|
|
3431
|
+
const parts = formatted.split(/\s+/u);
|
|
3432
|
+
return { date: parts[0] || "—", time: parts.slice(1).join(" ") || "—" };
|
|
3141
3433
|
}
|
|
3142
3434
|
|
|
3143
3435
|
function renderWorkAuditRecords() {
|
|
3144
|
-
$("#work-audit-list").innerHTML = workAuditRecords.length ? workAuditRecords.map((record) =>
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3436
|
+
$("#work-audit-list").innerHTML = workAuditRecords.length ? workAuditRecords.map((record) => {
|
|
3437
|
+
const timestamp = workAuditTimestamp(record.createdAt);
|
|
3438
|
+
const details = workAuditDetailEntries(record.detail);
|
|
3439
|
+
return `<article class="work-audit-row">
|
|
3440
|
+
<header class="work-audit-time"><time datetime="${esc(record.createdAt)}"><span>${esc(timestamp.date)}</span><strong>${esc(timestamp.time)}</strong></time></header>
|
|
3441
|
+
<div class="work-audit-event">
|
|
3442
|
+
<div class="work-audit-event-heading"><strong>${esc(workAuditActionLabels[record.action] ?? record.action)}</strong><code>${esc(record.action)}</code></div>
|
|
3443
|
+
<dl class="work-audit-meta">
|
|
3444
|
+
<div><dt>操作者</dt><dd>${esc(record.actor || "system")}</dd></div>
|
|
3445
|
+
<div><dt>对象类型</dt><dd>${esc(workAuditEntityLabel(record.entityType))}</dd></div>
|
|
3446
|
+
<div><dt>对象 ID</dt><dd><code>${record.entityId ? esc(record.entityId) : "—"}</code></dd></div>
|
|
3447
|
+
</dl>
|
|
3448
|
+
${details.length ? `<dl class="work-audit-details">${details.map((detail) => `<div><dt>${esc(detail.label)}</dt><dd><code>${esc(detail.value)}</code></dd></div>`).join("")}</dl>` : ""}
|
|
3449
|
+
</div>
|
|
3450
|
+
</article>`;
|
|
3451
|
+
}).join("") : '<p class="entity-history-empty">当前作品还没有操作记录。</p>';
|
|
3452
|
+
$("#work-audit-summary").textContent = workAuditRecords.length
|
|
3453
|
+
? `已显示 ${workAuditRecords.length} 条记录${workAuditNextPage === null ? ",已加载全部记录" : ",还有更多记录可继续加载"}`
|
|
3454
|
+
: "当前作品还没有操作记录。";
|
|
3148
3455
|
$("#work-audit-load-more").classList.toggle("hidden", workAuditNextPage === null);
|
|
3149
3456
|
}
|
|
3150
3457
|
|
|
@@ -3156,18 +3463,36 @@ async function loadWorkAuditPage(page = 1, append = false) {
|
|
|
3156
3463
|
renderWorkAuditRecords();
|
|
3157
3464
|
}
|
|
3158
3465
|
|
|
3159
|
-
async function
|
|
3160
|
-
if (!state.work || !["admin", "owner"].includes(String(state.work.accessRole))) return;
|
|
3466
|
+
async function showWorkAudit() {
|
|
3467
|
+
if (!state.work || !["admin", "owner"].includes(String(state.work.accessRole))) return false;
|
|
3161
3468
|
workAuditRecords = [];
|
|
3162
3469
|
workAuditNextPage = null;
|
|
3470
|
+
dismissChapterInsightToast();
|
|
3471
|
+
updateDocumentTitle(state.work);
|
|
3472
|
+
$("#app").classList.add("shelf-mode");
|
|
3473
|
+
$("#shelf-view").classList.add("hidden");
|
|
3474
|
+
$("#platform-ai-view").classList.add("hidden");
|
|
3475
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
3476
|
+
$("#settings-hub-view").classList.add("hidden");
|
|
3477
|
+
$("#work-audit-view").classList.remove("hidden");
|
|
3478
|
+
$("#welcome-view").classList.add("hidden");
|
|
3479
|
+
$("#editor-view").classList.add("hidden");
|
|
3480
|
+
$("#module-view").classList.add("hidden");
|
|
3481
|
+
$("#work-audit-eyebrow").textContent = `作品安全 · 《${state.work.title}》`;
|
|
3482
|
+
$("#work-meta").textContent = "操作记录";
|
|
3483
|
+
$("#settings-button").setAttribute("aria-current", "page");
|
|
3484
|
+
setTopbarViewState("操作记录");
|
|
3485
|
+
$("#work-audit-summary").textContent = "正在读取操作记录……";
|
|
3163
3486
|
$("#work-audit-list").innerHTML = '<p class="entity-history-empty">正在加载操作记录…</p>';
|
|
3164
|
-
|
|
3487
|
+
replacePageRoute({ view: "work-audit", workId: state.work.id, ...settingsRouteContext() });
|
|
3165
3488
|
try {
|
|
3166
3489
|
await loadWorkAuditPage();
|
|
3167
3490
|
} catch (error) {
|
|
3168
|
-
$("#work-audit-
|
|
3491
|
+
$("#work-audit-summary").textContent = "操作记录加载失败。";
|
|
3492
|
+
$("#work-audit-list").innerHTML = '<p class="entity-history-empty">暂时无法读取操作记录,请稍后刷新。</p>';
|
|
3169
3493
|
toast(error.message, "error");
|
|
3170
3494
|
}
|
|
3495
|
+
return true;
|
|
3171
3496
|
}
|
|
3172
3497
|
|
|
3173
3498
|
async function saveWritingGoal(event) {
|
|
@@ -3468,7 +3793,8 @@ async function openSearchResult(result) {
|
|
|
3468
3793
|
$("#search-dialog").close();
|
|
3469
3794
|
const inSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
3470
3795
|
|| !$("#platform-ai-view").classList.contains("hidden")
|
|
3471
|
-
|| !$("#platform-usage-view").classList.contains("hidden")
|
|
3796
|
+
|| !$("#platform-usage-view").classList.contains("hidden")
|
|
3797
|
+
|| !$("#work-audit-view").classList.contains("hidden");
|
|
3472
3798
|
if (inSettings) await returnFromSettings();
|
|
3473
3799
|
if (target.kind === "chapter") {
|
|
3474
3800
|
await selectChapter(target.id);
|
|
@@ -3494,7 +3820,8 @@ async function openSearchResult(result) {
|
|
|
3494
3820
|
async function showSettingsHub() {
|
|
3495
3821
|
const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
3496
3822
|
|| !$("#platform-ai-view").classList.contains("hidden")
|
|
3497
|
-
|| !$("#platform-usage-view").classList.contains("hidden")
|
|
3823
|
+
|| !$("#platform-usage-view").classList.contains("hidden")
|
|
3824
|
+
|| !$("#work-audit-view").classList.contains("hidden");
|
|
3498
3825
|
if (!alreadyInSettings) {
|
|
3499
3826
|
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?"))) return false;
|
|
3500
3827
|
settingsReturnContext = captureSettingsReturnContext();
|
|
@@ -3506,13 +3833,14 @@ async function showSettingsHub() {
|
|
|
3506
3833
|
$("#shelf-view").classList.add("hidden");
|
|
3507
3834
|
$("#platform-ai-view").classList.add("hidden");
|
|
3508
3835
|
$("#platform-usage-view").classList.add("hidden");
|
|
3836
|
+
$("#work-audit-view").classList.add("hidden");
|
|
3509
3837
|
$("#settings-hub-view").classList.remove("hidden");
|
|
3510
3838
|
$("#welcome-view").classList.add("hidden");
|
|
3511
3839
|
$("#editor-view").classList.add("hidden");
|
|
3512
3840
|
$("#module-view").classList.add("hidden");
|
|
3513
3841
|
$("#work-meta").textContent = "设置中心";
|
|
3514
3842
|
$("#settings-button").setAttribute("aria-current", "page");
|
|
3515
|
-
|
|
3843
|
+
setTopbarViewState("设置");
|
|
3516
3844
|
renderSettingsHub();
|
|
3517
3845
|
replacePageRoute({ view: "settings", workId: state.work?.id ?? null, ...settingsRouteContext() });
|
|
3518
3846
|
return true;
|
|
@@ -3532,6 +3860,7 @@ async function returnFromSettings() {
|
|
|
3532
3860
|
$("#settings-hub-view").classList.add("hidden");
|
|
3533
3861
|
$("#platform-ai-view").classList.add("hidden");
|
|
3534
3862
|
$("#platform-usage-view").classList.add("hidden");
|
|
3863
|
+
$("#work-audit-view").classList.add("hidden");
|
|
3535
3864
|
if (context.view === "shelf" || !state.work) return showShelf();
|
|
3536
3865
|
$("#app").classList.remove("shelf-mode");
|
|
3537
3866
|
$("#shelf-view").classList.add("hidden");
|
|
@@ -3551,13 +3880,14 @@ async function showPlatformAi() {
|
|
|
3551
3880
|
$("#shelf-view").classList.add("hidden");
|
|
3552
3881
|
$("#platform-ai-view").classList.remove("hidden");
|
|
3553
3882
|
$("#platform-usage-view").classList.add("hidden");
|
|
3883
|
+
$("#work-audit-view").classList.add("hidden");
|
|
3554
3884
|
$("#settings-hub-view").classList.add("hidden");
|
|
3555
3885
|
$("#welcome-view").classList.add("hidden");
|
|
3556
3886
|
$("#editor-view").classList.add("hidden");
|
|
3557
3887
|
$("#module-view").classList.add("hidden");
|
|
3558
3888
|
$("#work-meta").textContent = "平台 AI 管理";
|
|
3559
3889
|
$("#settings-button").setAttribute("aria-current", "page");
|
|
3560
|
-
|
|
3890
|
+
setTopbarViewState("平台 AI");
|
|
3561
3891
|
await renderPlatformAiConfig();
|
|
3562
3892
|
replacePageRoute({ view: "platform-ai", workId: state.work?.id ?? null, ...settingsRouteContext() });
|
|
3563
3893
|
return true;
|
|
@@ -3571,13 +3901,14 @@ async function showPlatformUsage() {
|
|
|
3571
3901
|
$("#shelf-view").classList.add("hidden");
|
|
3572
3902
|
$("#platform-ai-view").classList.add("hidden");
|
|
3573
3903
|
$("#platform-usage-view").classList.remove("hidden");
|
|
3904
|
+
$("#work-audit-view").classList.add("hidden");
|
|
3574
3905
|
$("#settings-hub-view").classList.add("hidden");
|
|
3575
3906
|
$("#welcome-view").classList.add("hidden");
|
|
3576
3907
|
$("#editor-view").classList.add("hidden");
|
|
3577
3908
|
$("#module-view").classList.add("hidden");
|
|
3578
3909
|
$("#work-meta").textContent = "Token 用量";
|
|
3579
3910
|
$("#settings-button").setAttribute("aria-current", "page");
|
|
3580
|
-
|
|
3911
|
+
setTopbarViewState("Token 用量");
|
|
3581
3912
|
await renderPlatformTokenUsage();
|
|
3582
3913
|
replacePageRoute({ view: "platform-usage", workId: state.work?.id ?? null, ...settingsRouteContext() });
|
|
3583
3914
|
return true;
|
|
@@ -3651,7 +3982,7 @@ function resetWorkScopedUiCaches() {
|
|
|
3651
3982
|
resetAiFeed();
|
|
3652
3983
|
$("#ai-conversation-title").textContent = "新对话";
|
|
3653
3984
|
$("#ai-model").innerHTML = '<option value="">使用创作助手时加载模型</option>';
|
|
3654
|
-
|
|
3985
|
+
resetAiContextMeter();
|
|
3655
3986
|
renderAiConversationHistory();
|
|
3656
3987
|
}
|
|
3657
3988
|
|
|
@@ -3666,11 +3997,12 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
3666
3997
|
}
|
|
3667
3998
|
const nextWork = await api(`/api/works/${workId}?directory=volumes`);
|
|
3668
3999
|
if (state.work?.id !== nextWork.id) resetWorkScopedUiCaches();
|
|
3669
|
-
|
|
4000
|
+
showSystemStatus();
|
|
3670
4001
|
$("#app").classList.remove("shelf-mode");
|
|
3671
4002
|
$("#shelf-view").classList.add("hidden");
|
|
3672
4003
|
$("#platform-ai-view").classList.add("hidden");
|
|
3673
4004
|
$("#platform-usage-view").classList.add("hidden");
|
|
4005
|
+
$("#work-audit-view").classList.add("hidden");
|
|
3674
4006
|
$("#settings-hub-view").classList.add("hidden");
|
|
3675
4007
|
$("#settings-button").removeAttribute("aria-current");
|
|
3676
4008
|
settingsReturnContext = null;
|
|
@@ -4151,6 +4483,7 @@ function showWelcome(hasWork = false) {
|
|
|
4151
4483
|
$("#editor-view").classList.add("hidden");
|
|
4152
4484
|
$("#module-view").classList.add("hidden");
|
|
4153
4485
|
$("#welcome-view").classList.remove("hidden");
|
|
4486
|
+
if (hasWork) showSystemStatus();
|
|
4154
4487
|
$("#welcome-view h1").innerHTML = hasWork ? "故事已经就位,<br>从新章节继续。" : "把长篇故事的每条线索,<br>留在作者掌控之中。";
|
|
4155
4488
|
$("#welcome-new-work").textContent = hasWork ? "新建章节" : "创建第一部作品";
|
|
4156
4489
|
replacePageRoute(hasWork && state.work ? { view: "welcome", workId: state.work.id } : { view: "shelf" });
|
|
@@ -4204,6 +4537,7 @@ async function showModule(module) {
|
|
|
4204
4537
|
}
|
|
4205
4538
|
return;
|
|
4206
4539
|
}
|
|
4540
|
+
showSystemStatus();
|
|
4207
4541
|
dismissChapterInsightToast();
|
|
4208
4542
|
$("#welcome-view").classList.add("hidden");
|
|
4209
4543
|
$("#editor-view").classList.add("hidden");
|
|
@@ -4698,8 +5032,7 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
4698
5032
|
<div><strong>想法操作</strong><small>删除后将从想法列表移除,版本历史仍会保留。</small></div>
|
|
4699
5033
|
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-draft-delete>删除想法</button></div>
|
|
4700
5034
|
</section>` : "";
|
|
4701
|
-
const fields =
|
|
4702
|
-
+ field("draftType", "想法类型", "select", item?.draftType ?? "prose", [["prose", "正文想法"], ["setting", "设定想法"]])
|
|
5035
|
+
const fields = field("draftType", "想法类型", "select", item?.draftType ?? "prose", [["prose", "正文想法"], ["setting", "设定想法"]])
|
|
4703
5036
|
+ `<div class="draft-binding-field" data-draft-binding-field="prose">${field("volumeId", "绑定分卷", "select", item?.volumeId ?? "", volumeOptions)}</div>`
|
|
4704
5037
|
+ `<div class="draft-binding-field" data-draft-binding-field="setting">${field("settingModule", "绑定设定模块", "select", item?.settingModule ?? "", settingModuleOptions)}</div>`
|
|
4705
5038
|
+ field("title", "标题", "text", item?.title ?? "")
|
|
@@ -4731,7 +5064,8 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
4731
5064
|
submitLabel: viewOnly ? "关闭" : "保存想法",
|
|
4732
5065
|
hideCancel: viewOnly,
|
|
4733
5066
|
editor: true,
|
|
4734
|
-
errorPrefix: "想法保存失败:"
|
|
5067
|
+
errorPrefix: "想法保存失败:",
|
|
5068
|
+
meta: "这里记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。"
|
|
4735
5069
|
});
|
|
4736
5070
|
const draftTypeSelect = $("#dialog-fields").querySelector('select[name="draftType"]');
|
|
4737
5071
|
const syncDraftBindingFields = () => {
|
|
@@ -6560,21 +6894,72 @@ function tokenUsageCalendarMarkup(daily) {
|
|
|
6560
6894
|
const calendar = buildUsageCalendar(daily);
|
|
6561
6895
|
const cells = calendar.cells.map((cell) => {
|
|
6562
6896
|
const label = `${tokenUsageDateLabel(cell.date)}:${Number(cell.totalTokens).toLocaleString("zh-CN")} Token`;
|
|
6563
|
-
return
|
|
6897
|
+
return cell.future
|
|
6898
|
+
? `<span class="usage-calendar-cell is-future" data-level="${cell.level}" role="gridcell" aria-disabled="true"></span>`
|
|
6899
|
+
: `<button class="usage-calendar-cell" type="button" data-level="${cell.level}" data-usage-calendar-label="${esc(label)}" role="gridcell" aria-label="${esc(label)}"></button>`;
|
|
6564
6900
|
}).join("");
|
|
6565
6901
|
const months = calendar.months.map((month) => `<span style="grid-column:${month.week + 1}">${esc(month.label)}</span>`).join("");
|
|
6566
|
-
return `<div class="usage-calendar-
|
|
6567
|
-
<div class="usage-calendar-
|
|
6568
|
-
<div class="usage-calendar-
|
|
6569
|
-
|
|
6570
|
-
<div class="usage-calendar-
|
|
6571
|
-
|
|
6902
|
+
return `<div class="usage-calendar-widget">
|
|
6903
|
+
<div class="usage-calendar-scroll" tabindex="0" aria-label="每日 Token 用量日历,可横向滚动">
|
|
6904
|
+
<div class="usage-calendar-frame" style="--usage-week-count:${calendar.weekCount}">
|
|
6905
|
+
<div class="usage-calendar-months" aria-hidden="true">${months}</div>
|
|
6906
|
+
<div class="usage-calendar-body">
|
|
6907
|
+
<div class="usage-calendar-weekdays" aria-hidden="true"><span>一</span><span>三</span><span>五</span></div>
|
|
6908
|
+
<div class="usage-calendar-grid" role="grid" aria-label="过去 53 周每日 Token 用量">${cells}</div>
|
|
6909
|
+
</div>
|
|
6572
6910
|
</div>
|
|
6573
6911
|
</div>
|
|
6912
|
+
<output class="usage-calendar-tooltip" role="tooltip" hidden></output>
|
|
6574
6913
|
</div>
|
|
6575
6914
|
<div class="usage-calendar-legend"><span>少</span>${[0, 1, 2, 3, 4].map((level) => `<i data-level="${level}" aria-hidden="true"></i>`).join("")}<span>多</span></div>`;
|
|
6576
6915
|
}
|
|
6577
6916
|
|
|
6917
|
+
function bindUsageCalendarInteractions(root) {
|
|
6918
|
+
root.querySelectorAll(".usage-calendar-widget").forEach((widget) => {
|
|
6919
|
+
const tooltip = widget.querySelector(".usage-calendar-tooltip");
|
|
6920
|
+
const calendarScroll = widget.querySelector(".usage-calendar-scroll");
|
|
6921
|
+
let activeCell = null;
|
|
6922
|
+
const hideTooltip = () => {
|
|
6923
|
+
tooltip.hidden = true;
|
|
6924
|
+
activeCell = null;
|
|
6925
|
+
};
|
|
6926
|
+
const showTooltip = (cell) => {
|
|
6927
|
+
activeCell = cell;
|
|
6928
|
+
tooltip.textContent = cell.dataset.usageCalendarLabel;
|
|
6929
|
+
tooltip.hidden = false;
|
|
6930
|
+
const widgetRect = widget.getBoundingClientRect();
|
|
6931
|
+
const cellRect = cell.getBoundingClientRect();
|
|
6932
|
+
const edgeInset = tooltip.offsetWidth / 2 + 8;
|
|
6933
|
+
const centeredLeft = cellRect.left + cellRect.width / 2 - widgetRect.left;
|
|
6934
|
+
const fitsAbove = cellRect.top - widgetRect.top >= tooltip.offsetHeight + 8;
|
|
6935
|
+
tooltip.dataset.placement = fitsAbove ? "top" : "bottom";
|
|
6936
|
+
tooltip.style.left = `${Math.min(widget.clientWidth - edgeInset, Math.max(edgeInset, centeredLeft))}px`;
|
|
6937
|
+
tooltip.style.top = fitsAbove
|
|
6938
|
+
? `${cellRect.top - widgetRect.top - 8}px`
|
|
6939
|
+
: `${cellRect.bottom - widgetRect.top + 8}px`;
|
|
6940
|
+
};
|
|
6941
|
+
widget.querySelectorAll("button.usage-calendar-cell").forEach((cell) => {
|
|
6942
|
+
cell.addEventListener("mouseenter", () => showTooltip(cell));
|
|
6943
|
+
cell.addEventListener("mouseleave", () => {
|
|
6944
|
+
if (document.activeElement !== cell) hideTooltip();
|
|
6945
|
+
});
|
|
6946
|
+
cell.addEventListener("focus", () => showTooltip(cell));
|
|
6947
|
+
cell.addEventListener("blur", () => {
|
|
6948
|
+
if (!cell.matches(":hover")) hideTooltip();
|
|
6949
|
+
});
|
|
6950
|
+
cell.addEventListener("click", () => showTooltip(cell));
|
|
6951
|
+
cell.addEventListener("keydown", (event) => {
|
|
6952
|
+
if (event.key !== "Escape") return;
|
|
6953
|
+
hideTooltip();
|
|
6954
|
+
cell.blur();
|
|
6955
|
+
});
|
|
6956
|
+
});
|
|
6957
|
+
calendarScroll.addEventListener("scroll", () => {
|
|
6958
|
+
if (activeCell && !tooltip.hidden) showTooltip(activeCell);
|
|
6959
|
+
});
|
|
6960
|
+
});
|
|
6961
|
+
}
|
|
6962
|
+
|
|
6578
6963
|
function scrollUsageCalendarsToLatest(root) {
|
|
6579
6964
|
window.requestAnimationFrame(() => {
|
|
6580
6965
|
root.querySelectorAll(".usage-calendar-scroll").forEach((calendar) => {
|
|
@@ -6633,6 +7018,7 @@ async function renderPlatformTokenUsage() {
|
|
|
6633
7018
|
description: "汇总所有作品迄今产生的输入与输出 Token;缓存命中率仅基于供应商返回了缓存明细的调用。",
|
|
6634
7019
|
showWorks: true
|
|
6635
7020
|
});
|
|
7021
|
+
bindUsageCalendarInteractions(host);
|
|
6636
7022
|
scrollUsageCalendarsToLatest(host);
|
|
6637
7023
|
}
|
|
6638
7024
|
|
|
@@ -6652,10 +7038,20 @@ async function renderBookAiSettings() {
|
|
|
6652
7038
|
const host = $("#module-content");
|
|
6653
7039
|
const workId = String(state.work.id);
|
|
6654
7040
|
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts"]);
|
|
7041
|
+
const dailyTokenQuota = settings.dailyTokenQuota === null ? null : Number(settings.dailyTokenQuota);
|
|
7042
|
+
const quotaUsedTokens = Number(usage?.quota?.usedTokens) || 0;
|
|
7043
|
+
const quotaRemainingTokens = usage?.quota?.remainingTokens === null
|
|
7044
|
+
? null
|
|
7045
|
+
: Math.max(0, Number(usage?.quota?.remainingTokens) || 0);
|
|
7046
|
+
const quotaTimezone = String(usage?.quota?.timezone || "后端部署时区");
|
|
7047
|
+
const quotaStatusText = dailyTokenQuota === null
|
|
7048
|
+
? `今日已使用 ${quotaUsedTokens.toLocaleString("zh-CN")} Token,当前未启用额度限制。`
|
|
7049
|
+
: `今日已使用 ${quotaUsedTokens.toLocaleString("zh-CN")} / ${dailyTokenQuota.toLocaleString("zh-CN")} Token,剩余 ${Number(quotaRemainingTokens).toLocaleString("zh-CN")} Token。`;
|
|
6655
7050
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
6656
7051
|
title: "本书 Token 用量",
|
|
6657
7052
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
6658
|
-
})}</section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>对话 context 使用独立预算。达到该百分比阈值时先提醒;继续发送会对较早消息执行 compact,压缩上下文占用,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p
|
|
7053
|
+
})}</section><section class="config-section"><div class="config-section-header"><div><h2>每日 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然日可使用的输入与输出 Token 总量。额度最低为 10,000;达到额度后,新的 AI 请求会等到后端时区的次日零点重置后再执行。</p></div></div><div class="config-inline-save"><label><input id="daily-token-quota-enabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}>启用每日额度</label><label class="daily-token-quota-field">每日额度<input id="daily-token-quota" type="number" min="10000" max="2000000000" step="1000" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="本书每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label><button id="save-daily-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="daily-token-quota-status" class="usage-measurement-note" role="status">${esc(quotaStatusText)}</p></section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>对话 context 使用独立预算。达到该百分比阈值时先提醒;继续发送会对较早消息执行 compact,压缩上下文占用,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>Agent 工具调用上限</h2><p>限制单次回答里 Agent 可调用工具的次数,并用「全局倍数」给整次回答加一道不会因 Compact 重置的熔断阀,防止工具死循环空耗 Token。调用上限 5–48(默认 12);全局倍数 1–6(默认 3,全局上限 = 调用上限 × 倍数)。<a class="config-doc-link" href="https://scriverse.top/docs/global-tool-call-limit.html" target="_blank" rel="noopener noreferrer">了解原理与推荐设置</a></p></div></div><div class="config-inline-save"><label class="agent-tool-call-limit-field">调用上限<input id="agent-tool-call-limit" type="number" min="5" max="48" value="${esc(String(settings.agentToolCallLimit ?? 12))}" aria-label="Agent 工具调用上限"></label><div class="agent-tool-call-global-multiplier-field"><span id="agent-tool-call-global-multiplier-label">全局倍数</span><div class="settings-layout-toggle agent-tool-call-global-multiplier-toggle" role="group" aria-labelledby="agent-tool-call-global-multiplier-label">${[1, 2, 3, 4, 5, 6].map((value) => `<button type="button" data-global-multiplier="${value}" aria-pressed="${Number(settings.agentToolCallGlobalMultiplier ?? 3) === value}">${value}</button>`).join("")}</div><input id="agent-tool-call-global-multiplier" type="hidden" value="${esc(String(Math.min(6, Math.max(1, Number(settings.agentToolCallGlobalMultiplier ?? 3) || 3))))}" aria-label="Agent 工具调用全局倍数"></div><button id="save-agent-tool-call-limit" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。已开始的对话会锁定创建时的工具集,修改后仅对新对话生效,避免打断 prompt cache。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名、拼音或短关键词混合检索设定、人物、组织、时间线、关系、大纲和伏笔;非语义问答。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults, settings)}`;
|
|
7054
|
+
bindUsageCalendarInteractions(host);
|
|
6659
7055
|
scrollUsageCalendarsToLatest(host);
|
|
6660
7056
|
host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
|
|
6661
7057
|
"beforebegin",
|
|
@@ -6671,6 +7067,7 @@ async function renderBookAiSettings() {
|
|
|
6671
7067
|
);
|
|
6672
7068
|
if (!canEditModule("ai-settings")) {
|
|
6673
7069
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
7070
|
+
host.querySelectorAll(".agent-tool-call-global-multiplier-toggle button").forEach((button) => { button.disabled = true; });
|
|
6674
7071
|
host.querySelectorAll(".config-save-button").forEach((button) => button.classList.add("permission-hidden"));
|
|
6675
7072
|
}
|
|
6676
7073
|
const isCurrentRelationshipIndexPanel = () => state.module === "ai-settings"
|
|
@@ -6700,6 +7097,31 @@ async function renderBookAiSettings() {
|
|
|
6700
7097
|
return status;
|
|
6701
7098
|
};
|
|
6702
7099
|
updateRelationshipIndexStatus(relationshipIndex);
|
|
7100
|
+
$("#daily-token-quota-enabled").addEventListener("change", (event) => {
|
|
7101
|
+
$("#daily-token-quota").disabled = !event.currentTarget.checked;
|
|
7102
|
+
});
|
|
7103
|
+
$("#save-daily-token-quota").addEventListener("click", async () => {
|
|
7104
|
+
const button = $("#save-daily-token-quota");
|
|
7105
|
+
const enabled = $("#daily-token-quota-enabled").checked;
|
|
7106
|
+
const quota = Number($("#daily-token-quota").value);
|
|
7107
|
+
if (enabled && (!Number.isInteger(quota) || quota < 10_000 || quota > 2_000_000_000)) {
|
|
7108
|
+
toast("每日 Token 额度必须是 10,000 到 2,000,000,000 之间的整数", "error");
|
|
7109
|
+
$("#daily-token-quota").focus();
|
|
7110
|
+
return;
|
|
7111
|
+
}
|
|
7112
|
+
button.disabled = true;
|
|
7113
|
+
try {
|
|
7114
|
+
await api(`/api/works/${state.work.id}/ai-settings`, {
|
|
7115
|
+
method: "PATCH",
|
|
7116
|
+
body: { dailyTokenQuota: enabled ? quota : null }
|
|
7117
|
+
});
|
|
7118
|
+
toast(enabled ? "本书每日 Token 额度已保存" : "本书每日 Token 额度限制已关闭");
|
|
7119
|
+
await renderBookAiSettings();
|
|
7120
|
+
} catch (error) {
|
|
7121
|
+
toast(error.message, "error");
|
|
7122
|
+
button.disabled = false;
|
|
7123
|
+
}
|
|
7124
|
+
});
|
|
6703
7125
|
$("#save-work-system-prompt").addEventListener("click", async () => {
|
|
6704
7126
|
const button = $("#save-work-system-prompt");
|
|
6705
7127
|
button.disabled = true;
|
|
@@ -6779,6 +7201,34 @@ async function renderBookAiSettings() {
|
|
|
6779
7201
|
button.disabled = false;
|
|
6780
7202
|
}
|
|
6781
7203
|
});
|
|
7204
|
+
$("#save-agent-tool-call-limit").addEventListener("click", async () => {
|
|
7205
|
+
const button = $("#save-agent-tool-call-limit");
|
|
7206
|
+
button.disabled = true;
|
|
7207
|
+
try {
|
|
7208
|
+
await api(`/api/works/${state.work.id}/ai-settings`, {
|
|
7209
|
+
method: "PATCH",
|
|
7210
|
+
body: {
|
|
7211
|
+
agentToolCallLimit: Number($("#agent-tool-call-limit").value),
|
|
7212
|
+
agentToolCallGlobalMultiplier: Number($("#agent-tool-call-global-multiplier").value)
|
|
7213
|
+
}
|
|
7214
|
+
});
|
|
7215
|
+
toast("Agent 工具调用上限已保存");
|
|
7216
|
+
} catch (error) {
|
|
7217
|
+
toast(error.message, "error");
|
|
7218
|
+
} finally {
|
|
7219
|
+
button.disabled = false;
|
|
7220
|
+
}
|
|
7221
|
+
});
|
|
7222
|
+
host.querySelector(".agent-tool-call-global-multiplier-toggle")?.addEventListener("click", (event) => {
|
|
7223
|
+
const option = event.target.closest("button[data-global-multiplier]");
|
|
7224
|
+
if (!option || option.disabled) return;
|
|
7225
|
+
const value = option.getAttribute("data-global-multiplier");
|
|
7226
|
+
const hidden = $("#agent-tool-call-global-multiplier");
|
|
7227
|
+
if (hidden) hidden.value = value;
|
|
7228
|
+
host.querySelectorAll(".agent-tool-call-global-multiplier-toggle button[data-global-multiplier]").forEach((item) => {
|
|
7229
|
+
item.setAttribute("aria-pressed", String(item === option));
|
|
7230
|
+
});
|
|
7231
|
+
});
|
|
6782
7232
|
$("#save-agent-tools").addEventListener("click", async () => {
|
|
6783
7233
|
const button = $("#save-agent-tools");
|
|
6784
7234
|
button.disabled = true;
|
|
@@ -6917,10 +7367,12 @@ function renderAiContextDistribution(usage) {
|
|
|
6917
7367
|
}
|
|
6918
7368
|
|
|
6919
7369
|
function setAiContextMeter(usage) {
|
|
7370
|
+
const displayUsage = resolveAiContextUsage(latestAiContextUsage, usage);
|
|
7371
|
+
latestAiContextUsage = displayUsage;
|
|
6920
7372
|
const meter = $("#ai-context-meter");
|
|
6921
7373
|
const value = meter.querySelector("b");
|
|
6922
|
-
renderAiContextDistribution(
|
|
6923
|
-
if (!
|
|
7374
|
+
renderAiContextDistribution(displayUsage);
|
|
7375
|
+
if (!displayUsage) {
|
|
6924
7376
|
meter.classList.add("is-empty");
|
|
6925
7377
|
meter.classList.remove("is-warning", "is-danger");
|
|
6926
7378
|
meter.style.setProperty("--context-usage", "0");
|
|
@@ -6930,17 +7382,22 @@ function setAiContextMeter(usage) {
|
|
|
6930
7382
|
meter.setAttribute("aria-label", tooltip);
|
|
6931
7383
|
return;
|
|
6932
7384
|
}
|
|
6933
|
-
const percent = Math.max(0, Math.min(100, Number(
|
|
7385
|
+
const percent = Math.max(0, Math.min(100, Number(displayUsage.usagePercent) || 0));
|
|
6934
7386
|
meter.classList.remove("is-empty");
|
|
6935
7387
|
meter.classList.toggle("is-warning", percent >= 70 && percent < 90);
|
|
6936
7388
|
meter.classList.toggle("is-danger", percent >= 90);
|
|
6937
7389
|
meter.style.setProperty("--context-usage", String(percent));
|
|
6938
7390
|
value.textContent = `${percent}%`;
|
|
6939
|
-
const tooltip = formatAiContextUsageTooltip(
|
|
7391
|
+
const tooltip = formatAiContextUsageTooltip(displayUsage);
|
|
6940
7392
|
meter.dataset.tooltip = tooltip;
|
|
6941
7393
|
meter.setAttribute("aria-label", `当前上下文用量:${tooltip}`);
|
|
6942
7394
|
}
|
|
6943
7395
|
|
|
7396
|
+
function resetAiContextMeter() {
|
|
7397
|
+
latestAiContextUsage = null;
|
|
7398
|
+
setAiContextMeter(null);
|
|
7399
|
+
}
|
|
7400
|
+
|
|
6944
7401
|
function setAiContextDistributionVisible(visible) {
|
|
6945
7402
|
const meter = $("#ai-context-meter");
|
|
6946
7403
|
const popover = $("#ai-context-popover");
|
|
@@ -7300,9 +7757,39 @@ function bindWorkCoverControls(work) {
|
|
|
7300
7757
|
});
|
|
7301
7758
|
}
|
|
7302
7759
|
|
|
7303
|
-
function downloadWorkManuscript(work) {
|
|
7760
|
+
function downloadWorkManuscript(work, format = "markdown") {
|
|
7304
7761
|
if (!work?.id) return;
|
|
7305
|
-
|
|
7762
|
+
const exportFormat = format === "docx" ? "docx" : "markdown";
|
|
7763
|
+
window.location.href = `/api/works/${encodeURIComponent(work.id)}/export?format=${exportFormat}`;
|
|
7764
|
+
}
|
|
7765
|
+
|
|
7766
|
+
let manuscriptExportWork = null;
|
|
7767
|
+
|
|
7768
|
+
function closeManuscriptExportMenu() {
|
|
7769
|
+
const menu = $("#manuscript-export-menu");
|
|
7770
|
+
if (!menu) return;
|
|
7771
|
+
menu.classList.add("hidden");
|
|
7772
|
+
manuscriptExportWork = null;
|
|
7773
|
+
$("#export-button")?.setAttribute("aria-expanded", "false");
|
|
7774
|
+
$("#work-export-button")?.setAttribute("aria-expanded", "false");
|
|
7775
|
+
}
|
|
7776
|
+
|
|
7777
|
+
function showManuscriptExportMenu(anchor, work) {
|
|
7778
|
+
if (!work?.id || !anchor) return;
|
|
7779
|
+
const menu = $("#manuscript-export-menu");
|
|
7780
|
+
if (!menu) return;
|
|
7781
|
+
manuscriptExportWork = work;
|
|
7782
|
+
menu.classList.remove("hidden");
|
|
7783
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
7784
|
+
const menuRect = menu.getBoundingClientRect();
|
|
7785
|
+
const left = Math.max(8, Math.min(anchorRect.left, window.innerWidth - menuRect.width - 8));
|
|
7786
|
+
const top = Math.max(8, Math.min(anchorRect.bottom + 6, window.innerHeight - menuRect.height - 8));
|
|
7787
|
+
menu.style.left = `${left}px`;
|
|
7788
|
+
menu.style.top = `${top}px`;
|
|
7789
|
+
if (anchor.id === "export-button" || anchor.id === "work-export-button") {
|
|
7790
|
+
anchor.setAttribute("aria-expanded", "true");
|
|
7791
|
+
}
|
|
7792
|
+
menu.querySelector("button[data-export-format]")?.focus();
|
|
7306
7793
|
}
|
|
7307
7794
|
|
|
7308
7795
|
function openWorkSettingsDialog(work) {
|
|
@@ -7322,8 +7809,8 @@ function openWorkSettingsDialog(work) {
|
|
|
7322
7809
|
<button id="import-history-button" class="ghost-button" type="button" aria-controls="import-history-dialog" aria-haspopup="dialog" ${canOpenImportHistory ? "" : "disabled"}>${importHistoryAction}</button>
|
|
7323
7810
|
</section>`;
|
|
7324
7811
|
const exportField = `<section class="work-access-field" aria-labelledby="work-export-settings-title">
|
|
7325
|
-
<div><strong id="work-export-settings-title">导出正文</strong><small
|
|
7326
|
-
<button id="work-export-button" class="ghost-button" type="button"
|
|
7812
|
+
<div><strong id="work-export-settings-title">导出正文</strong><small>点击后选择导出 Markdown ZIP 或 DOCX(书名、分卷、章节为一级至三级标题;若已设置封面则嵌入为首页)。不包含角色、设定、关系、时间轴、大纲、伏笔或 AI 分析资料。</small></div>
|
|
7813
|
+
<button id="work-export-button" class="ghost-button" type="button" aria-haspopup="menu" aria-controls="manuscript-export-menu" aria-expanded="false">导出正文</button>
|
|
7327
7814
|
</section>`;
|
|
7328
7815
|
const recycleBinField = isCurrentWork ? `<section class="work-access-field" aria-labelledby="chapter-recycle-bin-settings-title">
|
|
7329
7816
|
<div><strong id="chapter-recycle-bin-settings-title">章节回收站</strong><small>恢复已软删除的章节,或彻底删除正文、版本和关联资料。</small></div>
|
|
@@ -7357,7 +7844,11 @@ function openWorkSettingsDialog(work) {
|
|
|
7357
7844
|
$("#form-dialog").close();
|
|
7358
7845
|
void openImportHistory();
|
|
7359
7846
|
});
|
|
7360
|
-
$("#work-export-button")?.addEventListener("click", () =>
|
|
7847
|
+
$("#work-export-button")?.addEventListener("click", (event) => {
|
|
7848
|
+
event.preventDefault();
|
|
7849
|
+
event.stopPropagation();
|
|
7850
|
+
showManuscriptExportMenu(event.currentTarget, work);
|
|
7851
|
+
});
|
|
7361
7852
|
$("#chapter-recycle-bin-button")?.addEventListener("click", () => {
|
|
7362
7853
|
$("#form-dialog").close();
|
|
7363
7854
|
void openChapterRecycleBin();
|
|
@@ -9295,6 +9786,7 @@ async function sendAi() {
|
|
|
9295
9786
|
try {
|
|
9296
9787
|
await ensureAiModelsLoaded();
|
|
9297
9788
|
} catch (error) {
|
|
9789
|
+
setAiAssistantStatus("error");
|
|
9298
9790
|
return toast(`创作助手加载失败:${error.message}`, "error");
|
|
9299
9791
|
}
|
|
9300
9792
|
const modelId = $("#ai-model").value;
|
|
@@ -9305,12 +9797,14 @@ async function sendAi() {
|
|
|
9305
9797
|
if (!requestScope) return toast("请先选择章节", "error");
|
|
9306
9798
|
const { taskType, scope, selection } = requestScope;
|
|
9307
9799
|
if (taskType === "polish" && !selection) return toast("请先在正文中选中一段文本", "error");
|
|
9800
|
+
setAiAssistantStatus("ready");
|
|
9308
9801
|
const citations = state.aiCitations.map(({ chapterId, chapterTitle, startLine, endLine, text }) => ({ chapterId, chapterTitle, startLine, endLine, text }));
|
|
9309
9802
|
let persistedUserMessage = null;
|
|
9310
9803
|
if (taskType !== "chat") {
|
|
9311
9804
|
try {
|
|
9312
9805
|
persistedUserMessage = await persistAiConversationMessage("user", instruction, citations);
|
|
9313
9806
|
} catch (error) {
|
|
9807
|
+
setAiAssistantStatus("error");
|
|
9314
9808
|
return toast(`对话记录创建失败:${error.message}`, "error");
|
|
9315
9809
|
}
|
|
9316
9810
|
state.aiPromptSent = true;
|
|
@@ -9336,6 +9830,10 @@ async function sendAi() {
|
|
|
9336
9830
|
applyAiConversationTitle(streamed.conversationTitle);
|
|
9337
9831
|
} else {
|
|
9338
9832
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
9833
|
+
const suggestionFailed = suggestion.guard?.status === "failed"
|
|
9834
|
+
|| suggestion.toolCalls?.some((toolCall) => toolCall.status === "failed")
|
|
9835
|
+
|| suggestion.processSteps?.some((step) => step?.toolCall?.status === "failed");
|
|
9836
|
+
if (suggestionFailed) setAiAssistantStatus("error");
|
|
9339
9837
|
setAiContextMeter(suggestion.contextUsage);
|
|
9340
9838
|
assistantContent = suggestion.content;
|
|
9341
9839
|
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
|
|
@@ -9358,10 +9856,12 @@ async function sendAi() {
|
|
|
9358
9856
|
} else if (suggestion) appendSuggestion(suggestion, persistedAssistantMessage.createdAt, persistedAssistantMessage.id);
|
|
9359
9857
|
}
|
|
9360
9858
|
} catch (error) {
|
|
9859
|
+
setAiAssistantStatus("error");
|
|
9361
9860
|
if (suggestion) appendSuggestion(suggestion);
|
|
9362
9861
|
toast(`AI 回复已生成,但历史记录保存失败:${error.message}`, "error");
|
|
9363
9862
|
}
|
|
9364
9863
|
} catch (error) {
|
|
9864
|
+
setAiAssistantStatus("error");
|
|
9365
9865
|
const failureMessage = formatAiFailureMessage(error);
|
|
9366
9866
|
let persistedFailureMessage = null;
|
|
9367
9867
|
try { persistedFailureMessage = await persistAiConversationMessage("assistant", failureMessage); } catch { /* 主请求错误已显示,历史记录保存失败不覆盖原始错误 */ }
|
|
@@ -9489,6 +9989,7 @@ async function streamChat(body) {
|
|
|
9489
9989
|
const toolCall = { ...payload };
|
|
9490
9990
|
const round = toolCall.round;
|
|
9491
9991
|
delete toolCall.round;
|
|
9992
|
+
if (toolCall.status === "failed") setAiAssistantStatus("error");
|
|
9492
9993
|
toolCalls.push(toolCall);
|
|
9493
9994
|
processSteps.push(aiToolProcessStep(toolCall, round));
|
|
9494
9995
|
renderAiProcessSteps(message, processSteps, finalAnswerStarted, elapsedProcessTime());
|
|
@@ -9516,6 +10017,7 @@ async function streamChat(body) {
|
|
|
9516
10017
|
attachAssistantCopyAction(message, streamedText);
|
|
9517
10018
|
scrollAiFeedToBottom();
|
|
9518
10019
|
} else if (eventName === "error") {
|
|
10020
|
+
setAiAssistantStatus("error");
|
|
9519
10021
|
streamError = createClientError(payload, "AI 流式调用失败", response.status);
|
|
9520
10022
|
}
|
|
9521
10023
|
};
|
|
@@ -9960,6 +10462,28 @@ $("#onboarding-dialog").addEventListener("cancel", (event) => {
|
|
|
9960
10462
|
event.preventDefault();
|
|
9961
10463
|
completeOnboarding();
|
|
9962
10464
|
});
|
|
10465
|
+
$("#system-restart-dialog").addEventListener("cancel", (event) => {
|
|
10466
|
+
event.preventDefault();
|
|
10467
|
+
});
|
|
10468
|
+
function hasUnsavedEditorChanges() {
|
|
10469
|
+
return state.dirty || entityEditorDirty || characterSectionEditorDirty || knowledgeSectionEditorDirty;
|
|
10470
|
+
}
|
|
10471
|
+
|
|
10472
|
+
function redirectToLoginAfterSystemRestart() {
|
|
10473
|
+
state.user = null;
|
|
10474
|
+
state.csrfToken = null;
|
|
10475
|
+
moduleRequestCache.clear();
|
|
10476
|
+
document.documentElement.classList.remove("dev-auth-bypass");
|
|
10477
|
+
document.documentElement.classList.add("login-route");
|
|
10478
|
+
window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
10479
|
+
const toastRegion = $("#toast-region");
|
|
10480
|
+
toastRegion.replaceChildren();
|
|
10481
|
+
if (typeof toastRegion.hidePopover === "function" && toastRegion.matches(":popover-open")) toastRegion.hidePopover();
|
|
10482
|
+
$("#system-restart-dialog").close();
|
|
10483
|
+
showAuth(false);
|
|
10484
|
+
}
|
|
10485
|
+
|
|
10486
|
+
$("#system-restart-confirm").addEventListener("click", redirectToLoginAfterSystemRestart);
|
|
9963
10487
|
$("#onboarding-dialog").addEventListener("keydown", (event) => {
|
|
9964
10488
|
if (event.key === "Escape") {
|
|
9965
10489
|
event.preventDefault();
|
|
@@ -10498,10 +11022,20 @@ $("#writing-progress-button").addEventListener("click", () => openWritingProgres
|
|
|
10498
11022
|
$("#writing-progress-close").addEventListener("click", () => $("#writing-progress-dialog").close());
|
|
10499
11023
|
$("#writing-progress-refresh").addEventListener("click", () => loadWritingProgress().catch((error) => toast(error.message, "error")));
|
|
10500
11024
|
$("#writing-goal-form").addEventListener("submit", saveWritingGoal);
|
|
10501
|
-
$("#work-audit-button").addEventListener("click", () =>
|
|
10502
|
-
$("#work-audit-
|
|
10503
|
-
$("#work-audit-
|
|
10504
|
-
$("#work-audit-refresh")
|
|
11025
|
+
$("#work-audit-button").addEventListener("click", () => showWorkAudit().catch((error) => toast(error.message, "error")));
|
|
11026
|
+
$("#work-audit-return").addEventListener("click", () => returnToSettingsHub("#work-audit-button").catch((error) => toast(error.message, "error")));
|
|
11027
|
+
$("#work-audit-refresh").addEventListener("click", async () => {
|
|
11028
|
+
const button = $("#work-audit-refresh");
|
|
11029
|
+
button.disabled = true;
|
|
11030
|
+
try {
|
|
11031
|
+
await loadWorkAuditPage();
|
|
11032
|
+
toast("操作记录已刷新");
|
|
11033
|
+
} catch (error) {
|
|
11034
|
+
toast(error.message, "error");
|
|
11035
|
+
} finally {
|
|
11036
|
+
button.disabled = false;
|
|
11037
|
+
}
|
|
11038
|
+
});
|
|
10505
11039
|
$("#work-audit-load-more").addEventListener("click", () => {
|
|
10506
11040
|
if (workAuditNextPage !== null) loadWorkAuditPage(workAuditNextPage, true).catch((error) => toast(error.message, "error"));
|
|
10507
11041
|
});
|
|
@@ -10564,6 +11098,7 @@ $("#form-dialog").addEventListener("close", () => {
|
|
|
10564
11098
|
formDialogVditors.forEach(destroyVditorEditor);
|
|
10565
11099
|
formDialogVditors = [];
|
|
10566
11100
|
void discardPendingMarkdownAttachments();
|
|
11101
|
+
closeManuscriptExportMenu();
|
|
10567
11102
|
if (relationshipPresenceId && !$("#form-dialog").open) setRelationshipPresence(null);
|
|
10568
11103
|
});
|
|
10569
11104
|
$("#member-user-select").addEventListener("change", () => selectMemberForConfiguration($("#member-user-select").value));
|
|
@@ -10923,6 +11458,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
10923
11458
|
if (!event.target.closest("#chapter-type-menu")) closeChapterTypeMenu();
|
|
10924
11459
|
if (!event.target.closest("#line-citation-menu")) closeLineCitationMenu();
|
|
10925
11460
|
if (!event.target.closest("#markdown-table-menu")) closeMarkdownTableMenu();
|
|
11461
|
+
if (!event.target.closest("#manuscript-export-menu") && !event.target.closest("#export-button") && !event.target.closest("#work-export-button")) {
|
|
11462
|
+
closeManuscriptExportMenu();
|
|
11463
|
+
}
|
|
10926
11464
|
if (!event.target.closest(".prompt-composer")) hideAiMentionMenu();
|
|
10927
11465
|
if (!event.target.closest("#ai-context-meter") && !event.target.closest("#ai-context-popover")) setAiContextDistributionVisible(false);
|
|
10928
11466
|
if (!event.target.closest("#account-button") && !event.target.closest("#account-menu")) {
|
|
@@ -10945,6 +11483,7 @@ document.addEventListener("keydown", (event) => {
|
|
|
10945
11483
|
closeChapterTypeMenu();
|
|
10946
11484
|
closeLineCitationMenu();
|
|
10947
11485
|
closeMarkdownTableMenu(true);
|
|
11486
|
+
closeManuscriptExportMenu();
|
|
10948
11487
|
hideAiMentionMenu();
|
|
10949
11488
|
setAiContextDistributionVisible(false);
|
|
10950
11489
|
}
|
|
@@ -11097,8 +11636,39 @@ $("#search-form").addEventListener("submit", async (event) => {
|
|
|
11097
11636
|
$("#search-results").innerHTML = `<p class="search-results-status">${esc(error.message)}</p>`;
|
|
11098
11637
|
});
|
|
11099
11638
|
});
|
|
11100
|
-
$("#export-button").addEventListener("click", () =>
|
|
11101
|
-
|
|
11639
|
+
$("#export-button").addEventListener("click", (event) => {
|
|
11640
|
+
event.preventDefault();
|
|
11641
|
+
event.stopPropagation();
|
|
11642
|
+
if (!state.work) return;
|
|
11643
|
+
const menu = $("#manuscript-export-menu");
|
|
11644
|
+
const expanded = menu && !menu.classList.contains("hidden") && manuscriptExportWork?.id === state.work.id;
|
|
11645
|
+
if (expanded) {
|
|
11646
|
+
closeManuscriptExportMenu();
|
|
11647
|
+
return;
|
|
11648
|
+
}
|
|
11649
|
+
showManuscriptExportMenu(event.currentTarget, state.work);
|
|
11650
|
+
});
|
|
11651
|
+
$("#manuscript-export-menu").addEventListener("click", (event) => {
|
|
11652
|
+
const option = event.target.closest("[data-export-format]");
|
|
11653
|
+
if (!option || !manuscriptExportWork) return;
|
|
11654
|
+
const format = option.getAttribute("data-export-format") === "docx" ? "docx" : "markdown";
|
|
11655
|
+
const work = manuscriptExportWork;
|
|
11656
|
+
closeManuscriptExportMenu();
|
|
11657
|
+
downloadWorkManuscript(work, format);
|
|
11658
|
+
});
|
|
11659
|
+
document.addEventListener("visibilitychange", () => {
|
|
11660
|
+
if (document.visibilityState !== "visible") return;
|
|
11661
|
+
if (state.user && !systemRestartDetected) scheduleSystemBootCheck(0);
|
|
11662
|
+
void refreshSystemHealth();
|
|
11663
|
+
});
|
|
11664
|
+
window.addEventListener("beforeunload", (event) => {
|
|
11665
|
+
if (hasUnsavedEditorChanges()) event.preventDefault();
|
|
11666
|
+
});
|
|
11667
|
+
window.addEventListener("online", () => {
|
|
11668
|
+
updateSystemHealth({ status: "checking" });
|
|
11669
|
+
void refreshSystemHealth();
|
|
11670
|
+
});
|
|
11671
|
+
window.addEventListener("offline", () => updateSystemHealth({ status: "offline" }));
|
|
11102
11672
|
|
|
11103
11673
|
initializePage().catch((error) => {
|
|
11104
11674
|
restoringPageRoute = false;
|