@musnows/scriverse 0.6.7 → 0.6.9
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 +461 -49
- package/dist/ai.js.map +1 -1
- package/dist/app.js +35 -7
- package/dist/app.js.map +1 -1
- package/dist/database.js +174 -2
- package/dist/database.js.map +1 -1
- package/dist/hybrid-search.js +2 -1
- package/dist/hybrid-search.js.map +1 -1
- package/dist/public/app.js +302 -89
- package/dist/public/display-labels.js +2 -1
- package/dist/public/global-search.d.ts +3 -1
- package/dist/public/global-search.js +22 -0
- package/dist/public/index.html +7 -4
- package/dist/public/model-config.d.ts +3 -0
- package/dist/public/model-config.js +8 -0
- package/dist/public/styles.css +44 -2
- 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 +99 -18
- 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/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";
|
|
@@ -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-search-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 {
|
|
@@ -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;
|
|
@@ -1803,8 +1812,10 @@ async function ensureAiConversationsLoaded() {
|
|
|
1803
1812
|
}
|
|
1804
1813
|
}
|
|
1805
1814
|
|
|
1806
|
-
async function openAiConversation(conversationId, hideHistory = true) {
|
|
1807
|
-
const
|
|
1815
|
+
async function openAiConversation(conversationId, hideHistory = true, focusMessageId = null) {
|
|
1816
|
+
const parameters = new URLSearchParams({ page: "1", limit: "100" });
|
|
1817
|
+
if (focusMessageId) parameters.set("messageId", String(focusMessageId));
|
|
1818
|
+
const conversation = await api(`/api/ai-conversations/${conversationId}?${parameters}`);
|
|
1808
1819
|
upsertAiConversationSummary(conversation);
|
|
1809
1820
|
state.aiConversationId = conversation.id;
|
|
1810
1821
|
state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
|
|
@@ -1830,6 +1841,15 @@ async function openAiConversation(conversationId, hideHistory = true) {
|
|
|
1830
1841
|
if (hideHistory) setAiHistoryVisible(false);
|
|
1831
1842
|
}
|
|
1832
1843
|
|
|
1844
|
+
function focusAiConversationMessage(messageId) {
|
|
1845
|
+
const message = [...$("#ai-feed").querySelectorAll("[data-message-id]")]
|
|
1846
|
+
.find((candidate) => candidate.dataset.messageId === String(messageId));
|
|
1847
|
+
if (!message) return;
|
|
1848
|
+
message.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1849
|
+
message.classList.add("is-search-target");
|
|
1850
|
+
window.setTimeout(() => message.classList.remove("is-search-target"), 1800);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1833
1853
|
async function createNewAiConversation(taskType = "chat") {
|
|
1834
1854
|
if (!state.work) return;
|
|
1835
1855
|
const conversation = await api(`/api/works/${state.work.id}/ai-conversations`, { method: "POST", body: { taskType } });
|
|
@@ -2507,10 +2527,7 @@ async function api(path, options = {}) {
|
|
|
2507
2527
|
if (response.status === 401 && !path.startsWith("/api/auth/") && !path.includes("/presence")) {
|
|
2508
2528
|
const restarted = await checkSystemBoot(true);
|
|
2509
2529
|
if (!restarted) {
|
|
2510
|
-
|
|
2511
|
-
state.csrfToken = null;
|
|
2512
|
-
moduleRequestCache.clear();
|
|
2513
|
-
showAuth(false);
|
|
2530
|
+
invalidateAuthentication();
|
|
2514
2531
|
}
|
|
2515
2532
|
}
|
|
2516
2533
|
throw createClientError(payload.error, `请求失败:${response.status}`, response.status);
|
|
@@ -2650,6 +2667,35 @@ function applyProductHealthMetadata(health) {
|
|
|
2650
2667
|
});
|
|
2651
2668
|
}
|
|
2652
2669
|
|
|
2670
|
+
function applyProductUpdateMetadata(update) {
|
|
2671
|
+
if (update && typeof update === "object") productUpdateStatus = update;
|
|
2672
|
+
if (update?.checked !== true) {
|
|
2673
|
+
renderBackgroundTaskCenter();
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
const latestVersion = String(update.latestVersion ?? "").trim();
|
|
2677
|
+
const releaseUrl = String(update.releaseUrl ?? "").trim();
|
|
2678
|
+
const updateAvailable = update.updateAvailable === true && Boolean(latestVersion) && Boolean(releaseUrl);
|
|
2679
|
+
const updateDot = document.querySelector("[data-settings-update-dot]");
|
|
2680
|
+
updateDot?.classList.toggle("hidden", !updateAvailable);
|
|
2681
|
+
const settingsButton = $("#settings-button");
|
|
2682
|
+
settingsButton.setAttribute("aria-label", updateAvailable ? `设置,有新版本 v${latestVersion}` : "设置");
|
|
2683
|
+
settingsButton.title = updateAvailable ? `设置,有新版本 v${latestVersion}` : "设置";
|
|
2684
|
+
document.querySelectorAll("[data-product-footer-update]").forEach((element) => {
|
|
2685
|
+
element.classList.toggle("hidden", !updateAvailable);
|
|
2686
|
+
if (updateAvailable) {
|
|
2687
|
+
element.textContent = `发现新版本 v${latestVersion},查看更新说明`;
|
|
2688
|
+
element.href = releaseUrl;
|
|
2689
|
+
element.setAttribute("aria-label", `查看叙界 v${latestVersion} 更新说明`);
|
|
2690
|
+
} else {
|
|
2691
|
+
element.textContent = "";
|
|
2692
|
+
element.removeAttribute("href");
|
|
2693
|
+
element.removeAttribute("aria-label");
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
renderBackgroundTaskCenter();
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2653
2699
|
function scheduleSystemHealthCheck() {
|
|
2654
2700
|
if (systemHealthTimer !== null) window.clearTimeout(systemHealthTimer);
|
|
2655
2701
|
systemHealthTimer = window.setTimeout(() => {
|
|
@@ -2669,11 +2715,44 @@ async function refreshSystemHealth() {
|
|
|
2669
2715
|
}
|
|
2670
2716
|
}
|
|
2671
2717
|
|
|
2718
|
+
function scheduleProductUpdateCheck() {
|
|
2719
|
+
if (productUpdateTimer !== null) window.clearTimeout(productUpdateTimer);
|
|
2720
|
+
if (productUpdateStatus?.enabled === false) {
|
|
2721
|
+
productUpdateTimer = null;
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
const nextCheckAt = Date.parse(String(productUpdateStatus?.nextCheckAt ?? ""));
|
|
2725
|
+
const delay = Number.isFinite(nextCheckAt)
|
|
2726
|
+
? Math.max(1000, nextCheckAt - Date.now())
|
|
2727
|
+
: fallbackProductUpdatePollInterval;
|
|
2728
|
+
productUpdateTimer = window.setTimeout(() => {
|
|
2729
|
+
productUpdateTimer = null;
|
|
2730
|
+
void refreshProductUpdate();
|
|
2731
|
+
}, Math.min(delay, 2_147_483_647));
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
async function refreshProductUpdate() {
|
|
2735
|
+
if (productUpdateChecking || productUpdateStatus?.enabled === false) return;
|
|
2736
|
+
productUpdateChecking = true;
|
|
2737
|
+
renderBackgroundTaskCenter();
|
|
2738
|
+
try {
|
|
2739
|
+
applyProductUpdateMetadata(await api("/api/update-check"));
|
|
2740
|
+
} catch {
|
|
2741
|
+
// 更新探测失败时静默保留当前展示状态
|
|
2742
|
+
} finally {
|
|
2743
|
+
productUpdateChecking = false;
|
|
2744
|
+
renderBackgroundTaskCenter();
|
|
2745
|
+
scheduleProductUpdateCheck();
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2672
2749
|
async function initializeProductFooters() {
|
|
2673
2750
|
const year = String(new Date().getFullYear());
|
|
2674
2751
|
document.querySelectorAll("[data-product-footer-year]").forEach((element) => { element.textContent = year; });
|
|
2675
2752
|
applyProductHealthMetadata(null);
|
|
2753
|
+
applyProductUpdateMetadata(null);
|
|
2676
2754
|
await refreshSystemHealth();
|
|
2755
|
+
void refreshProductUpdate();
|
|
2677
2756
|
}
|
|
2678
2757
|
|
|
2679
2758
|
function observeSystemBootId(value) {
|
|
@@ -2768,8 +2847,28 @@ async function refreshAuthCaptcha(target = "login") {
|
|
|
2768
2847
|
if (answerInput) answerInput.value = "";
|
|
2769
2848
|
}
|
|
2770
2849
|
|
|
2850
|
+
function clearAuthenticationOverlays() {
|
|
2851
|
+
const toastRegion = $("#toast-region");
|
|
2852
|
+
toastRegion.replaceChildren();
|
|
2853
|
+
document.querySelectorAll("[popover]").forEach((popover) => {
|
|
2854
|
+
if (typeof popover.hidePopover === "function" && popover.matches(":popover-open")) popover.hidePopover();
|
|
2855
|
+
});
|
|
2856
|
+
document.querySelectorAll("dialog[open]").forEach((dialog) => dialog.close());
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function invalidateAuthentication() {
|
|
2860
|
+
state.user = null;
|
|
2861
|
+
state.csrfToken = null;
|
|
2862
|
+
moduleRequestCache.clear();
|
|
2863
|
+
showAuth(false);
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2771
2866
|
function showAuth(setupRequired, registrationOpen = false, setupTokenRequired = false) {
|
|
2772
2867
|
if (state.user) return;
|
|
2868
|
+
document.documentElement.classList.remove("dev-auth-bypass");
|
|
2869
|
+
document.documentElement.classList.add("login-route");
|
|
2870
|
+
window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
2871
|
+
clearAuthenticationOverlays();
|
|
2773
2872
|
document.body.classList.add("auth-pending");
|
|
2774
2873
|
$("#auth-view").classList.remove("hidden");
|
|
2775
2874
|
const canRegister = registrationOpen === true;
|
|
@@ -2897,6 +2996,7 @@ function dismissChapterInsightToast() {
|
|
|
2897
2996
|
}
|
|
2898
2997
|
|
|
2899
2998
|
function toast(message, type = "info") {
|
|
2999
|
+
if (systemRestartDetected || (!state.user && document.documentElement.classList.contains("login-route"))) return;
|
|
2900
3000
|
const region = $("#toast-region");
|
|
2901
3001
|
const element = document.createElement("div");
|
|
2902
3002
|
element.className = `toast ${type}`;
|
|
@@ -3876,8 +3976,9 @@ function renderSearchResults(results, query) {
|
|
|
3876
3976
|
$("#search-results").innerHTML = '<p class="search-results-status">未找到相关内容。</p>';
|
|
3877
3977
|
return;
|
|
3878
3978
|
}
|
|
3979
|
+
const orderedResults = prioritizeGlobalSearchResults(results);
|
|
3879
3980
|
const matchKindLabel = { metadata: "资料命中", exact: "精确命中", phonetic: "拼音命中" };
|
|
3880
|
-
$("#search-results").innerHTML = `<p class="search-results-summary">找到 ${
|
|
3981
|
+
$("#search-results").innerHTML = `<p class="search-results-summary">找到 ${orderedResults.length} 条结果,非正文结果优先,正文条目随后展示。</p>${orderedResults.map((item) => {
|
|
3881
3982
|
const matchKinds = Array.isArray(item.matchKinds) ? item.matchKinds : [];
|
|
3882
3983
|
const lineRange = Number.isInteger(item.startLine)
|
|
3883
3984
|
? `<span class="search-result-chip">${item.startLine === item.endLine ? `第 ${item.startLine} 行` : `第 ${item.startLine}-${item.endLine} 行`}</span>`
|
|
@@ -3892,7 +3993,7 @@ function renderSearchResults(results, query) {
|
|
|
3892
3993
|
}).join("")}`;
|
|
3893
3994
|
$("#search-results").querySelectorAll(".search-result").forEach((button, index) => {
|
|
3894
3995
|
button.addEventListener("click", () => {
|
|
3895
|
-
openSearchResult(
|
|
3996
|
+
openSearchResult(orderedResults[index])
|
|
3896
3997
|
.catch((error) => toast(error.message, "error"));
|
|
3897
3998
|
});
|
|
3898
3999
|
});
|
|
@@ -3961,6 +4062,12 @@ async function openSearchResult(result) {
|
|
|
3961
4062
|
|| !$("#platform-usage-view").classList.contains("hidden")
|
|
3962
4063
|
|| !$("#work-audit-view").classList.contains("hidden");
|
|
3963
4064
|
if (inSettings) await returnFromSettings();
|
|
4065
|
+
if (target.kind === "agent-history") {
|
|
4066
|
+
ensureAiPanelExpanded();
|
|
4067
|
+
await openAiConversation(target.conversationId, true, target.messageId);
|
|
4068
|
+
if (target.messageId) focusAiConversationMessage(target.messageId);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
3964
4071
|
if (target.kind === "chapter") {
|
|
3965
4072
|
await selectChapter(target.id);
|
|
3966
4073
|
if (state.chapter?.id === target.id && target.startLine) {
|
|
@@ -5067,15 +5174,14 @@ function bindModuleContentInteractions() {
|
|
|
5067
5174
|
moduleContentInteractionsBound = true;
|
|
5068
5175
|
const host = $("#module-content");
|
|
5069
5176
|
const open = async (card) => {
|
|
5070
|
-
const id = card.dataset.openSetting ?? card.dataset.
|
|
5177
|
+
const id = card.dataset.openSetting ?? card.dataset.openRace ?? card.dataset.openOrganization;
|
|
5071
5178
|
if (!id) return;
|
|
5072
5179
|
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
5180
|
if (card.dataset.openRace) return openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly: true });
|
|
5075
5181
|
if (card.dataset.openOrganization) return openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly: true });
|
|
5076
5182
|
};
|
|
5077
5183
|
const findCard = (target) => target instanceof Element
|
|
5078
|
-
? target.closest("[data-open-setting], [data-open-
|
|
5184
|
+
? target.closest("[data-open-setting], [data-open-race], [data-open-organization]")
|
|
5079
5185
|
: null;
|
|
5080
5186
|
host.addEventListener("click", (event) => {
|
|
5081
5187
|
if (event.target instanceof Element && event.target.closest("button, a, summary")) return;
|
|
@@ -5608,10 +5714,7 @@ async function renderRaces() {
|
|
|
5608
5714
|
}
|
|
5609
5715
|
|
|
5610
5716
|
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
|
-
]);
|
|
5717
|
+
state.organizations = await moduleApiAllPages("organizations", `/api/works/${state.work.id}/organizations`);
|
|
5615
5718
|
mountModuleCount(state.organizations.length);
|
|
5616
5719
|
const pageResult = paginateModuleItems(state.organizations, page, "organizations");
|
|
5617
5720
|
moduleListPages.organizations = pageResult.page;
|
|
@@ -5916,10 +6019,11 @@ async function renderReviews(page = moduleListPages.reviews) {
|
|
|
5916
6019
|
const canResolveReview = canEditModule("reviews");
|
|
5917
6020
|
const canMergeCharacters = canResolveReview
|
|
5918
6021
|
&& ["characters", "races", "organizations", "timeline", "relationships"].every((module) => canEditModule(module));
|
|
5919
|
-
const
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
6022
|
+
const reviews = await moduleApiAllPages("reviews", `/api/works/${state.work.id}/reviews`);
|
|
6023
|
+
const hasCharacterDuplicateReviews = reviews.some((item) => item.itemType === "character-duplicate");
|
|
6024
|
+
const characters = canReadCharacters && hasCharacterDuplicateReviews
|
|
6025
|
+
? await moduleApiAllPages("reviews", `/api/works/${state.work.id}/characters?includeMerged=1`)
|
|
6026
|
+
: [];
|
|
5923
6027
|
mountModuleCount(reviews.length);
|
|
5924
6028
|
const pageResult = paginateModuleItems(reviews, page, "reviews");
|
|
5925
6029
|
moduleListPages.reviews = pageResult.page;
|
|
@@ -6755,12 +6859,12 @@ function renderProviderCards(providers, models) {
|
|
|
6755
6859
|
const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
|
|
6756
6860
|
const modelStatus = !model.enabled
|
|
6757
6861
|
? `<span class="model-status-badge is-disabled">模型已停用</span>`
|
|
6758
|
-
: provider.
|
|
6759
|
-
? `<span class="model-status-badge is-
|
|
6760
|
-
:
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
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 ? "启用" : "停用"} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>${modelStatus}</div>`;
|
|
6862
|
+
: provider.connectionStatus !== "success"
|
|
6863
|
+
? `<span class="model-status-badge is-unavailable">连接不可用</span>`
|
|
6864
|
+
: "";
|
|
6865
|
+
const capability = model.multimodalEnabled ? " · 多模态" : "";
|
|
6866
|
+
const defaultBadge = model.imageToolDefault ? " · 默认读图模型" : "";
|
|
6867
|
+
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
6868
|
}).join("")}</div>
|
|
6765
6869
|
<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
6870
|
}).join("")}</div>`
|
|
@@ -6776,8 +6880,11 @@ function bindPlatformProviderActions(host, providers, models) {
|
|
|
6776
6880
|
await renderPlatformAiConfig();
|
|
6777
6881
|
await loadModels();
|
|
6778
6882
|
}));
|
|
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", () =>
|
|
6883
|
+
host.querySelectorAll("[data-add-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(button.dataset.addModel, null, providers.find((provider) => provider.id === button.dataset.addModel))));
|
|
6884
|
+
host.querySelectorAll("[data-edit-model]").forEach((button) => button.addEventListener("click", () => {
|
|
6885
|
+
const model = models.find((item) => item.id === button.dataset.editModel);
|
|
6886
|
+
openModelDialog(undefined, model, providers.find((provider) => provider.id === model?.providerId));
|
|
6887
|
+
}));
|
|
6781
6888
|
host.querySelectorAll("[data-edit-provider]").forEach((button) => button.addEventListener("click", () => openProviderDialog(providers.find((provider) => provider.id === button.dataset.editProvider))));
|
|
6782
6889
|
}
|
|
6783
6890
|
|
|
@@ -6785,14 +6892,21 @@ function renderTaskDefaults(models, providers, taskDefaults, settings) {
|
|
|
6785
6892
|
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
6786
6893
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
6787
6894
|
const availableModels = models.filter((model) => isSelectableModel(model));
|
|
6895
|
+
const imageModels = availableModels.filter((model) => model.multimodalEnabled && providerById.get(model.providerId)?.protocol === "openai-chat-completions");
|
|
6788
6896
|
const availableModelIds = new Set(availableModels.map((model) => model.id));
|
|
6789
6897
|
const currentDefaultModels = taskDefaults
|
|
6790
6898
|
.map((item) => item.model)
|
|
6791
6899
|
.filter((model) => model && !availableModelIds.has(model.id));
|
|
6792
6900
|
const optionModels = [...availableModels, ...currentDefaultModels];
|
|
6793
|
-
return optionModels.length ? `<section class="config-section">
|
|
6901
|
+
return optionModels.length || imageModels.length ? `<section class="config-section">
|
|
6794
6902
|
<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
|
|
6903
|
+
<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="多模态读图工具默认模型">
|
|
6904
|
+
<option value="" ${settings.imageToolModelId ? "" : "selected"}>跟随平台默认</option>
|
|
6905
|
+
${imageModels.map((model) => {
|
|
6906
|
+
const provider = providerById.get(model.providerId);
|
|
6907
|
+
return `<option value="${esc(model.id)}" ${model.id === settings.imageToolModelId ? "selected" : ""}>${esc(modelOptionLabel({ ...model, providerName: model.providerName || provider?.name }))}</option>`;
|
|
6908
|
+
}).join("")}
|
|
6909
|
+
</select></td></tr><tr><td>创作助手对话标题生成</td><td><select class="default-model-select" data-title-generation-default aria-label="创作助手对话标题生成">
|
|
6796
6910
|
<option value="" ${settings.titleGenerationModelId ? "" : "selected"}>使用提示词前 15 个字</option>
|
|
6797
6911
|
${models.map((model) => {
|
|
6798
6912
|
const provider = providerById.get(model.providerId);
|
|
@@ -6857,8 +6971,7 @@ function relationshipIndexStatusMarkup(status) {
|
|
|
6857
6971
|
function updateBackgroundTaskCenterVisibility() {
|
|
6858
6972
|
const button = $("#background-task-button");
|
|
6859
6973
|
if (!button) return;
|
|
6860
|
-
const visible = Boolean(state.
|
|
6861
|
-
&& (canReadModule("tasks") || canReadModule("ai-settings"));
|
|
6974
|
+
const visible = Boolean(state.user);
|
|
6862
6975
|
button.classList.toggle("hidden", !visible);
|
|
6863
6976
|
if (!visible) {
|
|
6864
6977
|
$("#background-task-count")?.classList.add("hidden");
|
|
@@ -6867,7 +6980,7 @@ function updateBackgroundTaskCenterVisibility() {
|
|
|
6867
6980
|
const activityCount = backgroundTaskActivityCount(
|
|
6868
6981
|
backgroundTaskCenterSnapshot.taskPage,
|
|
6869
6982
|
backgroundTaskCenterSnapshot.relationshipIndex
|
|
6870
|
-
);
|
|
6983
|
+
) + (productUpdateChecking ? 1 : 0);
|
|
6871
6984
|
const badge = $("#background-task-count");
|
|
6872
6985
|
badge.textContent = activityCount > 99 ? "99+" : String(activityCount);
|
|
6873
6986
|
badge.classList.toggle("hidden", activityCount === 0);
|
|
@@ -6876,6 +6989,37 @@ function updateBackgroundTaskCenterVisibility() {
|
|
|
6876
6989
|
button.setAttribute("title", activityCount > 0 ? `后台任务中心 · ${activityCount} 项进行中` : "后台任务中心");
|
|
6877
6990
|
}
|
|
6878
6991
|
|
|
6992
|
+
function backgroundProductUpdateMarkup() {
|
|
6993
|
+
const status = productUpdateStatus;
|
|
6994
|
+
const updateCheckDisabled = status?.enabled === false;
|
|
6995
|
+
const currentVersion = String(status?.currentVersion ?? "").trim();
|
|
6996
|
+
const latestVersion = String(status?.latestVersion ?? "").trim();
|
|
6997
|
+
const nextCheckAt = formatDateTime(status?.nextCheckAt) || "等待首次探测";
|
|
6998
|
+
const updateAvailable = status?.updateAvailable === true && Boolean(status?.releaseUrl);
|
|
6999
|
+
const badgeClass = productUpdateChecking ? "running" : updateCheckDisabled ? "unknown" : updateAvailable ? "partial" : status?.checked === true ? "completed" : status ? "unknown" : "pending";
|
|
7000
|
+
const badgeLabel = productUpdateChecking ? "探测中" : updateCheckDisabled ? "已关闭" : updateAvailable ? "有新版本" : status?.checked === true ? "已是最新" : status ? "本次未探测" : "等待探测";
|
|
7001
|
+
const detail = productUpdateChecking
|
|
7002
|
+
? "正在请求 GitHub 最新 Release"
|
|
7003
|
+
: updateCheckDisabled
|
|
7004
|
+
? "已通过服务端配置关闭版本更新探测"
|
|
7005
|
+
: updateAvailable
|
|
7006
|
+
? `当前 v${currentVersion || "—"} · 最新 v${latestVersion} · 下次探测 ${nextCheckAt}`
|
|
7007
|
+
: status?.checked === true
|
|
7008
|
+
? `当前 v${currentVersion || "—"} · 下次探测 ${nextCheckAt}`
|
|
7009
|
+
: status
|
|
7010
|
+
? `GitHub 暂不可用 · 下次探测 ${nextCheckAt}`
|
|
7011
|
+
: "应用启动后将在后台探测 GitHub Release";
|
|
7012
|
+
return `<section class="background-task-section">
|
|
7013
|
+
<div class="background-task-section-heading"><div><strong>版本更新探测</strong><small>按服务端配置定期检查 GitHub Release</small></div></div>
|
|
7014
|
+
<div class="background-task-list"><article class="background-task-row">
|
|
7015
|
+
<span class="task-status-badge is-${badgeClass}"><span class="task-status-indicator" aria-hidden="true"></span><span>${badgeLabel}</span></span>
|
|
7016
|
+
<div><strong>叙界版本更新</strong><small>${esc(detail)}</small></div>
|
|
7017
|
+
<span class="background-task-progress">${updateAvailable ? `v${esc(latestVersion)}` : "系统"}</span>
|
|
7018
|
+
${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>'}
|
|
7019
|
+
</article></div>
|
|
7020
|
+
</section>`;
|
|
7021
|
+
}
|
|
7022
|
+
|
|
6879
7023
|
function backgroundTaskTransitionMessage(transition) {
|
|
6880
7024
|
const label = analysisTaskTypeLabel(transition.task.taskType);
|
|
6881
7025
|
if (transition.status === "partial") return { message: `${label}部分失败,请打开任务详情查看`, type: "error" };
|
|
@@ -6934,17 +7078,17 @@ function renderBackgroundTaskCenter() {
|
|
|
6934
7078
|
updateBackgroundTaskCenterVisibility();
|
|
6935
7079
|
const content = $("#background-task-content");
|
|
6936
7080
|
if (!content) return;
|
|
6937
|
-
content.innerHTML = `${backgroundTaskListMarkup(
|
|
7081
|
+
content.innerHTML = `${backgroundProductUpdateMarkup()}${state.work ? `${backgroundTaskListMarkup(
|
|
6938
7082
|
backgroundTaskCenterSnapshot.taskPage,
|
|
6939
7083
|
backgroundTaskCenterSnapshot.errors.tasks
|
|
6940
7084
|
)}${backgroundIndexMarkup(
|
|
6941
7085
|
backgroundTaskCenterSnapshot.relationshipIndex,
|
|
6942
7086
|
backgroundTaskCenterSnapshot.errors.index
|
|
6943
|
-
)}`;
|
|
7087
|
+
)}` : ""}`;
|
|
6944
7088
|
$("#background-task-dialog-meta").textContent = state.work
|
|
6945
|
-
?
|
|
6946
|
-
: "
|
|
6947
|
-
$("#background-task-open-analysis").classList.toggle("hidden", !canReadModule("tasks"));
|
|
7089
|
+
? `系统更新与《${state.work.title}》的分析任务、索引队列`
|
|
7090
|
+
: "系统更新探测状态";
|
|
7091
|
+
$("#background-task-open-analysis").classList.toggle("hidden", !state.work || !canReadModule("tasks"));
|
|
6948
7092
|
content.querySelectorAll("[data-background-task-detail]").forEach((button) => button.addEventListener("click", async () => {
|
|
6949
7093
|
button.disabled = true;
|
|
6950
7094
|
try {
|
|
@@ -7063,8 +7207,7 @@ function stopBackgroundTaskCenter() {
|
|
|
7063
7207
|
backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
7064
7208
|
const dialog = $("#background-task-dialog");
|
|
7065
7209
|
if (dialog?.open) dialog.close();
|
|
7066
|
-
|
|
7067
|
-
$("#background-task-count")?.classList.add("hidden");
|
|
7210
|
+
updateBackgroundTaskCenterVisibility();
|
|
7068
7211
|
}
|
|
7069
7212
|
|
|
7070
7213
|
function startBackgroundTaskCenter(workId) {
|
|
@@ -7081,7 +7224,14 @@ async function renderPlatformAiConfig() {
|
|
|
7081
7224
|
api("/api/platform/ai/settings")
|
|
7082
7225
|
]);
|
|
7083
7226
|
const host = $("#platform-ai-content");
|
|
7084
|
-
|
|
7227
|
+
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
7228
|
+
const imageModels = models.filter((model) => model.multimodalEnabled && providerById.get(model.providerId)?.protocol === "openai-chat-completions");
|
|
7229
|
+
const imageModelOptions = imageModels.map((model) => {
|
|
7230
|
+
const provider = providerById.get(model.providerId);
|
|
7231
|
+
const available = isSelectableModel({ ...model, providerStatus: provider?.status, providerConnectionStatus: provider?.connectionStatus });
|
|
7232
|
+
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>`;
|
|
7233
|
+
}).join("");
|
|
7234
|
+
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
7235
|
$("#save-platform-system-prompt").addEventListener("click", async () => {
|
|
7086
7236
|
const button = $("#save-platform-system-prompt");
|
|
7087
7237
|
button.disabled = true;
|
|
@@ -7094,6 +7244,19 @@ async function renderPlatformAiConfig() {
|
|
|
7094
7244
|
button.disabled = false;
|
|
7095
7245
|
}
|
|
7096
7246
|
});
|
|
7247
|
+
$("#save-platform-image-tool-model").addEventListener("click", async () => {
|
|
7248
|
+
const button = $("#save-platform-image-tool-model");
|
|
7249
|
+
button.disabled = true;
|
|
7250
|
+
try {
|
|
7251
|
+
await api("/api/platform/ai/settings", { method: "PATCH", body: { imageToolModelId: $("#platform-image-tool-model").value || null } });
|
|
7252
|
+
toast("平台多模态读图默认模型已更新");
|
|
7253
|
+
await renderPlatformAiConfig();
|
|
7254
|
+
} catch (error) {
|
|
7255
|
+
toast(error.message, "error");
|
|
7256
|
+
} finally {
|
|
7257
|
+
button.disabled = false;
|
|
7258
|
+
}
|
|
7259
|
+
});
|
|
7097
7260
|
bindPlatformProviderActions(host, providers, models);
|
|
7098
7261
|
}
|
|
7099
7262
|
|
|
@@ -7253,7 +7416,7 @@ async function renderBookAiSettings() {
|
|
|
7253
7416
|
]);
|
|
7254
7417
|
const host = $("#module-content");
|
|
7255
7418
|
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"]);
|
|
7419
|
+
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image"]);
|
|
7257
7420
|
const dailyTokenQuota = settings.dailyTokenQuota === null ? null : Number(settings.dailyTokenQuota);
|
|
7258
7421
|
const quotaUsedTokens = Number(usage?.quota?.usedTokens) || 0;
|
|
7259
7422
|
const quotaRemainingTokens = usage?.quota?.remainingTokens === null
|
|
@@ -7280,7 +7443,7 @@ async function renderBookAiSettings() {
|
|
|
7280
7443
|
);
|
|
7281
7444
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
7282
7445
|
"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>`
|
|
7446
|
+
`<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
7447
|
);
|
|
7285
7448
|
if (!canEditModule("ai-settings")) {
|
|
7286
7449
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -7485,6 +7648,18 @@ async function renderBookAiSettings() {
|
|
|
7485
7648
|
await renderBookAiSettings();
|
|
7486
7649
|
await loadModels();
|
|
7487
7650
|
});
|
|
7651
|
+
host.querySelector("[data-image-tool-default]")?.addEventListener("change", async (event) => {
|
|
7652
|
+
const select = event.currentTarget;
|
|
7653
|
+
select.disabled = true;
|
|
7654
|
+
try {
|
|
7655
|
+
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { imageToolModelId: select.value || null } });
|
|
7656
|
+
toast("本书多模态读图模型已更新");
|
|
7657
|
+
} catch (error) {
|
|
7658
|
+
toast(error.message, "error");
|
|
7659
|
+
}
|
|
7660
|
+
await renderBookAiSettings();
|
|
7661
|
+
await loadModels();
|
|
7662
|
+
});
|
|
7488
7663
|
host.querySelectorAll("[data-task-default]").forEach((select) => select.addEventListener("change", async () => {
|
|
7489
7664
|
select.disabled = true;
|
|
7490
7665
|
try {
|
|
@@ -7692,7 +7867,7 @@ async function ensureAiReferencesLoaded() {
|
|
|
7692
7867
|
|
|
7693
7868
|
function field(name, label, type = "text", value = "", options = []) {
|
|
7694
7869
|
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>`;
|
|
7870
|
+
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
7871
|
if (type === "item-list") {
|
|
7697
7872
|
const values = Array.isArray(value) && value.length ? value : [""];
|
|
7698
7873
|
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 +8470,15 @@ function activateCharacterEditorTab(key) {
|
|
|
8295
8470
|
button.tabIndex = active ? 0 : -1;
|
|
8296
8471
|
});
|
|
8297
8472
|
document.querySelectorAll("[data-character-editor-panel]").forEach((panel) => panel.classList.toggle("hidden", panel.dataset.characterEditorPanel !== key));
|
|
8473
|
+
if (
|
|
8474
|
+
key === "relationships"
|
|
8475
|
+
&& characterEditorItem?.id
|
|
8476
|
+
&& canReadModule("relationships")
|
|
8477
|
+
&& !characterEditorRelationshipsLoaded
|
|
8478
|
+
&& !characterEditorRelationshipsLoading
|
|
8479
|
+
) {
|
|
8480
|
+
void loadCharacterEditorRelationships(characterEditorItem.id);
|
|
8481
|
+
}
|
|
8298
8482
|
}
|
|
8299
8483
|
|
|
8300
8484
|
function setCharacterHistoryVisible(visible) {
|
|
@@ -8316,6 +8500,10 @@ function renderCharacterEditorRelationships() {
|
|
|
8316
8500
|
host.innerHTML = '<p class="character-relationship-status">正在读取人物关系……</p>';
|
|
8317
8501
|
return;
|
|
8318
8502
|
}
|
|
8503
|
+
if (!characterEditorRelationshipsLoaded) {
|
|
8504
|
+
host.innerHTML = '<p class="character-relationship-status">打开人物关系分区后载入关系。</p>';
|
|
8505
|
+
return;
|
|
8506
|
+
}
|
|
8319
8507
|
const characterId = String(characterEditorItem.id);
|
|
8320
8508
|
const nameOf = (id) => state.characters.find((character) => character.id === id)?.name ?? "未知角色";
|
|
8321
8509
|
const rows = characterEditorRelationships.map((relationship) => {
|
|
@@ -8338,20 +8526,23 @@ function renderCharacterEditorRelationships() {
|
|
|
8338
8526
|
host.querySelector("[data-character-relationship-create]")?.addEventListener("click", () => void openRelationshipDialog(null, { characterId }));
|
|
8339
8527
|
}
|
|
8340
8528
|
|
|
8341
|
-
async function loadCharacterEditorRelationships(characterId) {
|
|
8529
|
+
async function loadCharacterEditorRelationships(characterId, { refresh = false } = {}) {
|
|
8342
8530
|
const workId = state.work?.id;
|
|
8343
8531
|
if (!workId || characterEditorItem?.id !== characterId) return;
|
|
8532
|
+
if (!refresh && (characterEditorRelationshipsLoaded || characterEditorRelationshipsLoading)) return;
|
|
8344
8533
|
characterEditorRelationshipsLoading = true;
|
|
8534
|
+
characterEditorRelationshipsLoaded = false;
|
|
8345
8535
|
renderCharacterEditorRelationships();
|
|
8346
8536
|
let loaded = false;
|
|
8347
8537
|
try {
|
|
8348
8538
|
const [characters, relationships] = await Promise.all([
|
|
8349
|
-
|
|
8350
|
-
|
|
8539
|
+
moduleApiAllPages("characters", `/api/works/${workId}/characters`),
|
|
8540
|
+
moduleApiAllPages("relationships", `/api/works/${workId}/relationships`)
|
|
8351
8541
|
]);
|
|
8352
8542
|
if (state.work?.id !== workId || characterEditorItem?.id !== characterId) return;
|
|
8353
8543
|
state.characters = characters;
|
|
8354
8544
|
characterEditorRelationships = relationships.filter((relationship) => relationship.fromCharacterId === characterId || relationship.toCharacterId === characterId);
|
|
8545
|
+
characterEditorRelationshipsLoaded = true;
|
|
8355
8546
|
loaded = true;
|
|
8356
8547
|
} catch (error) {
|
|
8357
8548
|
if (state.work?.id === workId && characterEditorItem?.id === characterId) {
|
|
@@ -8369,7 +8560,7 @@ async function refreshRelationshipSurfaces(characterId = null) {
|
|
|
8369
8560
|
const tasks = [];
|
|
8370
8561
|
if (state.module === "relationships") tasks.push(renderRelationships());
|
|
8371
8562
|
if (characterId && entityEditorType === "character" && !$("#entity-editor-view").classList.contains("hidden") && characterEditorItem?.id === characterId) {
|
|
8372
|
-
tasks.push(loadCharacterEditorRelationships(characterId));
|
|
8563
|
+
tasks.push(loadCharacterEditorRelationships(characterId, { refresh: true }));
|
|
8373
8564
|
}
|
|
8374
8565
|
await Promise.all(tasks);
|
|
8375
8566
|
}
|
|
@@ -9140,15 +9331,15 @@ async function showCharacterHistory() {
|
|
|
9140
9331
|
|
|
9141
9332
|
async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
9142
9333
|
entityEditorReadOnly = readOnly;
|
|
9143
|
-
[state.races, state.organizations
|
|
9334
|
+
[state.races, state.organizations] = await Promise.all([
|
|
9144
9335
|
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([])
|
|
9336
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
9147
9337
|
]);
|
|
9148
9338
|
characterEditorItem = item ?? null;
|
|
9149
9339
|
characterEditorVersions = [];
|
|
9150
9340
|
characterEditorRelationships = [];
|
|
9151
|
-
characterEditorRelationshipsLoading =
|
|
9341
|
+
characterEditorRelationshipsLoading = false;
|
|
9342
|
+
characterEditorRelationshipsLoaded = false;
|
|
9152
9343
|
characterEditorSections = [];
|
|
9153
9344
|
$("#character-editor-eyebrow").textContent = item ? "人物主档案" : "建立人物档案";
|
|
9154
9345
|
$("#character-editor-title").textContent = item?.name || "新建角色";
|
|
@@ -9160,20 +9351,30 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
|
9160
9351
|
const characterMergeButton = $("#character-merge-button");
|
|
9161
9352
|
const characterDeleteButton = $("#character-delete-button");
|
|
9162
9353
|
const canManageCharacter = Boolean(item && !readOnly && canEditModule("characters"));
|
|
9163
|
-
characterMergeButton.classList.toggle("hidden", !canManageCharacter
|
|
9354
|
+
characterMergeButton.classList.toggle("hidden", !canManageCharacter);
|
|
9164
9355
|
characterDeleteButton.classList.toggle("hidden", !canManageCharacter);
|
|
9165
9356
|
characterMergeButton.onclick = async () => {
|
|
9166
9357
|
if (!item) return;
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9358
|
+
const workId = state.work?.id;
|
|
9359
|
+
if (!workId) return;
|
|
9360
|
+
try {
|
|
9361
|
+
const candidates = await moduleApiAllPages("characters", `/api/works/${workId}/characters`);
|
|
9362
|
+
if (state.work?.id !== workId || characterEditorItem?.id !== item.id) return;
|
|
9363
|
+
state.characters = candidates;
|
|
9364
|
+
if (candidates.length < 2) return toast("至少需要两个角色才能合并", "error");
|
|
9365
|
+
await closeEntityEditor({ force: true });
|
|
9366
|
+
openEntityMergeDialog({
|
|
9367
|
+
typeLabel: "角色",
|
|
9368
|
+
source: item,
|
|
9369
|
+
candidates,
|
|
9370
|
+
endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}/merge`,
|
|
9371
|
+
body: (target) => ({ targetCharacterId: target.id, expectedTargetVersionNo: target.versionNo, expectedSourceVersionNo: item.versionNo }),
|
|
9372
|
+
refresh: renderCharacters,
|
|
9373
|
+
impact: "来源角色的别名、组织、档案章节、时间线与人物关系会迁移到目标角色。"
|
|
9374
|
+
});
|
|
9375
|
+
} catch (error) {
|
|
9376
|
+
toast(error.message, "error");
|
|
9377
|
+
}
|
|
9177
9378
|
};
|
|
9178
9379
|
characterDeleteButton.onclick = () => {
|
|
9179
9380
|
if (!item) return;
|
|
@@ -9231,7 +9432,6 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
|
9231
9432
|
};
|
|
9232
9433
|
showEntityEditorPage("character", { readOnly });
|
|
9233
9434
|
if (item) {
|
|
9234
|
-
if (canReadModule("relationships")) void loadCharacterEditorRelationships(item.id);
|
|
9235
9435
|
void loadCharacterMarkdownSections(item.id);
|
|
9236
9436
|
}
|
|
9237
9437
|
}
|
|
@@ -9263,7 +9463,7 @@ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
|
|
|
9263
9463
|
+ field("description", `${label}简介`, "textarea", item?.description, []);
|
|
9264
9464
|
const memberField = memberOptions.length
|
|
9265
9465
|
? field("memberIds", isRace ? "属于该种族的角色(可多选)" : "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions)
|
|
9266
|
-
: `<div class="character-editor-empty-field"><strong>${isRace ? "种族成员" : "组织成员"}</strong><span
|
|
9466
|
+
: `<div class="character-editor-empty-field"><strong>${isRace ? "种族成员" : "组织成员"}</strong><span>${readOnly ? "该档案尚未绑定角色。" : "当前还没有可绑定的角色。"}</span></div>`;
|
|
9267
9467
|
$("#knowledge-editor-fields").innerHTML = knowledgeEditorSection("basic", "基础资料", isRace ? "先定义名称、层级和简介,再补充共同设定。" : "先定义组织名称和简介,再补充完整的组织设定。", basicFields)
|
|
9268
9468
|
+ knowledgeEditorSection("settings", title, "", '<div id="knowledge-markdown-sections" class="knowledge-markdown-sections"></div>')
|
|
9269
9469
|
+ knowledgeEditorSection("members", isRace ? "种族成员" : "组织成员", "成员关系会同步到角色档案中。", memberField);
|
|
@@ -9278,8 +9478,13 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
|
|
|
9278
9478
|
entityEditorReadOnly = readOnly;
|
|
9279
9479
|
await discardPendingMarkdownAttachments();
|
|
9280
9480
|
if (kind === "race" && !(await ensureCompleteRaceList())) return;
|
|
9281
|
-
|
|
9282
|
-
|
|
9481
|
+
const memberCharacters = readOnly
|
|
9482
|
+
? (Array.isArray(item?.members) ? item.members : []).map((member) => ({ id: member.characterId, name: member.name, aliases: [] }))
|
|
9483
|
+
: canReadModule("characters")
|
|
9484
|
+
? await moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`)
|
|
9485
|
+
: [];
|
|
9486
|
+
if (!readOnly) state.characters = memberCharacters;
|
|
9487
|
+
const memberOptions = memberCharacters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
9283
9488
|
const isRace = kind === "race";
|
|
9284
9489
|
const module = isRace ? "races" : "organizations";
|
|
9285
9490
|
const label = isRace ? "种族" : "组织";
|
|
@@ -9588,7 +9793,7 @@ function openTimelineSplitDialog(item) {
|
|
|
9588
9793
|
|
|
9589
9794
|
async function openRelationshipDialog(item, options = {}) {
|
|
9590
9795
|
if (!canReadModule("characters")) return toast("配置人物关系前需要角色模块读取权限", "error");
|
|
9591
|
-
state.characters = await
|
|
9796
|
+
state.characters = await moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`);
|
|
9592
9797
|
if (state.characters.length < 2) return toast("至少需要两个角色才能创建关系", "error");
|
|
9593
9798
|
const characterOptions = state.characters.map((item) => [item.id, item.name]);
|
|
9594
9799
|
const defaultFrom = options.characterId && state.characters.some((character) => character.id === options.characterId) ? options.characterId : characterOptions[0][0];
|
|
@@ -10016,18 +10221,23 @@ function openProviderDialog(item) {
|
|
|
10016
10221
|
protocolSelect.addEventListener("change", syncProviderCredentialField);
|
|
10017
10222
|
}
|
|
10018
10223
|
|
|
10019
|
-
function openModelDialog(providerId, item = null) {
|
|
10224
|
+
function openModelDialog(providerId, item = null, provider = null) {
|
|
10020
10225
|
const values = modelFormValues(item);
|
|
10226
|
+
const imageDefaultSupported = supportsMultimodalModelProtocol(provider?.protocol);
|
|
10227
|
+
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
10228
|
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
10229
|
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>`;
|
|
10230
|
+
const connectionTestDescription = values.multimodalEnabled && imageDefaultSupported
|
|
10231
|
+
? "使用当前已保存的模型标识符和供应商凭据,并发送一张测试图片验证图片请求。"
|
|
10232
|
+
: "使用当前已保存的模型标识符和供应商凭据发起最小请求。";
|
|
10023
10233
|
const connectionTest = item && item.providerStatus === "enabled"
|
|
10024
10234
|
? `<section class="model-connection-test">
|
|
10025
|
-
<div><strong>模型连接测试</strong><p
|
|
10235
|
+
<div><strong>模型连接测试</strong><p>${connectionTestDescription}</p></div>
|
|
10026
10236
|
<button class="ghost-button" type="button" data-test-model="${esc(item.id)}">测试连接</button>
|
|
10027
10237
|
</section>`
|
|
10028
10238
|
: "";
|
|
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);
|
|
10239
|
+
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) => {
|
|
10240
|
+
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
10241
|
await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
|
|
10032
10242
|
await renderPlatformAiConfig();
|
|
10033
10243
|
await loadModels();
|
|
@@ -10037,6 +10247,16 @@ function openModelDialog(providerId, item = null) {
|
|
|
10037
10247
|
const contextWindowHint = $("#model-context-window-hint");
|
|
10038
10248
|
const temperatureInput = $("#dialog-fields input[name='temperature']");
|
|
10039
10249
|
const temperatureHint = $("#model-temperature-hint");
|
|
10250
|
+
const multimodalInput = $("#model-multimodal-enabled");
|
|
10251
|
+
const imageDefaultField = $("#model-image-tool-default-field");
|
|
10252
|
+
const imageDefaultInput = $("#model-image-tool-default");
|
|
10253
|
+
const syncMultimodalFields = () => {
|
|
10254
|
+
if (!multimodalInput || !imageDefaultField || !imageDefaultInput) return;
|
|
10255
|
+
const hideImageDefault = !multimodalInput.checked || !imageDefaultSupported;
|
|
10256
|
+
imageDefaultField.hidden = hideImageDefault;
|
|
10257
|
+
imageDefaultField.classList.toggle("hidden", hideImageDefault);
|
|
10258
|
+
if (!multimodalInput.checked) imageDefaultInput.checked = false;
|
|
10259
|
+
};
|
|
10040
10260
|
const syncModelContextWindowGuidance = () => {
|
|
10041
10261
|
const guidance = modelContextWindowGuidance(contextWindowInput.value);
|
|
10042
10262
|
contextWindowInput.setCustomValidity(guidance.belowMinimum ? "模型上下文不能低于 32K(32768 Token)。" : "");
|
|
@@ -10051,13 +10271,14 @@ function openModelDialog(providerId, item = null) {
|
|
|
10051
10271
|
syncKimiTemperature();
|
|
10052
10272
|
});
|
|
10053
10273
|
contextWindowInput.addEventListener("input", syncModelContextWindowGuidance);
|
|
10274
|
+
multimodalInput?.addEventListener("change", syncMultimodalFields);
|
|
10054
10275
|
$("#dialog-fields [data-test-model]")?.addEventListener("click", async (event) => {
|
|
10055
10276
|
const button = event.currentTarget;
|
|
10056
10277
|
button.disabled = true;
|
|
10057
10278
|
button.textContent = "测试中";
|
|
10058
10279
|
try {
|
|
10059
10280
|
const result = await api(`/api/models/${button.dataset.testModel}/test`, { method: "POST", body: {} });
|
|
10060
|
-
toast(result.ok ? "模型连接测试成功" : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
10281
|
+
toast(result.ok ? (result.multimodalTested ? "模型连接测试成功,图片请求已验证" : "模型连接测试成功") : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
10061
10282
|
await renderPlatformAiConfig();
|
|
10062
10283
|
await loadModels();
|
|
10063
10284
|
} catch (error) {
|
|
@@ -10069,6 +10290,7 @@ function openModelDialog(providerId, item = null) {
|
|
|
10069
10290
|
});
|
|
10070
10291
|
syncModelContextWindowGuidance();
|
|
10071
10292
|
syncKimiTemperature();
|
|
10293
|
+
syncMultimodalFields();
|
|
10072
10294
|
}
|
|
10073
10295
|
|
|
10074
10296
|
async function sendAi() {
|
|
@@ -10773,17 +10995,7 @@ function hasUnsavedEditorChanges() {
|
|
|
10773
10995
|
}
|
|
10774
10996
|
|
|
10775
10997
|
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);
|
|
10998
|
+
invalidateAuthentication();
|
|
10787
10999
|
}
|
|
10788
11000
|
|
|
10789
11001
|
$("#system-restart-confirm").addEventListener("click", redirectToLoginAfterSystemRestart);
|
|
@@ -11967,13 +12179,14 @@ $("#background-task-button").addEventListener("click", () => {
|
|
|
11967
12179
|
const dialog = $("#background-task-dialog");
|
|
11968
12180
|
if (!dialog.open) dialog.showModal();
|
|
11969
12181
|
void refreshBackgroundTaskCenter();
|
|
12182
|
+
void refreshProductUpdate();
|
|
11970
12183
|
});
|
|
11971
12184
|
$("#background-task-dialog").addEventListener("close", scheduleBackgroundTaskCenterRefresh);
|
|
11972
12185
|
$("#background-task-refresh").addEventListener("click", async () => {
|
|
11973
12186
|
const button = $("#background-task-refresh");
|
|
11974
12187
|
button.disabled = true;
|
|
11975
12188
|
try {
|
|
11976
|
-
await refreshBackgroundTaskCenter();
|
|
12189
|
+
await Promise.all([refreshBackgroundTaskCenter(), refreshProductUpdate()]);
|
|
11977
12190
|
} finally {
|
|
11978
12191
|
button.disabled = false;
|
|
11979
12192
|
}
|