@musnows/scriverse 0.6.8 → 0.6.10
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 +13 -1
- package/dist/ai-tool-results.js.map +1 -1
- package/dist/ai.js +76 -27
- package/dist/ai.js.map +1 -1
- package/dist/app.js +94 -6
- package/dist/app.js.map +1 -1
- package/dist/database.js +295 -5
- package/dist/database.js.map +1 -1
- package/dist/hybrid-search.js +2 -1
- package/dist/hybrid-search.js.map +1 -1
- package/dist/logger.js +2 -2
- package/dist/logger.js.map +1 -1
- package/dist/public/app.js +368 -15
- package/dist/public/display-labels.js +2 -1
- package/dist/public/global-search.d.ts +3 -1
- package/dist/public/global-search.js +31 -0
- package/dist/public/index.html +48 -3
- package/dist/public/s3-backup-ui.d.ts +17 -0
- package/dist/public/s3-backup-ui.js +28 -0
- package/dist/public/styles.css +86 -2
- package/dist/s3-backup.js +654 -0
- package/dist/s3-backup.js.map +1 -0
- package/dist/security.js +25 -0
- package/dist/security.js.map +1 -1
- package/dist/store.js +74 -15
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +6 -2
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -1
package/dist/public/app.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
taskScopeLabel,
|
|
37
37
|
timelineStatusLabel,
|
|
38
38
|
characterStateFieldLabel
|
|
39
|
-
} from "/display-labels.js?v=
|
|
39
|
+
} from "/display-labels.js?v=20260804-agent-history-search-v1";
|
|
40
40
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260731-work-comments-v2";
|
|
41
41
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
42
42
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
@@ -45,7 +45,7 @@ import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20
|
|
|
45
45
|
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260731-drafts-to-ideas-v1";
|
|
46
46
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
47
47
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
48
|
-
import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=
|
|
48
|
+
import { prioritizeGlobalSearchResults, resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260804-agent-history-score-sort-v1";
|
|
49
49
|
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
50
50
|
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
51
51
|
import {
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
|
|
58
58
|
import { createModuleRequestCache } from "/module-request-cache.js?v=20260730-module-request-cache-v1";
|
|
59
59
|
import { systemStatusPresentation } from "/system-status.js?v=20260801-system-health-v1";
|
|
60
|
+
import { collectS3BackupRunTransitions, s3BackupFailureToast, s3BackupRootPrefix, s3BackupStatusLabel } from "/s3-backup-ui.js?v=20260804-s3-backup-v1";
|
|
60
61
|
import {
|
|
61
62
|
clampCropRect,
|
|
62
63
|
containImageRect,
|
|
@@ -195,6 +196,14 @@ let backgroundTaskCenterWorkId = null;
|
|
|
195
196
|
let backgroundTaskCenterTasksInitialized = false;
|
|
196
197
|
let backgroundTaskCenterTaskSnapshots = new Map();
|
|
197
198
|
let backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
199
|
+
let s3BackupTargets = [];
|
|
200
|
+
let s3BackupRuns = [];
|
|
201
|
+
let editingS3BackupTargetId = null;
|
|
202
|
+
let s3BackupPollTimer = null;
|
|
203
|
+
let s3BackupPollRequest = 0;
|
|
204
|
+
let s3BackupRunsInitialized = false;
|
|
205
|
+
let s3BackupRunSnapshots = new Map();
|
|
206
|
+
const s3BackupPollInterval = 8_000;
|
|
198
207
|
let productUpdateStatus = null;
|
|
199
208
|
let productUpdateChecking = false;
|
|
200
209
|
let productUpdateTimer = null;
|
|
@@ -1812,8 +1821,10 @@ async function ensureAiConversationsLoaded() {
|
|
|
1812
1821
|
}
|
|
1813
1822
|
}
|
|
1814
1823
|
|
|
1815
|
-
async function openAiConversation(conversationId, hideHistory = true) {
|
|
1816
|
-
const
|
|
1824
|
+
async function openAiConversation(conversationId, hideHistory = true, focusMessageId = null) {
|
|
1825
|
+
const parameters = new URLSearchParams({ page: "1", limit: "100" });
|
|
1826
|
+
if (focusMessageId) parameters.set("messageId", String(focusMessageId));
|
|
1827
|
+
const conversation = await api(`/api/ai-conversations/${conversationId}?${parameters}`);
|
|
1817
1828
|
upsertAiConversationSummary(conversation);
|
|
1818
1829
|
state.aiConversationId = conversation.id;
|
|
1819
1830
|
state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
|
|
@@ -1839,6 +1850,15 @@ async function openAiConversation(conversationId, hideHistory = true) {
|
|
|
1839
1850
|
if (hideHistory) setAiHistoryVisible(false);
|
|
1840
1851
|
}
|
|
1841
1852
|
|
|
1853
|
+
function focusAiConversationMessage(messageId) {
|
|
1854
|
+
const message = [...$("#ai-feed").querySelectorAll("[data-message-id]")]
|
|
1855
|
+
.find((candidate) => candidate.dataset.messageId === String(messageId));
|
|
1856
|
+
if (!message) return;
|
|
1857
|
+
message.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1858
|
+
message.classList.add("is-search-target");
|
|
1859
|
+
window.setTimeout(() => message.classList.remove("is-search-target"), 1800);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1842
1862
|
async function createNewAiConversation(taskType = "chat") {
|
|
1843
1863
|
if (!state.work) return;
|
|
1844
1864
|
const conversation = await api(`/api/works/${state.work.id}/ai-conversations`, { method: "POST", body: { taskType } });
|
|
@@ -2571,6 +2591,28 @@ function formatAiFailureMessage(error) {
|
|
|
2571
2591
|
return lines.join("\n");
|
|
2572
2592
|
}
|
|
2573
2593
|
|
|
2594
|
+
function isAgentToolCallLimitFailure(text) {
|
|
2595
|
+
return /more than \d+ tool calls in one response cycle\./u.test(String(text ?? ""));
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
function aiToolCallSettingsLinkMarkup(text) {
|
|
2599
|
+
if (!isAgentToolCallLimitFailure(text) || !state.work || !canReadModule("ai-settings")) return "";
|
|
2600
|
+
const href = serializePageRoute({ view: "module", workId: state.work.id, module: "ai-settings" });
|
|
2601
|
+
return `<p class="ai-error-settings-link-wrap"><a class="ai-error-settings-link" data-ai-tool-call-settings-link href="${esc(href)}">前往本书 AI 设置调整工具调用上限</a></p>`;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
async function openAiToolCallSettings() {
|
|
2605
|
+
if (!state.work || !canReadModule("ai-settings")) return;
|
|
2606
|
+
await showModule("ai-settings");
|
|
2607
|
+
const target = document.getElementById("agent-tool-call-limit-settings");
|
|
2608
|
+
if (!target) return;
|
|
2609
|
+
target.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
|
|
2610
|
+
target.classList.add("is-targeted");
|
|
2611
|
+
window.setTimeout(() => target.classList.remove("is-targeted"), 1_800);
|
|
2612
|
+
const input = target.querySelector("#agent-tool-call-limit");
|
|
2613
|
+
if (input instanceof HTMLInputElement) input.focus({ preventScroll: true });
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2574
2616
|
async function apiPage(path, page = 1, limit = 30) {
|
|
2575
2617
|
const separator = path.includes("?") ? "&" : "?";
|
|
2576
2618
|
const result = await api(`${path}${separator}page=${page}&limit=${limit}`);
|
|
@@ -2846,6 +2888,7 @@ function clearAuthenticationOverlays() {
|
|
|
2846
2888
|
}
|
|
2847
2889
|
|
|
2848
2890
|
function invalidateAuthentication() {
|
|
2891
|
+
stopS3BackupEventPolling();
|
|
2849
2892
|
state.user = null;
|
|
2850
2893
|
state.csrfToken = null;
|
|
2851
2894
|
moduleRequestCache.clear();
|
|
@@ -2964,6 +3007,7 @@ async function initializeAuthentication() {
|
|
|
2964
3007
|
applyAuthenticatedUser(session);
|
|
2965
3008
|
scheduleSystemBootCheck();
|
|
2966
3009
|
await loadPlatformUiSettings();
|
|
3010
|
+
startS3BackupEventPolling();
|
|
2967
3011
|
return true;
|
|
2968
3012
|
}
|
|
2969
3013
|
|
|
@@ -3462,6 +3506,7 @@ function renderSettingsHub() {
|
|
|
3462
3506
|
$("#platform-usage-button").classList.toggle("hidden", !isAdmin);
|
|
3463
3507
|
$("#user-management-button").classList.toggle("hidden", !isAdmin);
|
|
3464
3508
|
$("#platform-ui-settings-button").classList.toggle("hidden", !isAdmin);
|
|
3509
|
+
$("#s3-backup-button").classList.toggle("hidden", !isAdmin);
|
|
3465
3510
|
$("#collaboration-button").disabled = !canManageWork;
|
|
3466
3511
|
$("#writing-progress-button").disabled = !hasWork || !canReadModule("editor");
|
|
3467
3512
|
$("#work-audit-button").disabled = !canManageWork;
|
|
@@ -3839,6 +3884,258 @@ async function openPlatformUiSettingsDialog() {
|
|
|
3839
3884
|
}
|
|
3840
3885
|
}
|
|
3841
3886
|
|
|
3887
|
+
function s3BackupTargetFailed(target) {
|
|
3888
|
+
const failedAt = Date.parse(target.lastFailureAt || "");
|
|
3889
|
+
const succeededAt = Date.parse(target.lastSuccessAt || "");
|
|
3890
|
+
return Number.isFinite(failedAt) && (!Number.isFinite(succeededAt) || failedAt > succeededAt);
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
function renderS3BackupTargets() {
|
|
3894
|
+
const enabledCount = s3BackupTargets.filter((target) => target.enabled).length;
|
|
3895
|
+
$("#s3-backup-summary").textContent = s3BackupTargets.length
|
|
3896
|
+
? `共 ${s3BackupTargets.length} 个目标,其中 ${enabledCount} 个已启用;定时任务使用服务器本地时间。`
|
|
3897
|
+
: "尚未配置备份目标。新增目标后可选择定时启用,也可随时手动执行。";
|
|
3898
|
+
$("#s3-backup-run-all").disabled = enabledCount === 0;
|
|
3899
|
+
const host = $("#s3-backup-targets");
|
|
3900
|
+
host.innerHTML = s3BackupTargets.length ? s3BackupTargets.map((target) => {
|
|
3901
|
+
const failed = s3BackupTargetFailed(target);
|
|
3902
|
+
const path = `${target.endpoint} / ${target.bucket} / ${target.rootPrefix}`;
|
|
3903
|
+
return `<article class="s3-backup-target-card${failed ? " is-failed" : ""}" data-s3-backup-target="${esc(target.id)}">
|
|
3904
|
+
<div class="s3-backup-target-heading"><span class="s3-backup-status ${target.enabled ? "is-enabled" : ""}">${target.enabled ? "已启用" : "已停用"}</span><div><strong>${esc(target.name)}</strong><code>${esc(path)}</code></div></div>
|
|
3905
|
+
<div class="s3-backup-target-actions"><button type="button" data-s3-backup-run="${esc(target.id)}" aria-label="立即运行 ${esc(target.name)}">立即备份</button><button type="button" data-s3-backup-edit="${esc(target.id)}" aria-label="编辑 ${esc(target.name)}">编辑</button><button class="danger-button" type="button" data-s3-backup-delete="${esc(target.id)}" aria-label="删除 ${esc(target.name)}">删除</button></div>
|
|
3906
|
+
<dl class="s3-backup-target-meta">
|
|
3907
|
+
<div><dt>定时计划</dt><dd>${target.enabled ? `每天 ${esc(target.scheduleTime)}` : "不自动执行"}</dd></div>
|
|
3908
|
+
<div><dt>同步内容</dt><dd>${target.backupImages ? "数据库与图片" : "仅数据库"}</dd></div>
|
|
3909
|
+
<div><dt>数据库留存</dt><dd>${Number(target.retentionCount)} 个快照</dd></div>
|
|
3910
|
+
<div><dt>最近成功</dt><dd>${target.lastSuccessAt ? esc(formatDateTime(target.lastSuccessAt)) : "尚未成功"}</dd></div>
|
|
3911
|
+
</dl>
|
|
3912
|
+
${failed ? `<p class="s3-backup-target-error">最近失败:${esc(target.lastError || "S3 服务请求失败")} · ${esc(formatDateTime(target.lastFailureAt))}</p>` : ""}
|
|
3913
|
+
</article>`;
|
|
3914
|
+
}).join("") : '<p class="s3-backup-empty">还没有 S3 备份目标。点击“新增目标”配置第一个全系统备份位置。</p>';
|
|
3915
|
+
|
|
3916
|
+
host.querySelectorAll("[data-s3-backup-run]").forEach((button) => button.addEventListener("click", () => {
|
|
3917
|
+
void queueS3BackupRuns([button.dataset.s3BackupRun], button);
|
|
3918
|
+
}));
|
|
3919
|
+
host.querySelectorAll("[data-s3-backup-edit]").forEach((button) => button.addEventListener("click", () => {
|
|
3920
|
+
const target = s3BackupTargets.find((item) => item.id === button.dataset.s3BackupEdit);
|
|
3921
|
+
if (target) openS3BackupTargetDialog(target);
|
|
3922
|
+
}));
|
|
3923
|
+
host.querySelectorAll("[data-s3-backup-delete]").forEach((button) => button.addEventListener("click", async () => {
|
|
3924
|
+
const target = s3BackupTargets.find((item) => item.id === button.dataset.s3BackupDelete);
|
|
3925
|
+
if (!target) return;
|
|
3926
|
+
const confirmed = await confirmToast(`删除 S3 备份目标“${target.name}”吗?远端已有对象不会被删除。`, {
|
|
3927
|
+
title: "删除备份目标",
|
|
3928
|
+
confirmLabel: "确认删除"
|
|
3929
|
+
});
|
|
3930
|
+
if (!confirmed) return;
|
|
3931
|
+
button.disabled = true;
|
|
3932
|
+
try {
|
|
3933
|
+
await api(`/api/platform/backups/targets/${encodeURIComponent(target.id)}`, { method: "DELETE" });
|
|
3934
|
+
await loadS3BackupData();
|
|
3935
|
+
toast(`备份目标“${target.name}”已删除`);
|
|
3936
|
+
} catch (error) {
|
|
3937
|
+
button.disabled = false;
|
|
3938
|
+
toast(error.message, "error");
|
|
3939
|
+
}
|
|
3940
|
+
}));
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
function renderS3BackupRuns() {
|
|
3944
|
+
const host = $("#s3-backup-runs");
|
|
3945
|
+
host.innerHTML = s3BackupRuns.length ? s3BackupRuns.map((run) => {
|
|
3946
|
+
const status = s3BackupStatusLabel(run.status);
|
|
3947
|
+
const details = run.serverResponse ? JSON.stringify(run.serverResponse, null, 2) : "";
|
|
3948
|
+
const metrics = run.status === "running"
|
|
3949
|
+
? "正在同步"
|
|
3950
|
+
: `图片上传 ${Number(run.imagesUploaded)} · 跳过 ${Number(run.imagesSkipped)} · 清理快照 ${Number(run.databasesDeleted)}`;
|
|
3951
|
+
return `<article class="s3-backup-run-card">
|
|
3952
|
+
<span class="s3-backup-status is-${esc(run.status)}">${esc(status)}</span>
|
|
3953
|
+
<div class="s3-backup-run-copy"><strong>${esc(run.targetName)}</strong><small>${run.trigger === "scheduled" ? "定时任务" : "手动触发"} · ${esc(formatDateTime(run.finishedAt || run.startedAt))}${run.errorMessage ? ` · ${esc(run.errorMessage)}` : ""}</small></div>
|
|
3954
|
+
<span class="s3-backup-run-metrics">${esc(metrics)}</span>
|
|
3955
|
+
${details ? `<details><summary>查看 S3 服务端返回结果</summary><pre>${esc(details)}</pre></details>` : ""}
|
|
3956
|
+
</article>`;
|
|
3957
|
+
}).join("") : '<p class="s3-backup-empty">还没有备份运行记录。</p>';
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
async function loadS3BackupData({ loading = false } = {}) {
|
|
3961
|
+
if (loading) {
|
|
3962
|
+
$("#s3-backup-targets").innerHTML = '<p class="s3-backup-empty">正在读取备份目标……</p>';
|
|
3963
|
+
$("#s3-backup-runs").innerHTML = '<p class="s3-backup-empty">正在读取运行记录……</p>';
|
|
3964
|
+
}
|
|
3965
|
+
const [targets, runs] = await Promise.all([
|
|
3966
|
+
api("/api/platform/backups/targets"),
|
|
3967
|
+
api("/api/platform/backups/runs?limit=30")
|
|
3968
|
+
]);
|
|
3969
|
+
s3BackupTargets = targets;
|
|
3970
|
+
s3BackupRuns = runs.items;
|
|
3971
|
+
renderS3BackupTargets();
|
|
3972
|
+
renderS3BackupRuns();
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
async function openS3BackupDialog() {
|
|
3976
|
+
if (state.user?.role !== "admin") return toast("需要系统管理员权限", "error");
|
|
3977
|
+
const dialog = $("#s3-backup-dialog");
|
|
3978
|
+
if (!dialog.open) dialog.showModal();
|
|
3979
|
+
try {
|
|
3980
|
+
await loadS3BackupData({ loading: true });
|
|
3981
|
+
} catch (error) {
|
|
3982
|
+
$("#s3-backup-targets").innerHTML = '<p class="s3-backup-empty">备份配置加载失败,请稍后刷新。</p>';
|
|
3983
|
+
toast(error.message, "error");
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
function updateS3BackupRootPreview() {
|
|
3988
|
+
$("#s3-backup-root-preview").textContent = s3BackupRootPrefix($("#s3-backup-base-path").value);
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
function openS3BackupTargetDialog(target = null) {
|
|
3992
|
+
editingS3BackupTargetId = target?.id ?? null;
|
|
3993
|
+
const form = $("#s3-backup-target-form");
|
|
3994
|
+
form.reset();
|
|
3995
|
+
$("#s3-backup-target-title").textContent = target ? "编辑 S3 目标" : "新增 S3 目标";
|
|
3996
|
+
$("#s3-backup-name").value = target?.name ?? "";
|
|
3997
|
+
$("#s3-backup-endpoint").value = target?.endpoint ?? "";
|
|
3998
|
+
$("#s3-backup-region").value = target?.region ?? "us-east-1";
|
|
3999
|
+
$("#s3-backup-bucket").value = target?.bucket ?? "";
|
|
4000
|
+
$("#s3-backup-base-path").value = target?.basePath ?? "";
|
|
4001
|
+
$("#s3-backup-schedule-time").value = target?.scheduleTime ?? "03:00";
|
|
4002
|
+
$("#s3-backup-retention-count").value = String(target?.retentionCount ?? 7);
|
|
4003
|
+
$("#s3-backup-enabled").checked = target?.enabled ?? false;
|
|
4004
|
+
$("#s3-backup-images").checked = target?.backupImages ?? true;
|
|
4005
|
+
$("#s3-backup-force-path-style").checked = target?.forcePathStyle ?? true;
|
|
4006
|
+
$("#s3-backup-access-key").required = !target;
|
|
4007
|
+
$("#s3-backup-secret-key").required = !target;
|
|
4008
|
+
$("#s3-backup-access-key").placeholder = target ? "留空则保持现有 Access Key" : "请输入 Access Key";
|
|
4009
|
+
$("#s3-backup-secret-key").placeholder = target ? "留空则保持现有 Secret Key" : "请输入 Secret Key";
|
|
4010
|
+
$("#s3-backup-credentials-note").textContent = target
|
|
4011
|
+
? "AK 与 SK 均留空时保持现有凭据;只填写其中一项时仅轮换对应凭据。"
|
|
4012
|
+
: "新增目标时必须填写 AK 和 SK。";
|
|
4013
|
+
$("#s3-backup-target-save").textContent = target ? "保存修改" : "保存目标";
|
|
4014
|
+
updateS3BackupRootPreview();
|
|
4015
|
+
const dialog = $("#s3-backup-target-dialog");
|
|
4016
|
+
if (!dialog.open) dialog.showModal();
|
|
4017
|
+
queueMicrotask(() => $("#s3-backup-name").focus());
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
async function saveS3BackupTarget(event) {
|
|
4021
|
+
event.preventDefault();
|
|
4022
|
+
const button = $("#s3-backup-target-save");
|
|
4023
|
+
const accessKeyId = $("#s3-backup-access-key").value;
|
|
4024
|
+
const secretAccessKey = $("#s3-backup-secret-key").value;
|
|
4025
|
+
const body = {
|
|
4026
|
+
name: $("#s3-backup-name").value.trim(),
|
|
4027
|
+
endpoint: $("#s3-backup-endpoint").value.trim(),
|
|
4028
|
+
region: $("#s3-backup-region").value.trim(),
|
|
4029
|
+
bucket: $("#s3-backup-bucket").value.trim(),
|
|
4030
|
+
basePath: $("#s3-backup-base-path").value.trim(),
|
|
4031
|
+
forcePathStyle: $("#s3-backup-force-path-style").checked,
|
|
4032
|
+
enabled: $("#s3-backup-enabled").checked,
|
|
4033
|
+
backupImages: $("#s3-backup-images").checked,
|
|
4034
|
+
scheduleTime: $("#s3-backup-schedule-time").value,
|
|
4035
|
+
retentionCount: Number($("#s3-backup-retention-count").value),
|
|
4036
|
+
...(!editingS3BackupTargetId || accessKeyId ? { accessKeyId } : {}),
|
|
4037
|
+
...(!editingS3BackupTargetId || secretAccessKey ? { secretAccessKey } : {})
|
|
4038
|
+
};
|
|
4039
|
+
button.disabled = true;
|
|
4040
|
+
try {
|
|
4041
|
+
await api(editingS3BackupTargetId
|
|
4042
|
+
? `/api/platform/backups/targets/${encodeURIComponent(editingS3BackupTargetId)}`
|
|
4043
|
+
: "/api/platform/backups/targets", {
|
|
4044
|
+
method: editingS3BackupTargetId ? "PATCH" : "POST",
|
|
4045
|
+
body
|
|
4046
|
+
});
|
|
4047
|
+
const message = editingS3BackupTargetId ? "S3 备份目标已更新" : "S3 备份目标已创建";
|
|
4048
|
+
$("#s3-backup-target-dialog").close();
|
|
4049
|
+
await loadS3BackupData();
|
|
4050
|
+
toast(message);
|
|
4051
|
+
} catch (error) {
|
|
4052
|
+
toast(error.message, "error");
|
|
4053
|
+
} finally {
|
|
4054
|
+
button.disabled = false;
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
async function queueS3BackupRuns(targetIds, button = null) {
|
|
4059
|
+
if (button) button.disabled = true;
|
|
4060
|
+
try {
|
|
4061
|
+
const result = await api("/api/platform/backups/run", {
|
|
4062
|
+
method: "POST",
|
|
4063
|
+
body: targetIds ? { targetIds } : {}
|
|
4064
|
+
});
|
|
4065
|
+
if (result.acceptedTargetIds.length) {
|
|
4066
|
+
toast(`已将 ${result.acceptedTargetIds.length} 个 S3 备份目标加入串行队列`);
|
|
4067
|
+
} else if (result.skippedTargetIds.length) {
|
|
4068
|
+
toast("所选 S3 备份目标已在队列中");
|
|
4069
|
+
} else {
|
|
4070
|
+
toast("当前没有已启用的 S3 备份目标", "error");
|
|
4071
|
+
}
|
|
4072
|
+
window.setTimeout(() => {
|
|
4073
|
+
void refreshS3BackupEvents();
|
|
4074
|
+
if ($("#s3-backup-dialog").open) void loadS3BackupData().catch((error) => toast(error.message, "error"));
|
|
4075
|
+
}, 500);
|
|
4076
|
+
} catch (error) {
|
|
4077
|
+
toast(error.message, "error");
|
|
4078
|
+
} finally {
|
|
4079
|
+
if (button) button.disabled = false;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
function scheduleS3BackupEventPoll(delay = s3BackupPollInterval) {
|
|
4084
|
+
if (s3BackupPollTimer !== null) window.clearTimeout(s3BackupPollTimer);
|
|
4085
|
+
s3BackupPollTimer = null;
|
|
4086
|
+
if (state.user?.role !== "admin" || systemRestartDetected) return;
|
|
4087
|
+
s3BackupPollTimer = window.setTimeout(() => {
|
|
4088
|
+
s3BackupPollTimer = null;
|
|
4089
|
+
void refreshS3BackupEvents();
|
|
4090
|
+
}, delay);
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
async function refreshS3BackupEvents({ announce = true } = {}) {
|
|
4094
|
+
if (state.user?.role !== "admin" || systemRestartDetected) return;
|
|
4095
|
+
if (s3BackupPollTimer !== null) window.clearTimeout(s3BackupPollTimer);
|
|
4096
|
+
s3BackupPollTimer = null;
|
|
4097
|
+
const requestId = ++s3BackupPollRequest;
|
|
4098
|
+
try {
|
|
4099
|
+
const result = await api("/api/platform/backups/runs?limit=100");
|
|
4100
|
+
if (requestId !== s3BackupPollRequest || state.user?.role !== "admin") return;
|
|
4101
|
+
const transitions = collectS3BackupRunTransitions(
|
|
4102
|
+
s3BackupRunSnapshots,
|
|
4103
|
+
result.items,
|
|
4104
|
+
s3BackupRunsInitialized
|
|
4105
|
+
);
|
|
4106
|
+
s3BackupRunSnapshots = transitions.snapshots;
|
|
4107
|
+
if (s3BackupRunsInitialized && announce) {
|
|
4108
|
+
for (const run of transitions.failures) toast(s3BackupFailureToast(run), "error");
|
|
4109
|
+
}
|
|
4110
|
+
s3BackupRunsInitialized = true;
|
|
4111
|
+
if ($("#s3-backup-dialog").open) {
|
|
4112
|
+
s3BackupRuns = result.items.slice(0, 30);
|
|
4113
|
+
renderS3BackupRuns();
|
|
4114
|
+
s3BackupTargets = await api("/api/platform/backups/targets");
|
|
4115
|
+
renderS3BackupTargets();
|
|
4116
|
+
}
|
|
4117
|
+
} catch (error) {
|
|
4118
|
+
console.error("Failed to refresh S3 backup events", error);
|
|
4119
|
+
} finally {
|
|
4120
|
+
scheduleS3BackupEventPoll();
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
function startS3BackupEventPolling() {
|
|
4125
|
+
if (state.user?.role !== "admin") return;
|
|
4126
|
+
s3BackupRunSnapshots = new Map();
|
|
4127
|
+
s3BackupRunsInitialized = false;
|
|
4128
|
+
void refreshS3BackupEvents({ announce: false });
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
function stopS3BackupEventPolling() {
|
|
4132
|
+
if (s3BackupPollTimer !== null) window.clearTimeout(s3BackupPollTimer);
|
|
4133
|
+
s3BackupPollTimer = null;
|
|
4134
|
+
s3BackupPollRequest += 1;
|
|
4135
|
+
s3BackupRunsInitialized = false;
|
|
4136
|
+
s3BackupRunSnapshots = new Map();
|
|
4137
|
+
}
|
|
4138
|
+
|
|
3842
4139
|
function renderMemberPermissionGrid(value) {
|
|
3843
4140
|
const permissions = normalizeModulePermissions(value, "custom");
|
|
3844
4141
|
$("#member-permission-grid").innerHTML = WORK_PERMISSION_MODULES.map((item) => `<label class="member-permission-row">
|
|
@@ -3965,8 +4262,9 @@ function renderSearchResults(results, query) {
|
|
|
3965
4262
|
$("#search-results").innerHTML = '<p class="search-results-status">未找到相关内容。</p>';
|
|
3966
4263
|
return;
|
|
3967
4264
|
}
|
|
4265
|
+
const orderedResults = prioritizeGlobalSearchResults(results);
|
|
3968
4266
|
const matchKindLabel = { metadata: "资料命中", exact: "精确命中", phonetic: "拼音命中" };
|
|
3969
|
-
$("#search-results").innerHTML = `<p class="search-results-summary">找到 ${
|
|
4267
|
+
$("#search-results").innerHTML = `<p class="search-results-summary">找到 ${orderedResults.length} 条结果,非正文结果优先,正文条目随后展示。</p>${orderedResults.map((item) => {
|
|
3970
4268
|
const matchKinds = Array.isArray(item.matchKinds) ? item.matchKinds : [];
|
|
3971
4269
|
const lineRange = Number.isInteger(item.startLine)
|
|
3972
4270
|
? `<span class="search-result-chip">${item.startLine === item.endLine ? `第 ${item.startLine} 行` : `第 ${item.startLine}-${item.endLine} 行`}</span>`
|
|
@@ -3981,7 +4279,7 @@ function renderSearchResults(results, query) {
|
|
|
3981
4279
|
}).join("")}`;
|
|
3982
4280
|
$("#search-results").querySelectorAll(".search-result").forEach((button, index) => {
|
|
3983
4281
|
button.addEventListener("click", () => {
|
|
3984
|
-
openSearchResult(
|
|
4282
|
+
openSearchResult(orderedResults[index])
|
|
3985
4283
|
.catch((error) => toast(error.message, "error"));
|
|
3986
4284
|
});
|
|
3987
4285
|
});
|
|
@@ -4050,6 +4348,12 @@ async function openSearchResult(result) {
|
|
|
4050
4348
|
|| !$("#platform-usage-view").classList.contains("hidden")
|
|
4051
4349
|
|| !$("#work-audit-view").classList.contains("hidden");
|
|
4052
4350
|
if (inSettings) await returnFromSettings();
|
|
4351
|
+
if (target.kind === "agent-history") {
|
|
4352
|
+
ensureAiPanelExpanded();
|
|
4353
|
+
await openAiConversation(target.conversationId, true, target.messageId);
|
|
4354
|
+
if (target.messageId) focusAiConversationMessage(target.messageId);
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4053
4357
|
if (target.kind === "chapter") {
|
|
4054
4358
|
await selectChapter(target.id);
|
|
4055
4359
|
if (state.chapter?.id === target.id && target.startLine) {
|
|
@@ -6841,11 +7145,9 @@ function renderProviderCards(providers, models) {
|
|
|
6841
7145
|
const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
|
|
6842
7146
|
const modelStatus = !model.enabled
|
|
6843
7147
|
? `<span class="model-status-badge is-disabled">模型已停用</span>`
|
|
6844
|
-
: provider.
|
|
6845
|
-
? `<span class="model-status-badge is-
|
|
6846
|
-
:
|
|
6847
|
-
? `<span class="model-status-badge is-unavailable">连接不可用</span>`
|
|
6848
|
-
: "";
|
|
7148
|
+
: provider.connectionStatus !== "success"
|
|
7149
|
+
? `<span class="model-status-badge is-unavailable">连接不可用</span>`
|
|
7150
|
+
: "";
|
|
6849
7151
|
const capability = model.multimodalEnabled ? " · 多模态" : "";
|
|
6850
7152
|
const defaultBadge = model.imageToolDefault ? " · 默认读图模型" : "";
|
|
6851
7153
|
return `<div class="provider-model-row${modelUnavailable ? " is-unavailable" : ""}"><button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"}${capability}${defaultBadge} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>${modelStatus}</div>`;
|
|
@@ -7400,6 +7702,7 @@ async function renderBookAiSettings() {
|
|
|
7400
7702
|
]);
|
|
7401
7703
|
const host = $("#module-content");
|
|
7402
7704
|
const workId = String(state.work.id);
|
|
7705
|
+
const maximumAgentToolCallLimit = Math.max(5, Number(settings.agentToolCallLimitMaximum) || 80);
|
|
7403
7706
|
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image"]);
|
|
7404
7707
|
const dailyTokenQuota = settings.dailyTokenQuota === null ? null : Number(settings.dailyTokenQuota);
|
|
7405
7708
|
const quotaUsedTokens = Number(usage?.quota?.usedTokens) || 0;
|
|
@@ -7413,8 +7716,14 @@ async function renderBookAiSettings() {
|
|
|
7413
7716
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
7414
7717
|
title: "本书 Token 用量",
|
|
7415
7718
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
7416
|
-
})}</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)}`;
|
|
7417
|
-
|
|
7719
|
+
})}</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 class="checkbox-field config-checkbox-field"><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>设定上下文注入</h2><p>开启后,本书的普通 AI 请求会自动注入锁定设定、组织、种族与相关约束;即使本轮同时使用“@注入上下文设定”,也只会注入一次。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="always-include-setting-info" type="checkbox" ${settings.alwaysIncludeSettingInfo ? "checked" : ""}>是否注入设定</label><button id="save-always-include-setting-info" 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 ai-agent-tools-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)}`;
|
|
7720
|
+
const agentToolCallLimitInput = host.querySelector("#agent-tool-call-limit");
|
|
7721
|
+
agentToolCallLimitInput?.setAttribute("max", String(maximumAgentToolCallLimit));
|
|
7722
|
+
const agentToolCallDescription = agentToolCallLimitInput?.closest(".config-section")?.querySelector(".config-section-header p");
|
|
7723
|
+
if (agentToolCallDescription?.firstChild) agentToolCallDescription.firstChild.nodeValue = agentToolCallDescription.firstChild.nodeValue.replace("5–48", `5–${maximumAgentToolCallLimit}`);
|
|
7724
|
+
host.querySelectorAll(".config-section").forEach((section) => {
|
|
7725
|
+
if (section.querySelector("h2")?.textContent === "Agent 工具调用上限") section.id = "agent-tool-call-limit-settings";
|
|
7726
|
+
});
|
|
7418
7727
|
bindUsageCalendarInteractions(host);
|
|
7419
7728
|
scrollUsageCalendarsToLatest(host);
|
|
7420
7729
|
host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
|
|
@@ -7581,12 +7890,25 @@ async function renderBookAiSettings() {
|
|
|
7581
7890
|
});
|
|
7582
7891
|
$("#save-agent-tool-call-limit").addEventListener("click", async () => {
|
|
7583
7892
|
const button = $("#save-agent-tool-call-limit");
|
|
7893
|
+
const input = $("#agent-tool-call-limit");
|
|
7894
|
+
const value = Number(input.value);
|
|
7895
|
+
const maximum = Number(input.max) || 80;
|
|
7896
|
+
if (!Number.isInteger(value) || value < 5) {
|
|
7897
|
+
toast(`Agent 工具调用上限必须是 5 到 ${maximum} 之间的整数`, "error");
|
|
7898
|
+
input.focus();
|
|
7899
|
+
return;
|
|
7900
|
+
}
|
|
7901
|
+
if (value > maximum) {
|
|
7902
|
+
toast(`Agent 工具调用上限不能超过 ${maximum} 次`, "error");
|
|
7903
|
+
input.focus();
|
|
7904
|
+
return;
|
|
7905
|
+
}
|
|
7584
7906
|
button.disabled = true;
|
|
7585
7907
|
try {
|
|
7586
7908
|
await api(`/api/works/${state.work.id}/ai-settings`, {
|
|
7587
7909
|
method: "PATCH",
|
|
7588
7910
|
body: {
|
|
7589
|
-
agentToolCallLimit:
|
|
7911
|
+
agentToolCallLimit: value,
|
|
7590
7912
|
agentToolCallGlobalMultiplier: Number($("#agent-tool-call-global-multiplier").value)
|
|
7591
7913
|
}
|
|
7592
7914
|
});
|
|
@@ -10560,9 +10882,13 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
10560
10882
|
const isFailure = role === "assistant" && text.startsWith("调用失败:");
|
|
10561
10883
|
message.className = `${role === "user" ? "user-message" : "assistant-message"}${isFailure ? " is-error" : ""}`;
|
|
10562
10884
|
const messageBody = isFailure
|
|
10563
|
-
? `<p class="ai-error-text">${esc(text)}</p
|
|
10885
|
+
? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
|
|
10564
10886
|
: renderMarkdown(text);
|
|
10565
10887
|
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
10888
|
+
message.querySelector("[data-ai-tool-call-settings-link]")?.addEventListener("click", (event) => {
|
|
10889
|
+
event.preventDefault();
|
|
10890
|
+
openAiToolCallSettings().catch((error) => toast(`打开 AI 设置失败:${error.message}`, "error"));
|
|
10891
|
+
});
|
|
10566
10892
|
const heading = attachMessageHeading(message, role === "user" ? "作者" : aiAssistantLabel(), createdAt ?? undefined);
|
|
10567
10893
|
if (isFailure) {
|
|
10568
10894
|
message.dataset.status = "failed";
|
|
@@ -11409,6 +11735,7 @@ $("#api-key-copy-button").addEventListener("click", async () => {
|
|
|
11409
11735
|
});
|
|
11410
11736
|
$("#logout-button").addEventListener("click", async () => {
|
|
11411
11737
|
try {
|
|
11738
|
+
stopS3BackupEventPolling();
|
|
11412
11739
|
await api("/api/auth/session", { method: "DELETE" });
|
|
11413
11740
|
state.user = null;
|
|
11414
11741
|
state.csrfToken = null;
|
|
@@ -11539,6 +11866,7 @@ $("#work-audit-load-more").addEventListener("click", () => {
|
|
|
11539
11866
|
if (workAuditNextPage !== null) loadWorkAuditPage(workAuditNextPage, true).catch((error) => toast(error.message, "error"));
|
|
11540
11867
|
});
|
|
11541
11868
|
$("#platform-ui-settings-button").addEventListener("click", openPlatformUiSettingsDialog);
|
|
11869
|
+
$("#s3-backup-button").addEventListener("click", () => openS3BackupDialog().catch((error) => toast(error.message, "error")));
|
|
11542
11870
|
$("#collaboration-button").addEventListener("click", () => openMembersDialog());
|
|
11543
11871
|
$("#presence-button").addEventListener("click", () => {
|
|
11544
11872
|
const panel = $("#presence-panel");
|
|
@@ -11550,6 +11878,30 @@ $("#users-settings-return").addEventListener("click", () => returnToSettingsHub(
|
|
|
11550
11878
|
$("#platform-ui-settings-close").addEventListener("click", () => $("#platform-ui-settings-dialog").close());
|
|
11551
11879
|
$("#platform-ui-settings-return").addEventListener("click", () => returnToSettingsHub("#platform-ui-settings-button", "#platform-ui-settings-dialog").catch((error) => toast(error.message, "error")));
|
|
11552
11880
|
$("#platform-ui-settings-cancel").addEventListener("click", () => $("#platform-ui-settings-dialog").close());
|
|
11881
|
+
$("#s3-backup-close").addEventListener("click", () => $("#s3-backup-dialog").close());
|
|
11882
|
+
$("#s3-backup-settings-return").addEventListener("click", () => returnToSettingsHub("#s3-backup-button", "#s3-backup-dialog").catch((error) => toast(error.message, "error")));
|
|
11883
|
+
$("#s3-backup-add").addEventListener("click", () => openS3BackupTargetDialog());
|
|
11884
|
+
$("#s3-backup-run-all").addEventListener("click", (event) => void queueS3BackupRuns(null, event.currentTarget));
|
|
11885
|
+
$("#s3-backup-refresh").addEventListener("click", async (event) => {
|
|
11886
|
+
const button = event.currentTarget;
|
|
11887
|
+
button.disabled = true;
|
|
11888
|
+
try {
|
|
11889
|
+
await loadS3BackupData();
|
|
11890
|
+
toast("S3 备份状态已刷新");
|
|
11891
|
+
} catch (error) {
|
|
11892
|
+
toast(error.message, "error");
|
|
11893
|
+
} finally {
|
|
11894
|
+
button.disabled = false;
|
|
11895
|
+
}
|
|
11896
|
+
});
|
|
11897
|
+
$("#s3-backup-target-close").addEventListener("click", () => $("#s3-backup-target-dialog").close());
|
|
11898
|
+
$("#s3-backup-target-cancel").addEventListener("click", () => $("#s3-backup-target-dialog").close());
|
|
11899
|
+
$("#s3-backup-target-form").addEventListener("submit", saveS3BackupTarget);
|
|
11900
|
+
$("#s3-backup-base-path").addEventListener("input", updateS3BackupRootPreview);
|
|
11901
|
+
$("#s3-backup-target-dialog").addEventListener("close", () => {
|
|
11902
|
+
editingS3BackupTargetId = null;
|
|
11903
|
+
$("#s3-backup-target-form").reset();
|
|
11904
|
+
});
|
|
11553
11905
|
$("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
11554
11906
|
event.preventDefault();
|
|
11555
11907
|
const button = $("#platform-ui-settings-save");
|
|
@@ -12209,6 +12561,7 @@ $("#manuscript-export-menu").addEventListener("click", (event) => {
|
|
|
12209
12561
|
document.addEventListener("visibilitychange", () => {
|
|
12210
12562
|
if (document.visibilityState !== "visible") return;
|
|
12211
12563
|
if (state.user && !systemRestartDetected) scheduleSystemBootCheck(0);
|
|
12564
|
+
if (state.user?.role === "admin" && !systemRestartDetected) void refreshS3BackupEvents();
|
|
12212
12565
|
void refreshSystemHealth();
|
|
12213
12566
|
});
|
|
12214
12567
|
window.addEventListener("beforeunload", (event) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type GlobalSearchTarget =
|
|
2
2
|
| { kind: "chapter"; type: "chapter"; id: string; module: "editor"; startLine?: number; endLine?: number }
|
|
3
|
+
| { kind: "agent-history"; type: "agent-history"; id: string; module: "editor"; conversationId: string; messageId?: string }
|
|
3
4
|
| {
|
|
4
5
|
kind: "entity";
|
|
5
6
|
type: "setting" | "character" | "race" | "organization" | "timeline-track" | "timeline-event" | "relationship" | "chapter-outline" | "foreshadow" | "review";
|
|
@@ -9,5 +10,6 @@ export type GlobalSearchTarget =
|
|
|
9
10
|
apiPath: string;
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
export function prioritizeGlobalSearchResults<T extends { type?: unknown }>(results: readonly T[]): T[];
|
|
12
14
|
export function splitGlobalSearchHighlight(value: unknown, query: unknown): Array<{ text: string; match: boolean }>;
|
|
13
|
-
export function resolveGlobalSearchTarget(result?: { type?: unknown; id?: unknown; startLine?: unknown; endLine?: unknown }): GlobalSearchTarget | null;
|
|
15
|
+
export function resolveGlobalSearchTarget(result?: { type?: unknown; id?: unknown; startLine?: unknown; endLine?: unknown; conversationId?: unknown; messageId?: unknown }): GlobalSearchTarget | null;
|