@musnows/scriverse 0.6.7 → 0.6.8
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-protocol.js +5 -1
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai.js +390 -27
- package/dist/ai.js.map +1 -1
- package/dist/app.js +25 -5
- package/dist/app.js.map +1 -1
- package/dist/database.js +43 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +275 -78
- package/dist/public/index.html +5 -3
- package/dist/public/model-config.d.ts +3 -0
- package/dist/public/model-config.js +8 -0
- package/dist/public/styles.css +42 -1
- package/dist/release-update.js +170 -0
- package/dist/release-update.js.map +1 -0
- package/dist/server-runtime.js +5 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +32 -6
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -5,7 +5,7 @@ import { findAiMention, listAiMentionOptions, mergeAiReferenceScope } from "/ai-
|
|
|
5
5
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
6
|
import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
7
7
|
import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
|
|
8
|
-
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=
|
|
8
|
+
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, supportsMultimodalModelProtocol } from "/model-config.js?v=20260803-multimodal-model-config-v2";
|
|
9
9
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
10
10
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
11
11
|
import { createStreamTypewriter } from "/stream-typewriter.js?v=20260730-ai-stream-typewriter-v3";
|
|
@@ -195,6 +195,10 @@ let backgroundTaskCenterWorkId = null;
|
|
|
195
195
|
let backgroundTaskCenterTasksInitialized = false;
|
|
196
196
|
let backgroundTaskCenterTaskSnapshots = new Map();
|
|
197
197
|
let backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
198
|
+
let productUpdateStatus = null;
|
|
199
|
+
let productUpdateChecking = false;
|
|
200
|
+
let productUpdateTimer = null;
|
|
201
|
+
const fallbackProductUpdatePollInterval = 60 * 60 * 1000;
|
|
198
202
|
const systemHealthPollInterval = 30_000;
|
|
199
203
|
let systemHealthTimer = null;
|
|
200
204
|
let systemHealthSnapshot = { status: "checking", version: "" };
|
|
@@ -965,6 +969,7 @@ let knowledgeSectionEditorDirty = false;
|
|
|
965
969
|
let characterEditorVersions = [];
|
|
966
970
|
let characterEditorRelationships = [];
|
|
967
971
|
let characterEditorRelationshipsLoading = false;
|
|
972
|
+
let characterEditorRelationshipsLoaded = false;
|
|
968
973
|
let characterEditorSections = [];
|
|
969
974
|
let characterSectionPendingAttachments = [];
|
|
970
975
|
let markdownEditorPendingAttachments = [];
|
|
@@ -1483,7 +1488,9 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
1483
1488
|
search_story_entities: "搜索作品实体",
|
|
1484
1489
|
read_character_sections: "读取人物 Markdown 章节",
|
|
1485
1490
|
search_drafts: "搜索想法",
|
|
1486
|
-
recall_self: "回忆自身"
|
|
1491
|
+
recall_self: "回忆自身",
|
|
1492
|
+
image: "读取设定图片",
|
|
1493
|
+
recall_relationship: "回忆人物关系"
|
|
1487
1494
|
};
|
|
1488
1495
|
|
|
1489
1496
|
const AI_TOOL_DESCRIPTIONS = {
|
|
@@ -1493,7 +1500,9 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
1493
1500
|
search_story_entities: "按实体名、拼音或短关键词混合检索设定、人物、组织等结构化记录;非语义问答。",
|
|
1494
1501
|
read_character_sections: "读取指定人物 Markdown 档案章节的摘要或原文。",
|
|
1495
1502
|
search_drafts: "搜索可能采用、也可能永远不会进入正文或正式设定的未确认临时想法。",
|
|
1496
|
-
recall_self: "读取当前扮演角色自己的角色卡、档案,以及自己参与的关系、时间线和正文记忆。"
|
|
1503
|
+
recall_self: "读取当前扮演角色自己的角色卡、档案,以及自己参与的关系、时间线和正文记忆。",
|
|
1504
|
+
image: "读取设定正文引用的图片附件,并返回多模态模型的理解内容。",
|
|
1505
|
+
recall_relationship: "不传角色列表时读取有关系的角色列表;传入一个或多个角色后读取当前角色与这些角色之间的关系详情。"
|
|
1497
1506
|
};
|
|
1498
1507
|
|
|
1499
1508
|
let aiFeedScrollFrame = null;
|
|
@@ -2507,10 +2516,7 @@ async function api(path, options = {}) {
|
|
|
2507
2516
|
if (response.status === 401 && !path.startsWith("/api/auth/") && !path.includes("/presence")) {
|
|
2508
2517
|
const restarted = await checkSystemBoot(true);
|
|
2509
2518
|
if (!restarted) {
|
|
2510
|
-
|
|
2511
|
-
state.csrfToken = null;
|
|
2512
|
-
moduleRequestCache.clear();
|
|
2513
|
-
showAuth(false);
|
|
2519
|
+
invalidateAuthentication();
|
|
2514
2520
|
}
|
|
2515
2521
|
}
|
|
2516
2522
|
throw createClientError(payload.error, `请求失败:${response.status}`, response.status);
|
|
@@ -2650,6 +2656,35 @@ function applyProductHealthMetadata(health) {
|
|
|
2650
2656
|
});
|
|
2651
2657
|
}
|
|
2652
2658
|
|
|
2659
|
+
function applyProductUpdateMetadata(update) {
|
|
2660
|
+
if (update && typeof update === "object") productUpdateStatus = update;
|
|
2661
|
+
if (update?.checked !== true) {
|
|
2662
|
+
renderBackgroundTaskCenter();
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
const latestVersion = String(update.latestVersion ?? "").trim();
|
|
2666
|
+
const releaseUrl = String(update.releaseUrl ?? "").trim();
|
|
2667
|
+
const updateAvailable = update.updateAvailable === true && Boolean(latestVersion) && Boolean(releaseUrl);
|
|
2668
|
+
const updateDot = document.querySelector("[data-settings-update-dot]");
|
|
2669
|
+
updateDot?.classList.toggle("hidden", !updateAvailable);
|
|
2670
|
+
const settingsButton = $("#settings-button");
|
|
2671
|
+
settingsButton.setAttribute("aria-label", updateAvailable ? `设置,有新版本 v${latestVersion}` : "设置");
|
|
2672
|
+
settingsButton.title = updateAvailable ? `设置,有新版本 v${latestVersion}` : "设置";
|
|
2673
|
+
document.querySelectorAll("[data-product-footer-update]").forEach((element) => {
|
|
2674
|
+
element.classList.toggle("hidden", !updateAvailable);
|
|
2675
|
+
if (updateAvailable) {
|
|
2676
|
+
element.textContent = `发现新版本 v${latestVersion},查看更新说明`;
|
|
2677
|
+
element.href = releaseUrl;
|
|
2678
|
+
element.setAttribute("aria-label", `查看叙界 v${latestVersion} 更新说明`);
|
|
2679
|
+
} else {
|
|
2680
|
+
element.textContent = "";
|
|
2681
|
+
element.removeAttribute("href");
|
|
2682
|
+
element.removeAttribute("aria-label");
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
renderBackgroundTaskCenter();
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2653
2688
|
function scheduleSystemHealthCheck() {
|
|
2654
2689
|
if (systemHealthTimer !== null) window.clearTimeout(systemHealthTimer);
|
|
2655
2690
|
systemHealthTimer = window.setTimeout(() => {
|
|
@@ -2669,11 +2704,44 @@ async function refreshSystemHealth() {
|
|
|
2669
2704
|
}
|
|
2670
2705
|
}
|
|
2671
2706
|
|
|
2707
|
+
function scheduleProductUpdateCheck() {
|
|
2708
|
+
if (productUpdateTimer !== null) window.clearTimeout(productUpdateTimer);
|
|
2709
|
+
if (productUpdateStatus?.enabled === false) {
|
|
2710
|
+
productUpdateTimer = null;
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
const nextCheckAt = Date.parse(String(productUpdateStatus?.nextCheckAt ?? ""));
|
|
2714
|
+
const delay = Number.isFinite(nextCheckAt)
|
|
2715
|
+
? Math.max(1000, nextCheckAt - Date.now())
|
|
2716
|
+
: fallbackProductUpdatePollInterval;
|
|
2717
|
+
productUpdateTimer = window.setTimeout(() => {
|
|
2718
|
+
productUpdateTimer = null;
|
|
2719
|
+
void refreshProductUpdate();
|
|
2720
|
+
}, Math.min(delay, 2_147_483_647));
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
async function refreshProductUpdate() {
|
|
2724
|
+
if (productUpdateChecking || productUpdateStatus?.enabled === false) return;
|
|
2725
|
+
productUpdateChecking = true;
|
|
2726
|
+
renderBackgroundTaskCenter();
|
|
2727
|
+
try {
|
|
2728
|
+
applyProductUpdateMetadata(await api("/api/update-check"));
|
|
2729
|
+
} catch {
|
|
2730
|
+
// 更新探测失败时静默保留当前展示状态
|
|
2731
|
+
} finally {
|
|
2732
|
+
productUpdateChecking = false;
|
|
2733
|
+
renderBackgroundTaskCenter();
|
|
2734
|
+
scheduleProductUpdateCheck();
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2672
2738
|
async function initializeProductFooters() {
|
|
2673
2739
|
const year = String(new Date().getFullYear());
|
|
2674
2740
|
document.querySelectorAll("[data-product-footer-year]").forEach((element) => { element.textContent = year; });
|
|
2675
2741
|
applyProductHealthMetadata(null);
|
|
2742
|
+
applyProductUpdateMetadata(null);
|
|
2676
2743
|
await refreshSystemHealth();
|
|
2744
|
+
void refreshProductUpdate();
|
|
2677
2745
|
}
|
|
2678
2746
|
|
|
2679
2747
|
function observeSystemBootId(value) {
|
|
@@ -2768,8 +2836,28 @@ async function refreshAuthCaptcha(target = "login") {
|
|
|
2768
2836
|
if (answerInput) answerInput.value = "";
|
|
2769
2837
|
}
|
|
2770
2838
|
|
|
2839
|
+
function clearAuthenticationOverlays() {
|
|
2840
|
+
const toastRegion = $("#toast-region");
|
|
2841
|
+
toastRegion.replaceChildren();
|
|
2842
|
+
document.querySelectorAll("[popover]").forEach((popover) => {
|
|
2843
|
+
if (typeof popover.hidePopover === "function" && popover.matches(":popover-open")) popover.hidePopover();
|
|
2844
|
+
});
|
|
2845
|
+
document.querySelectorAll("dialog[open]").forEach((dialog) => dialog.close());
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function invalidateAuthentication() {
|
|
2849
|
+
state.user = null;
|
|
2850
|
+
state.csrfToken = null;
|
|
2851
|
+
moduleRequestCache.clear();
|
|
2852
|
+
showAuth(false);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2771
2855
|
function showAuth(setupRequired, registrationOpen = false, setupTokenRequired = false) {
|
|
2772
2856
|
if (state.user) return;
|
|
2857
|
+
document.documentElement.classList.remove("dev-auth-bypass");
|
|
2858
|
+
document.documentElement.classList.add("login-route");
|
|
2859
|
+
window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
2860
|
+
clearAuthenticationOverlays();
|
|
2773
2861
|
document.body.classList.add("auth-pending");
|
|
2774
2862
|
$("#auth-view").classList.remove("hidden");
|
|
2775
2863
|
const canRegister = registrationOpen === true;
|
|
@@ -2897,6 +2985,7 @@ function dismissChapterInsightToast() {
|
|
|
2897
2985
|
}
|
|
2898
2986
|
|
|
2899
2987
|
function toast(message, type = "info") {
|
|
2988
|
+
if (systemRestartDetected || (!state.user && document.documentElement.classList.contains("login-route"))) return;
|
|
2900
2989
|
const region = $("#toast-region");
|
|
2901
2990
|
const element = document.createElement("div");
|
|
2902
2991
|
element.className = `toast ${type}`;
|
|
@@ -5067,15 +5156,14 @@ function bindModuleContentInteractions() {
|
|
|
5067
5156
|
moduleContentInteractionsBound = true;
|
|
5068
5157
|
const host = $("#module-content");
|
|
5069
5158
|
const open = async (card) => {
|
|
5070
|
-
const id = card.dataset.openSetting ?? card.dataset.
|
|
5159
|
+
const id = card.dataset.openSetting ?? card.dataset.openRace ?? card.dataset.openOrganization;
|
|
5071
5160
|
if (!id) return;
|
|
5072
5161
|
if (card.dataset.openSetting) return openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly: true });
|
|
5073
|
-
if (card.dataset.openCharacter) return openCharacterEditor(await api(`/api/characters/${encodeURIComponent(id)}`), { readOnly: true });
|
|
5074
5162
|
if (card.dataset.openRace) return openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly: true });
|
|
5075
5163
|
if (card.dataset.openOrganization) return openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly: true });
|
|
5076
5164
|
};
|
|
5077
5165
|
const findCard = (target) => target instanceof Element
|
|
5078
|
-
? target.closest("[data-open-setting], [data-open-
|
|
5166
|
+
? target.closest("[data-open-setting], [data-open-race], [data-open-organization]")
|
|
5079
5167
|
: null;
|
|
5080
5168
|
host.addEventListener("click", (event) => {
|
|
5081
5169
|
if (event.target instanceof Element && event.target.closest("button, a, summary")) return;
|
|
@@ -5608,10 +5696,7 @@ async function renderRaces() {
|
|
|
5608
5696
|
}
|
|
5609
5697
|
|
|
5610
5698
|
async function renderOrganizations(page = moduleListPages.organizations) {
|
|
5611
|
-
|
|
5612
|
-
moduleApiAllPages("organizations", `/api/works/${state.work.id}/organizations`),
|
|
5613
|
-
canReadModule("characters") ? moduleApiAllPages("organizations", `/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
5614
|
-
]);
|
|
5699
|
+
state.organizations = await moduleApiAllPages("organizations", `/api/works/${state.work.id}/organizations`);
|
|
5615
5700
|
mountModuleCount(state.organizations.length);
|
|
5616
5701
|
const pageResult = paginateModuleItems(state.organizations, page, "organizations");
|
|
5617
5702
|
moduleListPages.organizations = pageResult.page;
|
|
@@ -5916,10 +6001,11 @@ async function renderReviews(page = moduleListPages.reviews) {
|
|
|
5916
6001
|
const canResolveReview = canEditModule("reviews");
|
|
5917
6002
|
const canMergeCharacters = canResolveReview
|
|
5918
6003
|
&& ["characters", "races", "organizations", "timeline", "relationships"].every((module) => canEditModule(module));
|
|
5919
|
-
const
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
6004
|
+
const reviews = await moduleApiAllPages("reviews", `/api/works/${state.work.id}/reviews`);
|
|
6005
|
+
const hasCharacterDuplicateReviews = reviews.some((item) => item.itemType === "character-duplicate");
|
|
6006
|
+
const characters = canReadCharacters && hasCharacterDuplicateReviews
|
|
6007
|
+
? await moduleApiAllPages("reviews", `/api/works/${state.work.id}/characters?includeMerged=1`)
|
|
6008
|
+
: [];
|
|
5923
6009
|
mountModuleCount(reviews.length);
|
|
5924
6010
|
const pageResult = paginateModuleItems(reviews, page, "reviews");
|
|
5925
6011
|
moduleListPages.reviews = pageResult.page;
|
|
@@ -6760,7 +6846,9 @@ function renderProviderCards(providers, models) {
|
|
|
6760
6846
|
: provider.connectionStatus !== "success"
|
|
6761
6847
|
? `<span class="model-status-badge is-unavailable">连接不可用</span>`
|
|
6762
6848
|
: "";
|
|
6763
|
-
|
|
6849
|
+
const capability = model.multimodalEnabled ? " · 多模态" : "";
|
|
6850
|
+
const defaultBadge = model.imageToolDefault ? " · 默认读图模型" : "";
|
|
6851
|
+
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>`;
|
|
6764
6852
|
}).join("")}</div>
|
|
6765
6853
|
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button>${provider.status === "enabled" ? `<button data-test-provider="${esc(provider.id)}" ${providerModels.length ? "" : "disabled aria-disabled=\"true\" title=\"请先添加模型\""}>测试连接</button>` : ""}<button data-add-model="${esc(provider.id)}">添加模型</button></div></article>`;
|
|
6766
6854
|
}).join("")}</div>`
|
|
@@ -6776,8 +6864,11 @@ function bindPlatformProviderActions(host, providers, models) {
|
|
|
6776
6864
|
await renderPlatformAiConfig();
|
|
6777
6865
|
await loadModels();
|
|
6778
6866
|
}));
|
|
6779
|
-
host.querySelectorAll("[data-add-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(button.dataset.addModel)));
|
|
6780
|
-
host.querySelectorAll("[data-edit-model]").forEach((button) => button.addEventListener("click", () =>
|
|
6867
|
+
host.querySelectorAll("[data-add-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(button.dataset.addModel, null, providers.find((provider) => provider.id === button.dataset.addModel))));
|
|
6868
|
+
host.querySelectorAll("[data-edit-model]").forEach((button) => button.addEventListener("click", () => {
|
|
6869
|
+
const model = models.find((item) => item.id === button.dataset.editModel);
|
|
6870
|
+
openModelDialog(undefined, model, providers.find((provider) => provider.id === model?.providerId));
|
|
6871
|
+
}));
|
|
6781
6872
|
host.querySelectorAll("[data-edit-provider]").forEach((button) => button.addEventListener("click", () => openProviderDialog(providers.find((provider) => provider.id === button.dataset.editProvider))));
|
|
6782
6873
|
}
|
|
6783
6874
|
|
|
@@ -6785,14 +6876,21 @@ function renderTaskDefaults(models, providers, taskDefaults, settings) {
|
|
|
6785
6876
|
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
6786
6877
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
6787
6878
|
const availableModels = models.filter((model) => isSelectableModel(model));
|
|
6879
|
+
const imageModels = availableModels.filter((model) => model.multimodalEnabled && providerById.get(model.providerId)?.protocol === "openai-chat-completions");
|
|
6788
6880
|
const availableModelIds = new Set(availableModels.map((model) => model.id));
|
|
6789
6881
|
const currentDefaultModels = taskDefaults
|
|
6790
6882
|
.map((item) => item.model)
|
|
6791
6883
|
.filter((model) => model && !availableModelIds.has(model.id));
|
|
6792
6884
|
const optionModels = [...availableModels, ...currentDefaultModels];
|
|
6793
|
-
return optionModels.length ? `<section class="config-section">
|
|
6885
|
+
return optionModels.length || imageModels.length ? `<section class="config-section">
|
|
6794
6886
|
<div class="config-section-header"><div><h2>本书任务默认模型</h2><p>选择平台模型作为当前作品的默认模型;所有请求都会携带最大输出令牌数,默认值为 32000。</p></div></div>
|
|
6795
|
-
<table class="table-list"><thead><tr><th>任务能力</th><th>默认模型</th></tr></thead><tbody><tr><td
|
|
6887
|
+
<table class="table-list"><thead><tr><th>任务能力</th><th>默认模型</th></tr></thead><tbody><tr><td>多模态读图工具</td><td><select class="default-model-select" data-image-tool-default aria-label="多模态读图工具默认模型">
|
|
6888
|
+
<option value="" ${settings.imageToolModelId ? "" : "selected"}>跟随平台默认</option>
|
|
6889
|
+
${imageModels.map((model) => {
|
|
6890
|
+
const provider = providerById.get(model.providerId);
|
|
6891
|
+
return `<option value="${esc(model.id)}" ${model.id === settings.imageToolModelId ? "selected" : ""}>${esc(modelOptionLabel({ ...model, providerName: model.providerName || provider?.name }))}</option>`;
|
|
6892
|
+
}).join("")}
|
|
6893
|
+
</select></td></tr><tr><td>创作助手对话标题生成</td><td><select class="default-model-select" data-title-generation-default aria-label="创作助手对话标题生成">
|
|
6796
6894
|
<option value="" ${settings.titleGenerationModelId ? "" : "selected"}>使用提示词前 15 个字</option>
|
|
6797
6895
|
${models.map((model) => {
|
|
6798
6896
|
const provider = providerById.get(model.providerId);
|
|
@@ -6857,8 +6955,7 @@ function relationshipIndexStatusMarkup(status) {
|
|
|
6857
6955
|
function updateBackgroundTaskCenterVisibility() {
|
|
6858
6956
|
const button = $("#background-task-button");
|
|
6859
6957
|
if (!button) return;
|
|
6860
|
-
const visible = Boolean(state.
|
|
6861
|
-
&& (canReadModule("tasks") || canReadModule("ai-settings"));
|
|
6958
|
+
const visible = Boolean(state.user);
|
|
6862
6959
|
button.classList.toggle("hidden", !visible);
|
|
6863
6960
|
if (!visible) {
|
|
6864
6961
|
$("#background-task-count")?.classList.add("hidden");
|
|
@@ -6867,7 +6964,7 @@ function updateBackgroundTaskCenterVisibility() {
|
|
|
6867
6964
|
const activityCount = backgroundTaskActivityCount(
|
|
6868
6965
|
backgroundTaskCenterSnapshot.taskPage,
|
|
6869
6966
|
backgroundTaskCenterSnapshot.relationshipIndex
|
|
6870
|
-
);
|
|
6967
|
+
) + (productUpdateChecking ? 1 : 0);
|
|
6871
6968
|
const badge = $("#background-task-count");
|
|
6872
6969
|
badge.textContent = activityCount > 99 ? "99+" : String(activityCount);
|
|
6873
6970
|
badge.classList.toggle("hidden", activityCount === 0);
|
|
@@ -6876,6 +6973,37 @@ function updateBackgroundTaskCenterVisibility() {
|
|
|
6876
6973
|
button.setAttribute("title", activityCount > 0 ? `后台任务中心 · ${activityCount} 项进行中` : "后台任务中心");
|
|
6877
6974
|
}
|
|
6878
6975
|
|
|
6976
|
+
function backgroundProductUpdateMarkup() {
|
|
6977
|
+
const status = productUpdateStatus;
|
|
6978
|
+
const updateCheckDisabled = status?.enabled === false;
|
|
6979
|
+
const currentVersion = String(status?.currentVersion ?? "").trim();
|
|
6980
|
+
const latestVersion = String(status?.latestVersion ?? "").trim();
|
|
6981
|
+
const nextCheckAt = formatDateTime(status?.nextCheckAt) || "等待首次探测";
|
|
6982
|
+
const updateAvailable = status?.updateAvailable === true && Boolean(status?.releaseUrl);
|
|
6983
|
+
const badgeClass = productUpdateChecking ? "running" : updateCheckDisabled ? "unknown" : updateAvailable ? "partial" : status?.checked === true ? "completed" : status ? "unknown" : "pending";
|
|
6984
|
+
const badgeLabel = productUpdateChecking ? "探测中" : updateCheckDisabled ? "已关闭" : updateAvailable ? "有新版本" : status?.checked === true ? "已是最新" : status ? "本次未探测" : "等待探测";
|
|
6985
|
+
const detail = productUpdateChecking
|
|
6986
|
+
? "正在请求 GitHub 最新 Release"
|
|
6987
|
+
: updateCheckDisabled
|
|
6988
|
+
? "已通过服务端配置关闭版本更新探测"
|
|
6989
|
+
: updateAvailable
|
|
6990
|
+
? `当前 v${currentVersion || "—"} · 最新 v${latestVersion} · 下次探测 ${nextCheckAt}`
|
|
6991
|
+
: status?.checked === true
|
|
6992
|
+
? `当前 v${currentVersion || "—"} · 下次探测 ${nextCheckAt}`
|
|
6993
|
+
: status
|
|
6994
|
+
? `GitHub 暂不可用 · 下次探测 ${nextCheckAt}`
|
|
6995
|
+
: "应用启动后将在后台探测 GitHub Release";
|
|
6996
|
+
return `<section class="background-task-section">
|
|
6997
|
+
<div class="background-task-section-heading"><div><strong>版本更新探测</strong><small>按服务端配置定期检查 GitHub Release</small></div></div>
|
|
6998
|
+
<div class="background-task-list"><article class="background-task-row">
|
|
6999
|
+
<span class="task-status-badge is-${badgeClass}"><span class="task-status-indicator" aria-hidden="true"></span><span>${badgeLabel}</span></span>
|
|
7000
|
+
<div><strong>叙界版本更新</strong><small>${esc(detail)}</small></div>
|
|
7001
|
+
<span class="background-task-progress">${updateAvailable ? `v${esc(latestVersion)}` : "系统"}</span>
|
|
7002
|
+
${updateAvailable ? `<a class="ghost-button background-update-link" href="${esc(status.releaseUrl)}" target="_blank" rel="noopener noreferrer" aria-label="查看叙界 v${esc(latestVersion)} 更新说明">说明</a>` : '<span class="background-task-action-placeholder" aria-hidden="true"></span>'}
|
|
7003
|
+
</article></div>
|
|
7004
|
+
</section>`;
|
|
7005
|
+
}
|
|
7006
|
+
|
|
6879
7007
|
function backgroundTaskTransitionMessage(transition) {
|
|
6880
7008
|
const label = analysisTaskTypeLabel(transition.task.taskType);
|
|
6881
7009
|
if (transition.status === "partial") return { message: `${label}部分失败,请打开任务详情查看`, type: "error" };
|
|
@@ -6934,17 +7062,17 @@ function renderBackgroundTaskCenter() {
|
|
|
6934
7062
|
updateBackgroundTaskCenterVisibility();
|
|
6935
7063
|
const content = $("#background-task-content");
|
|
6936
7064
|
if (!content) return;
|
|
6937
|
-
content.innerHTML = `${backgroundTaskListMarkup(
|
|
7065
|
+
content.innerHTML = `${backgroundProductUpdateMarkup()}${state.work ? `${backgroundTaskListMarkup(
|
|
6938
7066
|
backgroundTaskCenterSnapshot.taskPage,
|
|
6939
7067
|
backgroundTaskCenterSnapshot.errors.tasks
|
|
6940
7068
|
)}${backgroundIndexMarkup(
|
|
6941
7069
|
backgroundTaskCenterSnapshot.relationshipIndex,
|
|
6942
7070
|
backgroundTaskCenterSnapshot.errors.index
|
|
6943
|
-
)}`;
|
|
7071
|
+
)}` : ""}`;
|
|
6944
7072
|
$("#background-task-dialog-meta").textContent = state.work
|
|
6945
|
-
?
|
|
6946
|
-
: "
|
|
6947
|
-
$("#background-task-open-analysis").classList.toggle("hidden", !canReadModule("tasks"));
|
|
7073
|
+
? `系统更新与《${state.work.title}》的分析任务、索引队列`
|
|
7074
|
+
: "系统更新探测状态";
|
|
7075
|
+
$("#background-task-open-analysis").classList.toggle("hidden", !state.work || !canReadModule("tasks"));
|
|
6948
7076
|
content.querySelectorAll("[data-background-task-detail]").forEach((button) => button.addEventListener("click", async () => {
|
|
6949
7077
|
button.disabled = true;
|
|
6950
7078
|
try {
|
|
@@ -7063,8 +7191,7 @@ function stopBackgroundTaskCenter() {
|
|
|
7063
7191
|
backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
7064
7192
|
const dialog = $("#background-task-dialog");
|
|
7065
7193
|
if (dialog?.open) dialog.close();
|
|
7066
|
-
|
|
7067
|
-
$("#background-task-count")?.classList.add("hidden");
|
|
7194
|
+
updateBackgroundTaskCenterVisibility();
|
|
7068
7195
|
}
|
|
7069
7196
|
|
|
7070
7197
|
function startBackgroundTaskCenter(workId) {
|
|
@@ -7081,7 +7208,14 @@ async function renderPlatformAiConfig() {
|
|
|
7081
7208
|
api("/api/platform/ai/settings")
|
|
7082
7209
|
]);
|
|
7083
7210
|
const host = $("#platform-ai-content");
|
|
7084
|
-
|
|
7211
|
+
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
7212
|
+
const imageModels = models.filter((model) => model.multimodalEnabled && providerById.get(model.providerId)?.protocol === "openai-chat-completions");
|
|
7213
|
+
const imageModelOptions = imageModels.map((model) => {
|
|
7214
|
+
const provider = providerById.get(model.providerId);
|
|
7215
|
+
const available = isSelectableModel({ ...model, providerStatus: provider?.status, providerConnectionStatus: provider?.connectionStatus });
|
|
7216
|
+
return `<option value="${esc(model.id)}" ${model.id === settings.imageToolModelId ? "selected" : ""} ${available || model.id === settings.imageToolModelId ? "" : "disabled"}>${esc(`${available ? "" : "不可用 · "}${modelOptionLabel({ ...model, providerName: model.providerName || provider?.name })}`)}</option>`;
|
|
7217
|
+
}).join("");
|
|
7218
|
+
host.innerHTML = `<section class="config-section platform-system-prompt-section"><div class="config-section-header"><div><h2>平台全局系统提示词</h2><p>会追加在内置系统提示词之后,并在所有作品的专属提示词之前发送给模型。</p></div></div><div class="field-label"><textarea id="platform-system-prompt" rows="7" aria-label="全局系统提示词" placeholder="例如:默认使用简体中文,避免代替作者做最终决定。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-platform-system-prompt" class="primary-button">保存全局提示词</button></div></section><section class="config-section platform-image-tool-section"><div class="config-section-header"><div><h2>多模态读图默认模型</h2><p>Agent 的 image 工具使用这里配置的模型读取设定库图片;作品可以在自己的 AI 设置中覆盖此选择。</p></div></div><div class="platform-image-tool-panel"><label class="platform-image-tool-field"><span>当前平台默认模型</span><select id="platform-image-tool-model" aria-label="平台多模态读图默认模型"><option value="">未配置</option>${imageModelOptions}</select></label><button id="save-platform-image-tool-model" class="ghost-button config-save-button" type="button">保存默认模型</button></div></section><section class="config-section platform-providers-section"><div class="config-section-header"><div><h2>模型供应商配置</h2><p>管理供应商连接、模型列表和连接状态;模型的多模态能力在对应模型配置中设置。</p></div></div>${renderProviderCards(providers, models)}</section>`;
|
|
7085
7219
|
$("#save-platform-system-prompt").addEventListener("click", async () => {
|
|
7086
7220
|
const button = $("#save-platform-system-prompt");
|
|
7087
7221
|
button.disabled = true;
|
|
@@ -7094,6 +7228,19 @@ async function renderPlatformAiConfig() {
|
|
|
7094
7228
|
button.disabled = false;
|
|
7095
7229
|
}
|
|
7096
7230
|
});
|
|
7231
|
+
$("#save-platform-image-tool-model").addEventListener("click", async () => {
|
|
7232
|
+
const button = $("#save-platform-image-tool-model");
|
|
7233
|
+
button.disabled = true;
|
|
7234
|
+
try {
|
|
7235
|
+
await api("/api/platform/ai/settings", { method: "PATCH", body: { imageToolModelId: $("#platform-image-tool-model").value || null } });
|
|
7236
|
+
toast("平台多模态读图默认模型已更新");
|
|
7237
|
+
await renderPlatformAiConfig();
|
|
7238
|
+
} catch (error) {
|
|
7239
|
+
toast(error.message, "error");
|
|
7240
|
+
} finally {
|
|
7241
|
+
button.disabled = false;
|
|
7242
|
+
}
|
|
7243
|
+
});
|
|
7097
7244
|
bindPlatformProviderActions(host, providers, models);
|
|
7098
7245
|
}
|
|
7099
7246
|
|
|
@@ -7253,7 +7400,7 @@ async function renderBookAiSettings() {
|
|
|
7253
7400
|
]);
|
|
7254
7401
|
const host = $("#module-content");
|
|
7255
7402
|
const workId = String(state.work.id);
|
|
7256
|
-
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts"]);
|
|
7403
|
+
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image"]);
|
|
7257
7404
|
const dailyTokenQuota = settings.dailyTokenQuota === null ? null : Number(settings.dailyTokenQuota);
|
|
7258
7405
|
const quotaUsedTokens = Number(usage?.quota?.usedTokens) || 0;
|
|
7259
7406
|
const quotaRemainingTokens = usage?.quota?.remainingTokens === null
|
|
@@ -7280,7 +7427,7 @@ async function renderBookAiSettings() {
|
|
|
7280
7427
|
);
|
|
7281
7428
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
7282
7429
|
"beforeend",
|
|
7283
|
-
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索想法</strong><small>查询正文想法和设定想法。这些内容只是可能采用、也可能永远不会进入正文或正式设定的临时方向,Agent 不会把它当作已确认事实。</small></span></label>`
|
|
7430
|
+
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索想法</strong><small>查询正文想法和设定想法。这些内容只是可能采用、也可能永远不会进入正文或正式设定的临时方向,Agent 不会把它当作已确认事实。</small></span></label><label><input name="agent-tool" type="checkbox" value="image" ${agentTools.has("image") ? "checked" : ""}><span><strong>读取设定图片</strong><small>读取设定正文引用的单张图片附件,并由多模态模型返回图片理解内容。</small></span></label>`
|
|
7284
7431
|
);
|
|
7285
7432
|
if (!canEditModule("ai-settings")) {
|
|
7286
7433
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -7485,6 +7632,18 @@ async function renderBookAiSettings() {
|
|
|
7485
7632
|
await renderBookAiSettings();
|
|
7486
7633
|
await loadModels();
|
|
7487
7634
|
});
|
|
7635
|
+
host.querySelector("[data-image-tool-default]")?.addEventListener("change", async (event) => {
|
|
7636
|
+
const select = event.currentTarget;
|
|
7637
|
+
select.disabled = true;
|
|
7638
|
+
try {
|
|
7639
|
+
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { imageToolModelId: select.value || null } });
|
|
7640
|
+
toast("本书多模态读图模型已更新");
|
|
7641
|
+
} catch (error) {
|
|
7642
|
+
toast(error.message, "error");
|
|
7643
|
+
}
|
|
7644
|
+
await renderBookAiSettings();
|
|
7645
|
+
await loadModels();
|
|
7646
|
+
});
|
|
7488
7647
|
host.querySelectorAll("[data-task-default]").forEach((select) => select.addEventListener("change", async () => {
|
|
7489
7648
|
select.disabled = true;
|
|
7490
7649
|
try {
|
|
@@ -7692,7 +7851,7 @@ async function ensureAiReferencesLoaded() {
|
|
|
7692
7851
|
|
|
7693
7852
|
function field(name, label, type = "text", value = "", options = []) {
|
|
7694
7853
|
if (type === "textarea") return `<label>${esc(label)}<textarea name="${esc(name)}">${esc(value)}</textarea></label>`;
|
|
7695
|
-
if (type === "markdown") return `<div class="form-field markdown-editor-field" data-vditor-editor-field><span>${esc(label)}</span><div class="vditor-editor-host" data-vditor-editor data-attachment-module="${esc(options.attachmentModule ?? "settings")}" data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="${esc(label)} Markdown 编辑器"></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="${esc(label)} Markdown 原文" ${options.readOnly ? "readonly" : ""}>${esc(value)}</textarea></div>`;
|
|
7854
|
+
if (type === "markdown") return `<div class="form-field markdown-editor-field${options.readOnly ? " is-read-only" : ""}" data-vditor-editor-field><span>${esc(label)}</span><div class="vditor-editor-host" data-vditor-editor data-attachment-module="${esc(options.attachmentModule ?? "settings")}" data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="${esc(label)} Markdown 编辑器"${options.readOnly ? ' aria-readonly="true"' : ""}></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="${esc(label)} Markdown 原文" ${options.readOnly ? "readonly" : ""}>${esc(value)}</textarea></div>`;
|
|
7696
7855
|
if (type === "item-list") {
|
|
7697
7856
|
const values = Array.isArray(value) && value.length ? value : [""];
|
|
7698
7857
|
return `<div class="form-field item-list-field"><span>${esc(label)}</span><div class="item-list-rows" data-item-list-rows data-name="${esc(name)}" data-label="${esc(label)}">${values.map((item) => `<div class="item-list-row"><input name="${esc(name)}" value="${esc(item)}" aria-label="${esc(label)}"><button type="button" data-item-list-remove aria-label="删除此条">删除</button></div>`).join("")}</div><button class="item-list-add" type="button" data-item-list-add>添加一条</button></div>`;
|
|
@@ -8295,6 +8454,15 @@ function activateCharacterEditorTab(key) {
|
|
|
8295
8454
|
button.tabIndex = active ? 0 : -1;
|
|
8296
8455
|
});
|
|
8297
8456
|
document.querySelectorAll("[data-character-editor-panel]").forEach((panel) => panel.classList.toggle("hidden", panel.dataset.characterEditorPanel !== key));
|
|
8457
|
+
if (
|
|
8458
|
+
key === "relationships"
|
|
8459
|
+
&& characterEditorItem?.id
|
|
8460
|
+
&& canReadModule("relationships")
|
|
8461
|
+
&& !characterEditorRelationshipsLoaded
|
|
8462
|
+
&& !characterEditorRelationshipsLoading
|
|
8463
|
+
) {
|
|
8464
|
+
void loadCharacterEditorRelationships(characterEditorItem.id);
|
|
8465
|
+
}
|
|
8298
8466
|
}
|
|
8299
8467
|
|
|
8300
8468
|
function setCharacterHistoryVisible(visible) {
|
|
@@ -8316,6 +8484,10 @@ function renderCharacterEditorRelationships() {
|
|
|
8316
8484
|
host.innerHTML = '<p class="character-relationship-status">正在读取人物关系……</p>';
|
|
8317
8485
|
return;
|
|
8318
8486
|
}
|
|
8487
|
+
if (!characterEditorRelationshipsLoaded) {
|
|
8488
|
+
host.innerHTML = '<p class="character-relationship-status">打开人物关系分区后载入关系。</p>';
|
|
8489
|
+
return;
|
|
8490
|
+
}
|
|
8319
8491
|
const characterId = String(characterEditorItem.id);
|
|
8320
8492
|
const nameOf = (id) => state.characters.find((character) => character.id === id)?.name ?? "未知角色";
|
|
8321
8493
|
const rows = characterEditorRelationships.map((relationship) => {
|
|
@@ -8338,20 +8510,23 @@ function renderCharacterEditorRelationships() {
|
|
|
8338
8510
|
host.querySelector("[data-character-relationship-create]")?.addEventListener("click", () => void openRelationshipDialog(null, { characterId }));
|
|
8339
8511
|
}
|
|
8340
8512
|
|
|
8341
|
-
async function loadCharacterEditorRelationships(characterId) {
|
|
8513
|
+
async function loadCharacterEditorRelationships(characterId, { refresh = false } = {}) {
|
|
8342
8514
|
const workId = state.work?.id;
|
|
8343
8515
|
if (!workId || characterEditorItem?.id !== characterId) return;
|
|
8516
|
+
if (!refresh && (characterEditorRelationshipsLoaded || characterEditorRelationshipsLoading)) return;
|
|
8344
8517
|
characterEditorRelationshipsLoading = true;
|
|
8518
|
+
characterEditorRelationshipsLoaded = false;
|
|
8345
8519
|
renderCharacterEditorRelationships();
|
|
8346
8520
|
let loaded = false;
|
|
8347
8521
|
try {
|
|
8348
8522
|
const [characters, relationships] = await Promise.all([
|
|
8349
|
-
|
|
8350
|
-
|
|
8523
|
+
moduleApiAllPages("characters", `/api/works/${workId}/characters`),
|
|
8524
|
+
moduleApiAllPages("relationships", `/api/works/${workId}/relationships`)
|
|
8351
8525
|
]);
|
|
8352
8526
|
if (state.work?.id !== workId || characterEditorItem?.id !== characterId) return;
|
|
8353
8527
|
state.characters = characters;
|
|
8354
8528
|
characterEditorRelationships = relationships.filter((relationship) => relationship.fromCharacterId === characterId || relationship.toCharacterId === characterId);
|
|
8529
|
+
characterEditorRelationshipsLoaded = true;
|
|
8355
8530
|
loaded = true;
|
|
8356
8531
|
} catch (error) {
|
|
8357
8532
|
if (state.work?.id === workId && characterEditorItem?.id === characterId) {
|
|
@@ -8369,7 +8544,7 @@ async function refreshRelationshipSurfaces(characterId = null) {
|
|
|
8369
8544
|
const tasks = [];
|
|
8370
8545
|
if (state.module === "relationships") tasks.push(renderRelationships());
|
|
8371
8546
|
if (characterId && entityEditorType === "character" && !$("#entity-editor-view").classList.contains("hidden") && characterEditorItem?.id === characterId) {
|
|
8372
|
-
tasks.push(loadCharacterEditorRelationships(characterId));
|
|
8547
|
+
tasks.push(loadCharacterEditorRelationships(characterId, { refresh: true }));
|
|
8373
8548
|
}
|
|
8374
8549
|
await Promise.all(tasks);
|
|
8375
8550
|
}
|
|
@@ -9140,15 +9315,15 @@ async function showCharacterHistory() {
|
|
|
9140
9315
|
|
|
9141
9316
|
async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
9142
9317
|
entityEditorReadOnly = readOnly;
|
|
9143
|
-
[state.races, state.organizations
|
|
9318
|
+
[state.races, state.organizations] = await Promise.all([
|
|
9144
9319
|
canReadModule("races") ? api(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
9145
|
-
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
9146
|
-
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
9320
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
9147
9321
|
]);
|
|
9148
9322
|
characterEditorItem = item ?? null;
|
|
9149
9323
|
characterEditorVersions = [];
|
|
9150
9324
|
characterEditorRelationships = [];
|
|
9151
|
-
characterEditorRelationshipsLoading =
|
|
9325
|
+
characterEditorRelationshipsLoading = false;
|
|
9326
|
+
characterEditorRelationshipsLoaded = false;
|
|
9152
9327
|
characterEditorSections = [];
|
|
9153
9328
|
$("#character-editor-eyebrow").textContent = item ? "人物主档案" : "建立人物档案";
|
|
9154
9329
|
$("#character-editor-title").textContent = item?.name || "新建角色";
|
|
@@ -9160,20 +9335,30 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
|
9160
9335
|
const characterMergeButton = $("#character-merge-button");
|
|
9161
9336
|
const characterDeleteButton = $("#character-delete-button");
|
|
9162
9337
|
const canManageCharacter = Boolean(item && !readOnly && canEditModule("characters"));
|
|
9163
|
-
characterMergeButton.classList.toggle("hidden", !canManageCharacter
|
|
9338
|
+
characterMergeButton.classList.toggle("hidden", !canManageCharacter);
|
|
9164
9339
|
characterDeleteButton.classList.toggle("hidden", !canManageCharacter);
|
|
9165
9340
|
characterMergeButton.onclick = async () => {
|
|
9166
9341
|
if (!item) return;
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9342
|
+
const workId = state.work?.id;
|
|
9343
|
+
if (!workId) return;
|
|
9344
|
+
try {
|
|
9345
|
+
const candidates = await moduleApiAllPages("characters", `/api/works/${workId}/characters`);
|
|
9346
|
+
if (state.work?.id !== workId || characterEditorItem?.id !== item.id) return;
|
|
9347
|
+
state.characters = candidates;
|
|
9348
|
+
if (candidates.length < 2) return toast("至少需要两个角色才能合并", "error");
|
|
9349
|
+
await closeEntityEditor({ force: true });
|
|
9350
|
+
openEntityMergeDialog({
|
|
9351
|
+
typeLabel: "角色",
|
|
9352
|
+
source: item,
|
|
9353
|
+
candidates,
|
|
9354
|
+
endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}/merge`,
|
|
9355
|
+
body: (target) => ({ targetCharacterId: target.id, expectedTargetVersionNo: target.versionNo, expectedSourceVersionNo: item.versionNo }),
|
|
9356
|
+
refresh: renderCharacters,
|
|
9357
|
+
impact: "来源角色的别名、组织、档案章节、时间线与人物关系会迁移到目标角色。"
|
|
9358
|
+
});
|
|
9359
|
+
} catch (error) {
|
|
9360
|
+
toast(error.message, "error");
|
|
9361
|
+
}
|
|
9177
9362
|
};
|
|
9178
9363
|
characterDeleteButton.onclick = () => {
|
|
9179
9364
|
if (!item) return;
|
|
@@ -9231,7 +9416,6 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
|
9231
9416
|
};
|
|
9232
9417
|
showEntityEditorPage("character", { readOnly });
|
|
9233
9418
|
if (item) {
|
|
9234
|
-
if (canReadModule("relationships")) void loadCharacterEditorRelationships(item.id);
|
|
9235
9419
|
void loadCharacterMarkdownSections(item.id);
|
|
9236
9420
|
}
|
|
9237
9421
|
}
|
|
@@ -9263,7 +9447,7 @@ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
|
|
|
9263
9447
|
+ field("description", `${label}简介`, "textarea", item?.description, []);
|
|
9264
9448
|
const memberField = memberOptions.length
|
|
9265
9449
|
? field("memberIds", isRace ? "属于该种族的角色(可多选)" : "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions)
|
|
9266
|
-
: `<div class="character-editor-empty-field"><strong>${isRace ? "种族成员" : "组织成员"}</strong><span
|
|
9450
|
+
: `<div class="character-editor-empty-field"><strong>${isRace ? "种族成员" : "组织成员"}</strong><span>${readOnly ? "该档案尚未绑定角色。" : "当前还没有可绑定的角色。"}</span></div>`;
|
|
9267
9451
|
$("#knowledge-editor-fields").innerHTML = knowledgeEditorSection("basic", "基础资料", isRace ? "先定义名称、层级和简介,再补充共同设定。" : "先定义组织名称和简介,再补充完整的组织设定。", basicFields)
|
|
9268
9452
|
+ knowledgeEditorSection("settings", title, "", '<div id="knowledge-markdown-sections" class="knowledge-markdown-sections"></div>')
|
|
9269
9453
|
+ knowledgeEditorSection("members", isRace ? "种族成员" : "组织成员", "成员关系会同步到角色档案中。", memberField);
|
|
@@ -9278,8 +9462,13 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
|
|
|
9278
9462
|
entityEditorReadOnly = readOnly;
|
|
9279
9463
|
await discardPendingMarkdownAttachments();
|
|
9280
9464
|
if (kind === "race" && !(await ensureCompleteRaceList())) return;
|
|
9281
|
-
|
|
9282
|
-
|
|
9465
|
+
const memberCharacters = readOnly
|
|
9466
|
+
? (Array.isArray(item?.members) ? item.members : []).map((member) => ({ id: member.characterId, name: member.name, aliases: [] }))
|
|
9467
|
+
: canReadModule("characters")
|
|
9468
|
+
? await moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`)
|
|
9469
|
+
: [];
|
|
9470
|
+
if (!readOnly) state.characters = memberCharacters;
|
|
9471
|
+
const memberOptions = memberCharacters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
9283
9472
|
const isRace = kind === "race";
|
|
9284
9473
|
const module = isRace ? "races" : "organizations";
|
|
9285
9474
|
const label = isRace ? "种族" : "组织";
|
|
@@ -9588,7 +9777,7 @@ function openTimelineSplitDialog(item) {
|
|
|
9588
9777
|
|
|
9589
9778
|
async function openRelationshipDialog(item, options = {}) {
|
|
9590
9779
|
if (!canReadModule("characters")) return toast("配置人物关系前需要角色模块读取权限", "error");
|
|
9591
|
-
state.characters = await
|
|
9780
|
+
state.characters = await moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`);
|
|
9592
9781
|
if (state.characters.length < 2) return toast("至少需要两个角色才能创建关系", "error");
|
|
9593
9782
|
const characterOptions = state.characters.map((item) => [item.id, item.name]);
|
|
9594
9783
|
const defaultFrom = options.characterId && state.characters.some((character) => character.id === options.characterId) ? options.characterId : characterOptions[0][0];
|
|
@@ -10016,18 +10205,23 @@ function openProviderDialog(item) {
|
|
|
10016
10205
|
protocolSelect.addEventListener("change", syncProviderCredentialField);
|
|
10017
10206
|
}
|
|
10018
10207
|
|
|
10019
|
-
function openModelDialog(providerId, item = null) {
|
|
10208
|
+
function openModelDialog(providerId, item = null, provider = null) {
|
|
10020
10209
|
const values = modelFormValues(item);
|
|
10210
|
+
const imageDefaultSupported = supportsMultimodalModelProtocol(provider?.protocol);
|
|
10211
|
+
const multimodalFields = imageDefaultSupported ? `<div class="form-field model-multimodal-fields" role="group" aria-labelledby="model-multimodal-heading"><span id="model-multimodal-heading" class="model-multimodal-heading">模型能力</span><label class="checkbox-field model-capability-option"><input id="model-multimodal-enabled" name="multimodalEnabled" type="checkbox" ${values.multimodalEnabled ? "checked" : ""}><span><strong>支持多模态图片理解</strong><small>启用后可用于读取设定库中的图片附件。</small></span></label><label id="model-image-tool-default-field" class="checkbox-field model-capability-option ${values.multimodalEnabled ? "" : "hidden"}"><input id="model-image-tool-default" name="imageToolDefault" type="checkbox" ${values.imageToolDefault ? "checked" : ""}><span><strong>设为多模态读图工具默认模型</strong><small>平台默认模型只能由 Chat Completions 协议提供。</small></span></label><small class="model-multimodal-note">当前供应商支持多模态读图工具默认模型。</small></div>` : "";
|
|
10021
10212
|
const contextWindowField = `<div class="form-field model-context-window-field"><label for="model-context-window">模型上下文令牌总量<input id="model-context-window" name="contextWindow" type="number" value="${esc(values.contextWindow)}" min="${MIN_MODEL_CONTEXT_WINDOW}" max="2000000" step="1" required aria-describedby="model-context-window-hint"></label><small id="model-context-window-hint" class="model-context-window-hint" hidden>低于 128K 的模型在小说创作场景不太适用,建议使用支持更长上下文的模型。</small></div>`;
|
|
10022
10213
|
const temperatureField = `<div class="form-field model-temperature-field"><label for="model-temperature">默认温度<input id="model-temperature" name="temperature" type="number" value="${esc(values.temperature)}" step="any" aria-describedby="model-temperature-hint"></label><small id="model-temperature-hint" class="model-temperature-hint" hidden>Kimi 模型必须设置温度为 1。</small></div>`;
|
|
10214
|
+
const connectionTestDescription = values.multimodalEnabled && imageDefaultSupported
|
|
10215
|
+
? "使用当前已保存的模型标识符和供应商凭据,并发送一张测试图片验证图片请求。"
|
|
10216
|
+
: "使用当前已保存的模型标识符和供应商凭据发起最小请求。";
|
|
10023
10217
|
const connectionTest = item && item.providerStatus === "enabled"
|
|
10024
10218
|
? `<section class="model-connection-test">
|
|
10025
|
-
<div><strong>模型连接测试</strong><p
|
|
10219
|
+
<div><strong>模型连接测试</strong><p>${connectionTestDescription}</p></div>
|
|
10026
10220
|
<button class="ghost-button" type="button" data-test-model="${esc(item.id)}">测试连接</button>
|
|
10027
10221
|
</section>`
|
|
10028
10222
|
: "";
|
|
10029
|
-
openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + contextWindowField + temperatureField + field("maxTokens", "默认最大输出令牌数", "number", values.maxTokens) + field("thinkingEnabled", "开启思考模式(供应商需支持相应参数)", "checkbox", values.thinkingEnabled) + field("enabled", "启用模型", "checkbox", values.enabled) + connectionTest, async (form) => {
|
|
10030
|
-
const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
|
|
10223
|
+
openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + contextWindowField + temperatureField + field("maxTokens", "默认最大输出令牌数", "number", values.maxTokens) + field("thinkingEnabled", "开启思考模式(供应商需支持相应参数)", "checkbox", values.thinkingEnabled) + multimodalFields + field("enabled", "启用模型", "checkbox", values.enabled) + connectionTest, async (form) => {
|
|
10224
|
+
const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", multimodalEnabled: form.get("multimodalEnabled") === "on", imageToolDefault: form.get("imageToolDefault") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
|
|
10031
10225
|
await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
|
|
10032
10226
|
await renderPlatformAiConfig();
|
|
10033
10227
|
await loadModels();
|
|
@@ -10037,6 +10231,16 @@ function openModelDialog(providerId, item = null) {
|
|
|
10037
10231
|
const contextWindowHint = $("#model-context-window-hint");
|
|
10038
10232
|
const temperatureInput = $("#dialog-fields input[name='temperature']");
|
|
10039
10233
|
const temperatureHint = $("#model-temperature-hint");
|
|
10234
|
+
const multimodalInput = $("#model-multimodal-enabled");
|
|
10235
|
+
const imageDefaultField = $("#model-image-tool-default-field");
|
|
10236
|
+
const imageDefaultInput = $("#model-image-tool-default");
|
|
10237
|
+
const syncMultimodalFields = () => {
|
|
10238
|
+
if (!multimodalInput || !imageDefaultField || !imageDefaultInput) return;
|
|
10239
|
+
const hideImageDefault = !multimodalInput.checked || !imageDefaultSupported;
|
|
10240
|
+
imageDefaultField.hidden = hideImageDefault;
|
|
10241
|
+
imageDefaultField.classList.toggle("hidden", hideImageDefault);
|
|
10242
|
+
if (!multimodalInput.checked) imageDefaultInput.checked = false;
|
|
10243
|
+
};
|
|
10040
10244
|
const syncModelContextWindowGuidance = () => {
|
|
10041
10245
|
const guidance = modelContextWindowGuidance(contextWindowInput.value);
|
|
10042
10246
|
contextWindowInput.setCustomValidity(guidance.belowMinimum ? "模型上下文不能低于 32K(32768 Token)。" : "");
|
|
@@ -10051,13 +10255,14 @@ function openModelDialog(providerId, item = null) {
|
|
|
10051
10255
|
syncKimiTemperature();
|
|
10052
10256
|
});
|
|
10053
10257
|
contextWindowInput.addEventListener("input", syncModelContextWindowGuidance);
|
|
10258
|
+
multimodalInput?.addEventListener("change", syncMultimodalFields);
|
|
10054
10259
|
$("#dialog-fields [data-test-model]")?.addEventListener("click", async (event) => {
|
|
10055
10260
|
const button = event.currentTarget;
|
|
10056
10261
|
button.disabled = true;
|
|
10057
10262
|
button.textContent = "测试中";
|
|
10058
10263
|
try {
|
|
10059
10264
|
const result = await api(`/api/models/${button.dataset.testModel}/test`, { method: "POST", body: {} });
|
|
10060
|
-
toast(result.ok ? "模型连接测试成功" : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
10265
|
+
toast(result.ok ? (result.multimodalTested ? "模型连接测试成功,图片请求已验证" : "模型连接测试成功") : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
10061
10266
|
await renderPlatformAiConfig();
|
|
10062
10267
|
await loadModels();
|
|
10063
10268
|
} catch (error) {
|
|
@@ -10069,6 +10274,7 @@ function openModelDialog(providerId, item = null) {
|
|
|
10069
10274
|
});
|
|
10070
10275
|
syncModelContextWindowGuidance();
|
|
10071
10276
|
syncKimiTemperature();
|
|
10277
|
+
syncMultimodalFields();
|
|
10072
10278
|
}
|
|
10073
10279
|
|
|
10074
10280
|
async function sendAi() {
|
|
@@ -10773,17 +10979,7 @@ function hasUnsavedEditorChanges() {
|
|
|
10773
10979
|
}
|
|
10774
10980
|
|
|
10775
10981
|
function redirectToLoginAfterSystemRestart() {
|
|
10776
|
-
|
|
10777
|
-
state.csrfToken = null;
|
|
10778
|
-
moduleRequestCache.clear();
|
|
10779
|
-
document.documentElement.classList.remove("dev-auth-bypass");
|
|
10780
|
-
document.documentElement.classList.add("login-route");
|
|
10781
|
-
window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
10782
|
-
const toastRegion = $("#toast-region");
|
|
10783
|
-
toastRegion.replaceChildren();
|
|
10784
|
-
if (typeof toastRegion.hidePopover === "function" && toastRegion.matches(":popover-open")) toastRegion.hidePopover();
|
|
10785
|
-
$("#system-restart-dialog").close();
|
|
10786
|
-
showAuth(false);
|
|
10982
|
+
invalidateAuthentication();
|
|
10787
10983
|
}
|
|
10788
10984
|
|
|
10789
10985
|
$("#system-restart-confirm").addEventListener("click", redirectToLoginAfterSystemRestart);
|
|
@@ -11967,13 +12163,14 @@ $("#background-task-button").addEventListener("click", () => {
|
|
|
11967
12163
|
const dialog = $("#background-task-dialog");
|
|
11968
12164
|
if (!dialog.open) dialog.showModal();
|
|
11969
12165
|
void refreshBackgroundTaskCenter();
|
|
12166
|
+
void refreshProductUpdate();
|
|
11970
12167
|
});
|
|
11971
12168
|
$("#background-task-dialog").addEventListener("close", scheduleBackgroundTaskCenterRefresh);
|
|
11972
12169
|
$("#background-task-refresh").addEventListener("click", async () => {
|
|
11973
12170
|
const button = $("#background-task-refresh");
|
|
11974
12171
|
button.disabled = true;
|
|
11975
12172
|
try {
|
|
11976
|
-
await refreshBackgroundTaskCenter();
|
|
12173
|
+
await Promise.all([refreshBackgroundTaskCenter(), refreshProductUpdate()]);
|
|
11977
12174
|
} finally {
|
|
11978
12175
|
button.disabled = false;
|
|
11979
12176
|
}
|