@musnows/scriverse 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +388 -61
- package/dist/ai.js.map +1 -1
- package/dist/app.js +77 -2
- package/dist/app.js.map +1 -1
- package/dist/database.js +34 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +366 -34
- package/dist/public/index.html +10 -2
- package/dist/public/plain-text-paste.js +89 -0
- package/dist/public/styles.css +35 -0
- package/dist/s3-backup.js +47 -10
- package/dist/s3-backup.js.map +1 -1
- package/dist/store.js +231 -69
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +1 -1
- 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
|
@@ -26,6 +26,7 @@ import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-ti
|
|
|
26
26
|
import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260812-context-usage-remaining-v2";
|
|
27
27
|
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
28
28
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
29
|
+
import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
|
|
29
30
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
30
31
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
31
32
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260801-entity-lifecycle-v1";
|
|
@@ -2144,11 +2145,27 @@ function closeAiHistoryActionMenu(restoreFocus = false) {
|
|
|
2144
2145
|
aiHistoryActionAnchor = null;
|
|
2145
2146
|
}
|
|
2146
2147
|
|
|
2148
|
+
function syncAiHistoryActionMenu(conversation) {
|
|
2149
|
+
const menu = $("#ai-history-action-menu");
|
|
2150
|
+
const isFavorite = conversation?.isFavorite === true;
|
|
2151
|
+
const favorite = menu.querySelector('[data-ai-history-action="favorite"]');
|
|
2152
|
+
const favoriteLabel = favorite?.querySelector("span");
|
|
2153
|
+
const favoriteHint = favorite?.querySelector("small");
|
|
2154
|
+
if (favoriteLabel) favoriteLabel.textContent = isFavorite ? "取消收藏" : "收藏对话";
|
|
2155
|
+
if (favoriteHint) favoriteHint.textContent = isFavorite ? "取消收藏后才可以清理对话" : "收藏后不会被清理";
|
|
2156
|
+
const deleteOption = menu.querySelector('[data-ai-history-action="delete"]');
|
|
2157
|
+
if (deleteOption) {
|
|
2158
|
+
deleteOption.disabled = isFavorite;
|
|
2159
|
+
deleteOption.title = isFavorite ? "收藏的对话不能清理,请先取消收藏" : "清理后无法恢复";
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2147
2163
|
function showAiHistoryActionMenu(anchor, conversation) {
|
|
2148
2164
|
const menu = $("#ai-history-action-menu");
|
|
2149
2165
|
closeAiHistoryActionMenu();
|
|
2150
2166
|
aiHistoryActionConversation = conversation;
|
|
2151
2167
|
aiHistoryActionAnchor = anchor;
|
|
2168
|
+
syncAiHistoryActionMenu(conversation);
|
|
2152
2169
|
menu.classList.remove("hidden");
|
|
2153
2170
|
const anchorRect = anchor.getBoundingClientRect();
|
|
2154
2171
|
const menuRect = menu.getBoundingClientRect();
|
|
@@ -2181,6 +2198,84 @@ async function downloadAiConversation(conversation) {
|
|
|
2181
2198
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
|
2182
2199
|
}
|
|
2183
2200
|
|
|
2201
|
+
function resetAiConversationAfterDelete() {
|
|
2202
|
+
state.aiConversationId = null;
|
|
2203
|
+
state.aiConversationModelId = null;
|
|
2204
|
+
state.aiPromptSent = false;
|
|
2205
|
+
state.aiCitations = [];
|
|
2206
|
+
state.aiReferences = [];
|
|
2207
|
+
applyAiConversationTaskType("chat");
|
|
2208
|
+
applyAiConversationContextScope({ type: "none" });
|
|
2209
|
+
applyAiRoleplayCharacter(null);
|
|
2210
|
+
$("#ai-conversation-title").textContent = "新对话";
|
|
2211
|
+
resetAiFeed();
|
|
2212
|
+
hideAiContextWarning();
|
|
2213
|
+
resetAiContextMeter();
|
|
2214
|
+
setAiPromptText("");
|
|
2215
|
+
renderAiCitations();
|
|
2216
|
+
renderAiReferences();
|
|
2217
|
+
renderAiQuickActions();
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
async function favoriteAiConversation(conversation) {
|
|
2221
|
+
const updated = await api(`/api/ai-conversations/${encodeURIComponent(conversation.id)}/favorite`, {
|
|
2222
|
+
method: "PATCH",
|
|
2223
|
+
body: { isFavorite: conversation.isFavorite !== true }
|
|
2224
|
+
});
|
|
2225
|
+
upsertAiConversationSummary(updated);
|
|
2226
|
+
toast(updated.isFavorite ? "对话已收藏" : "已取消收藏");
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
async function deleteAiConversation(conversation) {
|
|
2230
|
+
if (conversation.isFavorite === true) {
|
|
2231
|
+
toast("收藏的对话不能清理,请先取消收藏", "error");
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (!await confirmToast(`清理“${conversation.title}”后,对话及其消息将无法恢复。确认继续吗?`, {
|
|
2235
|
+
title: "清理对话",
|
|
2236
|
+
confirmLabel: "确认清理"
|
|
2237
|
+
})) return;
|
|
2238
|
+
await api(`/api/ai-conversations/${encodeURIComponent(conversation.id)}`, { method: "DELETE" });
|
|
2239
|
+
state.aiConversations = state.aiConversations.filter((item) => item.id !== conversation.id);
|
|
2240
|
+
if (state.aiConversationId === conversation.id) resetAiConversationAfterDelete();
|
|
2241
|
+
renderAiConversationHistory();
|
|
2242
|
+
toast("对话已清理");
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
const aiConversationTaskTypeLabels = {
|
|
2246
|
+
chat: "问答",
|
|
2247
|
+
roleplay: "角色扮演",
|
|
2248
|
+
continue: "续写",
|
|
2249
|
+
polish: "润色选中文本"
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
function aiConversationTaskTypeLabel(taskType) {
|
|
2253
|
+
return aiConversationTaskTypeLabels[String(taskType)] ?? aiConversationTaskTypeLabels.chat;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function aiConversationContextScopeLabel(scope) {
|
|
2257
|
+
const normalizedScope = scope && typeof scope === "object" ? scope : {};
|
|
2258
|
+
if (normalizedScope.type === "chapter" && normalizedScope.includeBookSummary === true) return "当前章节 + 全书概要";
|
|
2259
|
+
return {
|
|
2260
|
+
none: "无上下文",
|
|
2261
|
+
chapter: "当前章节",
|
|
2262
|
+
volume: "当前卷",
|
|
2263
|
+
book: "全书",
|
|
2264
|
+
"settings-catalog": "设定库"
|
|
2265
|
+
}[String(normalizedScope.type)] ?? "无上下文";
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
function aiConversationHistoryMeta(conversation) {
|
|
2269
|
+
const parts = [
|
|
2270
|
+
`${Number(conversation.messageCount ?? 0)} 条`,
|
|
2271
|
+
aiConversationTaskTypeLabel(conversation.taskType),
|
|
2272
|
+
aiConversationContextScopeLabel(conversation.contextScope)
|
|
2273
|
+
];
|
|
2274
|
+
if (conversation.roleplayCharacter?.name) parts.push(`扮演 ${conversation.roleplayCharacter.name}`);
|
|
2275
|
+
parts.push(formatDateTime(conversation.updatedAt));
|
|
2276
|
+
return parts.join(" · ");
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2184
2279
|
function renderAiConversationHistory() {
|
|
2185
2280
|
closeAiHistoryActionMenu();
|
|
2186
2281
|
const host = $("#ai-history-list");
|
|
@@ -2202,11 +2297,21 @@ function renderAiConversationHistory() {
|
|
|
2202
2297
|
button.type = "button";
|
|
2203
2298
|
button.className = `ai-history-item${conversation.id === state.aiConversationId ? " is-active" : ""}`;
|
|
2204
2299
|
button.dataset.aiConversationId = conversation.id;
|
|
2300
|
+
const titleRow = document.createElement("span");
|
|
2301
|
+
titleRow.className = "ai-history-title-row";
|
|
2205
2302
|
const title = document.createElement("strong");
|
|
2206
2303
|
title.textContent = conversation.title;
|
|
2304
|
+
titleRow.append(title);
|
|
2305
|
+
if (conversation.isFavorite === true) {
|
|
2306
|
+
const favorite = document.createElement("small");
|
|
2307
|
+
favorite.className = "ai-history-favorite";
|
|
2308
|
+
favorite.textContent = "已收藏";
|
|
2309
|
+
favorite.setAttribute("aria-label", "已收藏");
|
|
2310
|
+
titleRow.append(favorite);
|
|
2311
|
+
}
|
|
2207
2312
|
const meta = document.createElement("small");
|
|
2208
|
-
meta.textContent =
|
|
2209
|
-
button.append(
|
|
2313
|
+
meta.textContent = aiConversationHistoryMeta(conversation);
|
|
2314
|
+
button.append(titleRow, meta);
|
|
2210
2315
|
button.addEventListener("click", () => openAiConversation(conversation.id));
|
|
2211
2316
|
const more = document.createElement("button");
|
|
2212
2317
|
more.type = "button";
|
|
@@ -2419,12 +2524,37 @@ function renderAiRoleplayCharacterSelect() {
|
|
|
2419
2524
|
const canSelectCharacter = Boolean(state.work)
|
|
2420
2525
|
&& canReadModule("characters")
|
|
2421
2526
|
&& canWritePermissionModule(state.work, "ai-chat");
|
|
2422
|
-
select.disabled = aiInteractionBusy() || !canSelectCharacter
|
|
2527
|
+
select.disabled = aiInteractionBusy() || !canSelectCharacter;
|
|
2423
2528
|
select.title = canSelectCharacter
|
|
2424
|
-
?
|
|
2529
|
+
? state.aiPromptSent
|
|
2530
|
+
? aiConversationOptionLockedMessage
|
|
2531
|
+
: "为当前对话选择角色卡;角色扮演时 Agent 只能查询与该角色自身有关的记忆"
|
|
2425
2532
|
: "当前账户没有角色模块读取权限";
|
|
2426
2533
|
}
|
|
2427
2534
|
|
|
2535
|
+
const aiConversationOptionLockedMessage = "会话选项在会话开始后不支持修改,若需要修改,请新建会话";
|
|
2536
|
+
|
|
2537
|
+
function notifyAiConversationOptionLocked(select) {
|
|
2538
|
+
if (!state.aiPromptSent) return false;
|
|
2539
|
+
const now = Date.now();
|
|
2540
|
+
const lastToastAt = Number(select.dataset.lockedToastAt ?? 0);
|
|
2541
|
+
if (now - lastToastAt > 500) toast(aiConversationOptionLockedMessage);
|
|
2542
|
+
select.dataset.lockedToastAt = String(now);
|
|
2543
|
+
return true;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function blockLockedAiConversationOptionInteraction(event) {
|
|
2547
|
+
const select = event.currentTarget;
|
|
2548
|
+
if (!notifyAiConversationOptionLocked(select)) return;
|
|
2549
|
+
event.preventDefault();
|
|
2550
|
+
event.stopPropagation();
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function blockLockedAiConversationOptionKeydown(event) {
|
|
2554
|
+
if (["Tab", "Shift", "Control", "Alt", "Meta"].includes(event.key)) return;
|
|
2555
|
+
blockLockedAiConversationOptionInteraction(event);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2428
2558
|
function syncAiTaskOptions() {
|
|
2429
2559
|
const roleplaySelected = $("#ai-task").value === "roleplay";
|
|
2430
2560
|
const interactionBusy = aiInteractionBusy();
|
|
@@ -2432,14 +2562,14 @@ function syncAiTaskOptions() {
|
|
|
2432
2562
|
$("#ai-scope").setAttribute("aria-hidden", String(roleplaySelected));
|
|
2433
2563
|
$("#ai-roleplay-character").classList.toggle("hidden", !roleplaySelected);
|
|
2434
2564
|
$("#ai-roleplay-character").setAttribute("aria-hidden", String(!roleplaySelected));
|
|
2435
|
-
$("#ai-task").disabled = interactionBusy
|
|
2436
|
-
$("#ai-task").title = state.aiPromptSent ?
|
|
2437
|
-
$("#ai-scope").disabled = interactionBusy || roleplaySelected
|
|
2565
|
+
$("#ai-task").disabled = interactionBusy;
|
|
2566
|
+
$("#ai-task").title = state.aiPromptSent ? aiConversationOptionLockedMessage : "";
|
|
2567
|
+
$("#ai-scope").disabled = interactionBusy || roleplaySelected;
|
|
2438
2568
|
$("#ai-scope").title = state.aiPromptSent
|
|
2439
|
-
?
|
|
2569
|
+
? aiConversationOptionLockedMessage
|
|
2440
2570
|
: roleplaySelected ? "角色扮演模式只使用角色自身的记忆" : "";
|
|
2441
|
-
$("#ai-model").disabled = interactionBusy
|
|
2442
|
-
$("#ai-model").title = state.aiPromptSent ?
|
|
2571
|
+
$("#ai-model").disabled = interactionBusy;
|
|
2572
|
+
$("#ai-model").title = state.aiPromptSent ? aiConversationOptionLockedMessage : "";
|
|
2443
2573
|
}
|
|
2444
2574
|
|
|
2445
2575
|
function applyAiConversationTaskType(taskType) {
|
|
@@ -11692,6 +11822,7 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
11692
11822
|
const attachmentObserver = new MutationObserver(() => normalizeVditorAttachmentImages(editor));
|
|
11693
11823
|
attachmentObserver.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ["src"] });
|
|
11694
11824
|
editor.__attachmentObserver = attachmentObserver;
|
|
11825
|
+
editor.__plainTextPasteCleanup = bindPlainTextPaste(host);
|
|
11695
11826
|
host.__vditor = editor;
|
|
11696
11827
|
return editor;
|
|
11697
11828
|
}
|
|
@@ -11883,6 +12014,7 @@ function ensureVditorIconScript() {
|
|
|
11883
12014
|
function destroyVditorEditor(editor) {
|
|
11884
12015
|
if (!editor) return;
|
|
11885
12016
|
editor.__attachmentObserver?.disconnect();
|
|
12017
|
+
editor.__plainTextPasteCleanup?.();
|
|
11886
12018
|
editor.__vditorLineNumberObserver?.disconnect();
|
|
11887
12019
|
editor.__vditorLineNumberResizeObserver?.disconnect();
|
|
11888
12020
|
if (editor.__vditorLineNumberSurface && editor.__vditorLineNumberScrollHandler) {
|
|
@@ -12955,7 +13087,12 @@ function openReviewDialog() {
|
|
|
12955
13087
|
}
|
|
12956
13088
|
|
|
12957
13089
|
async function openTaskDialog() {
|
|
12958
|
-
const
|
|
13090
|
+
const volumeOptions = state.work.volumes.map((volume) => ({ id: String(volume.id), title: String(volume.title) }));
|
|
13091
|
+
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
|
|
13092
|
+
id: String(chapter.id),
|
|
13093
|
+
title: String(chapter.title),
|
|
13094
|
+
volumeTitle: String(volume.title)
|
|
13095
|
+
})));
|
|
12959
13096
|
let relationshipCharacters = [];
|
|
12960
13097
|
let taskModels = [];
|
|
12961
13098
|
let taskDefaults = [];
|
|
@@ -12971,6 +13108,23 @@ async function openTaskDialog() {
|
|
|
12971
13108
|
toast(`分析任务配置加载失败:${error.message}`, "error");
|
|
12972
13109
|
return;
|
|
12973
13110
|
}
|
|
13111
|
+
const taskScopePicker = (kind, label, options, emptyLabel) => `<div class="form-field task-scope-field task-scope-${kind}-field is-disabled" data-task-scope-field="${kind}" aria-disabled="true">
|
|
13112
|
+
<span id="task-${kind}-label">${esc(label)}(可多选)</span>
|
|
13113
|
+
<div class="task-scope-picker">
|
|
13114
|
+
<button class="task-scope-trigger" type="button" data-task-scope-trigger="${kind}" aria-expanded="false" aria-controls="task-${kind}-bubble" aria-labelledby="task-${kind}-label task-${kind}-summary" disabled>
|
|
13115
|
+
<span id="task-${kind}-summary">${esc(emptyLabel)}</span>
|
|
13116
|
+
<span class="task-scope-trigger-meta"><span data-task-scope-count="${kind}">未选择</span><span class="task-scope-chevron" aria-hidden="true">⌄</span></span>
|
|
13117
|
+
</button>
|
|
13118
|
+
<div id="task-${kind}-bubble" class="task-scope-bubble hidden" data-task-scope-bubble="${kind}">
|
|
13119
|
+
<label class="task-scope-search">筛选${esc(label)}<input type="search" data-task-scope-search="${kind}" placeholder="输入名称" autocomplete="off"></label>
|
|
13120
|
+
<div class="task-scope-bubble-meta"><span>共 ${options.length} ${kind === "chapter" ? "章" : "卷"}</span><button type="button" data-task-scope-clear="${kind}" disabled>清空选择</button></div>
|
|
13121
|
+
<div class="task-scope-options" role="group" aria-labelledby="task-${kind}-label">
|
|
13122
|
+
${options.map((item) => `<label class="task-scope-option" data-task-scope-search-name="${esc(`${item.title} ${item.volumeTitle ?? ""}`.toLocaleLowerCase())}"><input type="checkbox" name="${kind === "chapter" ? "chapterIds" : "volumeIds"}" value="${esc(item.id)}" data-task-scope-input="${kind}"><span><strong>${esc(item.title)}</strong>${item.volumeTitle ? `<small>${esc(item.volumeTitle)}</small>` : ""}</span></label>`).join("")}
|
|
13123
|
+
</div>
|
|
13124
|
+
<p class="task-scope-empty hidden" data-task-scope-empty="${kind}">没有匹配的${esc(label)}</p>
|
|
13125
|
+
</div>
|
|
13126
|
+
</div>
|
|
13127
|
+
</div>`;
|
|
12974
13128
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
12975
13129
|
const availableTaskModels = taskModels.filter((model) => isSelectableModel(model));
|
|
12976
13130
|
const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
|
|
@@ -12997,8 +13151,9 @@ async function openTaskDialog() {
|
|
|
12997
13151
|
const modelField = `<label>任务模型<select name="modelId" required aria-describedby="analysis-task-model-help">
|
|
12998
13152
|
<option value="" ${availableTaskModels.some((model) => model.id === defaultModelId) ? "" : "selected"} disabled>${availableTaskModels.length ? "请选择模型" : "没有可用模型"}</option>
|
|
12999
13153
|
${availableTaskModels.map((model) => `<option value="${esc(model.id)}" ${model.id === defaultModelId ? "selected" : ""}>${esc(modelOptionLabel(model))}</option>`).join("")}
|
|
13000
|
-
</select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small></label>`;
|
|
13001
|
-
const chapterField =
|
|
13154
|
+
</select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small><p class="analysis-context-warning hidden" data-analysis-context-warning role="alert"></p></label>`;
|
|
13155
|
+
const chapterField = taskScopePicker("chapter", "章节", chapterOptions, "选择需要分析的章节");
|
|
13156
|
+
const volumeField = taskScopePicker("volume", "分卷", volumeOptions, "选择需要分析的分卷");
|
|
13002
13157
|
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
13003
13158
|
${relationshipCharacterPicker}
|
|
13004
13159
|
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
|
|
@@ -13035,25 +13190,43 @@ async function openTaskDialog() {
|
|
|
13035
13190
|
const preFilterRelationshipSources = characterIds.length > 0 && form.get("preFilterRelationshipSources") === "on";
|
|
13036
13191
|
const previewRelationshipChanges = taskType === "relationship-analysis" && form.get("previewRelationshipChanges") === "on";
|
|
13037
13192
|
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
13193
|
+
const chapterIds = form.getAll("chapterIds").map(String).filter(Boolean);
|
|
13194
|
+
const volumeIds = form.getAll("volumeIds").map(String).filter(Boolean);
|
|
13195
|
+
const chapterScope = {
|
|
13196
|
+
type: "chapter",
|
|
13197
|
+
...(chapterIds[0] ? { chapterId: chapterIds[0] } : {}),
|
|
13198
|
+
...(chapterIds.length ? { chapterIds } : {})
|
|
13199
|
+
};
|
|
13200
|
+
const volumeScope = {
|
|
13201
|
+
type: "volume",
|
|
13202
|
+
...(volumeIds[0] ? { volumeId: volumeIds[0] } : {}),
|
|
13203
|
+
...(volumeIds.length ? { volumeIds } : {})
|
|
13204
|
+
};
|
|
13038
13205
|
const scope = settingsOnly
|
|
13039
13206
|
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13040
|
-
:
|
|
13207
|
+
: scopeType === "book" || includeAllSettings
|
|
13041
13208
|
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13042
|
-
: {
|
|
13209
|
+
: { ...(scopeType === "volume" ? volumeScope : chapterScope), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
13043
13210
|
return scope;
|
|
13044
13211
|
};
|
|
13045
13212
|
const relationshipPreviewKey = (scope, modelId) => JSON.stringify({
|
|
13046
13213
|
type: scope.type,
|
|
13047
13214
|
chapterId: scope.chapterId ?? null,
|
|
13215
|
+
chapterIds: scope.chapterIds ?? [],
|
|
13216
|
+
volumeId: scope.volumeId ?? null,
|
|
13217
|
+
volumeIds: scope.volumeIds ?? [],
|
|
13048
13218
|
includeAllSettings: scope.includeAllSettings === true,
|
|
13049
13219
|
characterIds: scope.characterIds ?? [],
|
|
13050
13220
|
preFilterRelationshipSources: scope.preFilterRelationshipSources !== false,
|
|
13051
13221
|
modelId
|
|
13052
13222
|
});
|
|
13053
|
-
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
13223
|
+
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["volume", "指定分卷"], ["book", "全书"]]) + chapterField + volumeField + relationshipFields, async (form) => {
|
|
13224
|
+
const workId = state.work.id;
|
|
13054
13225
|
const taskType = String(form.get("taskType"));
|
|
13055
13226
|
const modelId = String(form.get("modelId"));
|
|
13056
13227
|
const scope = buildRelationshipScope(form);
|
|
13228
|
+
if (scope.type === "chapter" && !scope.chapterIds?.length) return toast("请先选择至少一个章节", "error");
|
|
13229
|
+
if (scope.type === "volume" && !scope.volumeIds?.length) return toast("请先选择至少一个分卷", "error");
|
|
13057
13230
|
if (taskType === "relationship-analysis" && relationshipSourcePreview
|
|
13058
13231
|
&& relationshipSourcePreviewConfigKey === relationshipPreviewKey(scope, modelId)) {
|
|
13059
13232
|
scope.relationshipSourceRefs = [...$("#dialog-fields").querySelectorAll("[data-relationship-source-selected]:checked")]
|
|
@@ -13063,15 +13236,19 @@ async function openTaskDialog() {
|
|
|
13063
13236
|
sourceVersion: input.dataset.sourceVersion
|
|
13064
13237
|
}));
|
|
13065
13238
|
}
|
|
13066
|
-
|
|
13067
|
-
await refreshBackgroundTaskCenter({ announce: false });
|
|
13068
|
-
taskListPage = 1;
|
|
13239
|
+
clearContextWarning();
|
|
13069
13240
|
try {
|
|
13070
|
-
await
|
|
13071
|
-
toast("分析任务已创建,已进入任务队列");
|
|
13241
|
+
await api(`/api/works/${workId}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
|
|
13072
13242
|
} catch (error) {
|
|
13073
|
-
|
|
13243
|
+
if (error.code === "AI_CONTEXT_TOO_LARGE") {
|
|
13244
|
+
contextWarning.textContent = error.message || "当前分析范围超过模型上下文阈值,请切换到上下文更长的模型后重试。";
|
|
13245
|
+
contextWarning.classList.remove("hidden");
|
|
13246
|
+
}
|
|
13247
|
+
throw error;
|
|
13074
13248
|
}
|
|
13249
|
+
taskListPage = 1;
|
|
13250
|
+
toast("分析任务已创建,已进入任务队列");
|
|
13251
|
+
void refreshAnalysisTaskViewsAfterCreate(workId);
|
|
13075
13252
|
}, "AI 分析", {
|
|
13076
13253
|
submitLabel: "创建任务",
|
|
13077
13254
|
pendingLabel: "创建中…",
|
|
@@ -13081,8 +13258,42 @@ async function openTaskDialog() {
|
|
|
13081
13258
|
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
13082
13259
|
const taskModelSelect = $("#dialog-fields").querySelector('select[name="modelId"]');
|
|
13083
13260
|
const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
|
|
13084
|
-
const
|
|
13085
|
-
|
|
13261
|
+
const scopeFields = {
|
|
13262
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-field="chapter"]'),
|
|
13263
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-field="volume"]')
|
|
13264
|
+
};
|
|
13265
|
+
const scopeTriggers = {
|
|
13266
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-trigger="chapter"]'),
|
|
13267
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-trigger="volume"]')
|
|
13268
|
+
};
|
|
13269
|
+
const scopeBubbles = {
|
|
13270
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-bubble="chapter"]'),
|
|
13271
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-bubble="volume"]')
|
|
13272
|
+
};
|
|
13273
|
+
const scopeSearches = {
|
|
13274
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-search="chapter"]'),
|
|
13275
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-search="volume"]')
|
|
13276
|
+
};
|
|
13277
|
+
const scopeInputs = {
|
|
13278
|
+
chapter: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="chapter"]')],
|
|
13279
|
+
volume: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="volume"]')]
|
|
13280
|
+
};
|
|
13281
|
+
const scopeSummaries = {
|
|
13282
|
+
chapter: $("#dialog-fields").querySelector("#task-chapter-summary"),
|
|
13283
|
+
volume: $("#dialog-fields").querySelector("#task-volume-summary")
|
|
13284
|
+
};
|
|
13285
|
+
const scopeCounts = {
|
|
13286
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-count="chapter"]'),
|
|
13287
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-count="volume"]')
|
|
13288
|
+
};
|
|
13289
|
+
const scopeClearButtons = {
|
|
13290
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-clear="chapter"]'),
|
|
13291
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-clear="volume"]')
|
|
13292
|
+
};
|
|
13293
|
+
const scopeEmptyMessages = {
|
|
13294
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-empty="chapter"]'),
|
|
13295
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-empty="volume"]')
|
|
13296
|
+
};
|
|
13086
13297
|
const description = $("#analysis-type-description");
|
|
13087
13298
|
const relationshipOptions = $("#dialog-fields").querySelector(".relationship-analysis-options");
|
|
13088
13299
|
const relationshipPrompt = relationshipOptions.querySelector('textarea[name="additionalPrompt"]');
|
|
@@ -13100,17 +13311,57 @@ async function openTaskDialog() {
|
|
|
13100
13311
|
const relationshipSourcePreviewContent = relationshipOptions.querySelector("[data-relationship-source-preview-content]");
|
|
13101
13312
|
const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
|
|
13102
13313
|
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-existing-overwrite-card");
|
|
13314
|
+
const contextWarning = $("#dialog-fields").querySelector("[data-analysis-context-warning]");
|
|
13315
|
+
const clearContextWarning = () => {
|
|
13316
|
+
contextWarning.textContent = "";
|
|
13317
|
+
contextWarning.classList.add("hidden");
|
|
13318
|
+
};
|
|
13103
13319
|
const allSettingsOption = document.createElement("option");
|
|
13104
13320
|
allSettingsOption.value = "book-with-settings";
|
|
13105
13321
|
allSettingsOption.textContent = "全书 + 设定集";
|
|
13106
13322
|
const settingsOnlyOption = document.createElement("option");
|
|
13107
13323
|
settingsOnlyOption.value = "settings";
|
|
13108
13324
|
settingsOnlyOption.textContent = "仅设定集";
|
|
13325
|
+
const setTaskScopeBubbleOpen = (kind, open) => {
|
|
13326
|
+
scopeBubbles[kind].classList.toggle("hidden", !open);
|
|
13327
|
+
scopeTriggers[kind].setAttribute("aria-expanded", String(open));
|
|
13328
|
+
if (open) scopeSearches[kind].focus();
|
|
13329
|
+
};
|
|
13330
|
+
const syncTaskScopePicker = (kind) => {
|
|
13331
|
+
const selected = scopeInputs[kind].filter((input) => input.checked);
|
|
13332
|
+
const selectedNames = selected.map((input) => input.closest(".task-scope-option")?.querySelector("strong")?.textContent ?? "");
|
|
13333
|
+
const label = kind === "chapter" ? "选择需要分析的章节" : "选择需要分析的分卷";
|
|
13334
|
+
scopeSummaries[kind].textContent = selectedNames.length
|
|
13335
|
+
? `${selectedNames.slice(0, 2).join("、")}${selectedNames.length > 2 ? ` 等 ${selectedNames.length} 个` : ""}`
|
|
13336
|
+
: label;
|
|
13337
|
+
scopeCounts[kind].textContent = selected.length ? `已选 ${selected.length}` : "未选择";
|
|
13338
|
+
scopeClearButtons[kind].disabled = selected.length === 0;
|
|
13339
|
+
scopeTriggers[kind].setAttribute("aria-label", `筛选${kind === "chapter" ? "章节" : "分卷"},已选择 ${selected.length} 个`);
|
|
13340
|
+
};
|
|
13341
|
+
const filterTaskScopeOptions = (kind) => {
|
|
13342
|
+
const query = scopeSearches[kind].value.trim().toLocaleLowerCase();
|
|
13343
|
+
let visibleCount = 0;
|
|
13344
|
+
for (const input of scopeInputs[kind]) {
|
|
13345
|
+
const option = input.closest(".task-scope-option");
|
|
13346
|
+
const visible = !query || option.dataset.taskScopeSearchName.includes(query);
|
|
13347
|
+
option.classList.toggle("hidden", !visible);
|
|
13348
|
+
if (visible) visibleCount += 1;
|
|
13349
|
+
}
|
|
13350
|
+
scopeEmptyMessages[kind].classList.toggle("hidden", visibleCount > 0);
|
|
13351
|
+
};
|
|
13109
13352
|
const syncChapterField = () => {
|
|
13110
|
-
const
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
|
|
13353
|
+
const activeKind = ["chapter", "volume"].includes(scopeTypeSelect.value) ? scopeTypeSelect.value : null;
|
|
13354
|
+
for (const kind of ["chapter", "volume"]) {
|
|
13355
|
+
const enabled = kind === activeKind;
|
|
13356
|
+
scopeFields[kind].classList.toggle("is-disabled", !enabled);
|
|
13357
|
+
scopeFields[kind].classList.toggle("hidden", !enabled);
|
|
13358
|
+
scopeFields[kind].setAttribute("aria-disabled", String(!enabled));
|
|
13359
|
+
scopeTriggers[kind].disabled = !enabled;
|
|
13360
|
+
for (const input of scopeInputs[kind]) input.disabled = !enabled;
|
|
13361
|
+
if (!enabled) setTaskScopeBubbleOpen(kind, false);
|
|
13362
|
+
syncTaskScopePicker(kind);
|
|
13363
|
+
filterTaskScopeOptions(kind);
|
|
13364
|
+
}
|
|
13114
13365
|
};
|
|
13115
13366
|
const setRelationshipCharacterBubbleOpen = (open) => {
|
|
13116
13367
|
relationshipCharacterBubble.classList.toggle("hidden", !open);
|
|
@@ -13208,10 +13459,12 @@ async function openTaskDialog() {
|
|
|
13208
13459
|
taskTypeSelect.addEventListener("change", () => {
|
|
13209
13460
|
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
13210
13461
|
syncTaskModelDefault();
|
|
13462
|
+
clearContextWarning();
|
|
13211
13463
|
invalidateRelationshipSourcePreview();
|
|
13212
13464
|
syncRelationshipOptions();
|
|
13213
13465
|
});
|
|
13214
13466
|
taskModelSelect.addEventListener("change", () => {
|
|
13467
|
+
clearContextWarning();
|
|
13215
13468
|
invalidateRelationshipSourcePreview();
|
|
13216
13469
|
syncRelationshipOptions();
|
|
13217
13470
|
});
|
|
@@ -13258,11 +13511,39 @@ async function openTaskDialog() {
|
|
|
13258
13511
|
syncRelationshipOptions();
|
|
13259
13512
|
}
|
|
13260
13513
|
});
|
|
13514
|
+
for (const kind of ["chapter", "volume"]) {
|
|
13515
|
+
scopeTriggers[kind].addEventListener("click", () => {
|
|
13516
|
+
setTaskScopeBubbleOpen(kind, scopeTriggers[kind].getAttribute("aria-expanded") !== "true");
|
|
13517
|
+
});
|
|
13518
|
+
scopeSearches[kind].addEventListener("input", () => filterTaskScopeOptions(kind));
|
|
13519
|
+
for (const input of scopeInputs[kind]) input.addEventListener("change", () => {
|
|
13520
|
+
syncTaskScopePicker(kind);
|
|
13521
|
+
clearContextWarning();
|
|
13522
|
+
invalidateRelationshipSourcePreview();
|
|
13523
|
+
});
|
|
13524
|
+
scopeClearButtons[kind].addEventListener("click", () => {
|
|
13525
|
+
for (const input of scopeInputs[kind]) input.checked = false;
|
|
13526
|
+
syncTaskScopePicker(kind);
|
|
13527
|
+
clearContextWarning();
|
|
13528
|
+
invalidateRelationshipSourcePreview();
|
|
13529
|
+
});
|
|
13530
|
+
}
|
|
13261
13531
|
$("#dynamic-form").onclick = (event) => {
|
|
13262
13532
|
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
13533
|
+
for (const kind of ["chapter", "volume"]) {
|
|
13534
|
+
if (!scopeFields[kind].contains(event.target)) setTaskScopeBubbleOpen(kind, false);
|
|
13535
|
+
}
|
|
13263
13536
|
};
|
|
13264
13537
|
$("#dynamic-form").onkeydown = (event) => {
|
|
13265
|
-
if (event.key !== "Escape"
|
|
13538
|
+
if (event.key !== "Escape") return;
|
|
13539
|
+
const openScope = ["chapter", "volume"].find((kind) => !scopeBubbles[kind].classList.contains("hidden"));
|
|
13540
|
+
if (openScope) {
|
|
13541
|
+
event.preventDefault();
|
|
13542
|
+
setTaskScopeBubbleOpen(openScope, false);
|
|
13543
|
+
scopeTriggers[openScope].focus();
|
|
13544
|
+
return;
|
|
13545
|
+
}
|
|
13546
|
+
if (relationshipCharacterBubble.classList.contains("hidden")) return;
|
|
13266
13547
|
event.preventDefault();
|
|
13267
13548
|
setRelationshipCharacterBubbleOpen(false);
|
|
13268
13549
|
relationshipCharacterTrigger.focus();
|
|
@@ -13271,10 +13552,23 @@ async function openTaskDialog() {
|
|
|
13271
13552
|
invalidateRelationshipSourcePreview();
|
|
13272
13553
|
syncChapterField();
|
|
13273
13554
|
});
|
|
13274
|
-
chapterSelect.addEventListener("change", invalidateRelationshipSourcePreview);
|
|
13275
13555
|
syncRelationshipOptions();
|
|
13276
13556
|
}
|
|
13277
13557
|
|
|
13558
|
+
async function refreshAnalysisTaskViewsAfterCreate(workId) {
|
|
13559
|
+
try {
|
|
13560
|
+
await Promise.all([
|
|
13561
|
+
refreshBackgroundTaskCenter({ announce: false }),
|
|
13562
|
+
state.module === "tasks" && state.work?.id === workId
|
|
13563
|
+
? renderTasks(1, { refresh: true })
|
|
13564
|
+
: Promise.resolve()
|
|
13565
|
+
]);
|
|
13566
|
+
} catch (error) {
|
|
13567
|
+
console.error("Failed to refresh analysis task views after creation", error);
|
|
13568
|
+
toast(`任务已创建,但列表刷新失败:${error.message}`, "error");
|
|
13569
|
+
}
|
|
13570
|
+
}
|
|
13571
|
+
|
|
13278
13572
|
function openProviderDialog(item) {
|
|
13279
13573
|
const protocol = item?.protocol ?? "openai-chat-completions";
|
|
13280
13574
|
const providerProtocolOptions = [
|
|
@@ -15476,6 +15770,7 @@ $("#module-nav").addEventListener("click", (event) => {
|
|
|
15476
15770
|
});
|
|
15477
15771
|
$("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!moduleNavExpanded));
|
|
15478
15772
|
$("#module-create-button").addEventListener("click", () => ({ drafts: openDraftDialog, settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
|
|
15773
|
+
bindPlainTextPaste($("#ai-prompt"));
|
|
15479
15774
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
15480
15775
|
updateAiMentionMenu();
|
|
15481
15776
|
setAiContextMeter(null);
|
|
@@ -15490,13 +15785,18 @@ $("#ai-prompt").addEventListener("input", async () => {
|
|
|
15490
15785
|
$("#ai-prompt").addEventListener("focus", () => {
|
|
15491
15786
|
ensureAiModelsLoaded().catch((error) => toast(`模型加载失败:${error.message}`, "error"));
|
|
15492
15787
|
});
|
|
15788
|
+
for (const select of [$("#ai-task"), $("#ai-scope"), $("#ai-roleplay-character"), $("#ai-model")]) {
|
|
15789
|
+
select.addEventListener("pointerdown", blockLockedAiConversationOptionInteraction);
|
|
15790
|
+
select.addEventListener("click", blockLockedAiConversationOptionInteraction);
|
|
15791
|
+
select.addEventListener("keydown", blockLockedAiConversationOptionKeydown);
|
|
15792
|
+
}
|
|
15493
15793
|
$("#ai-model").addEventListener("focus", () => {
|
|
15494
15794
|
ensureAiModelsLoaded().catch((error) => toast(`模型加载失败:${error.message}`, "error"));
|
|
15495
15795
|
});
|
|
15496
15796
|
$("#ai-model").addEventListener("change", (event) => {
|
|
15497
15797
|
if (state.aiPromptSent) {
|
|
15498
15798
|
event.currentTarget.value = state.aiConversationModelId ?? event.currentTarget.value;
|
|
15499
|
-
return toast(
|
|
15799
|
+
return toast(aiConversationOptionLockedMessage);
|
|
15500
15800
|
}
|
|
15501
15801
|
setAiContextMeter(null);
|
|
15502
15802
|
});
|
|
@@ -15510,6 +15810,10 @@ $("#ai-roleplay-character").addEventListener("focus", async () => {
|
|
|
15510
15810
|
});
|
|
15511
15811
|
$("#ai-roleplay-character").addEventListener("change", async (event) => {
|
|
15512
15812
|
const select = event.currentTarget;
|
|
15813
|
+
if (state.aiPromptSent) {
|
|
15814
|
+
renderAiRoleplayCharacterSelect();
|
|
15815
|
+
return toast(aiConversationOptionLockedMessage);
|
|
15816
|
+
}
|
|
15513
15817
|
const characterId = select.value;
|
|
15514
15818
|
select.disabled = true;
|
|
15515
15819
|
try {
|
|
@@ -15527,7 +15831,7 @@ $("#ai-task").addEventListener("change", async (event) => {
|
|
|
15527
15831
|
const nextTaskType = select.value;
|
|
15528
15832
|
if (state.aiPromptSent) {
|
|
15529
15833
|
applyAiConversationTaskType(previousTaskType);
|
|
15530
|
-
return toast(
|
|
15834
|
+
return toast(aiConversationOptionLockedMessage);
|
|
15531
15835
|
}
|
|
15532
15836
|
applyAiConversationTaskType(nextTaskType);
|
|
15533
15837
|
if (state.aiConversationId) {
|
|
@@ -15547,7 +15851,7 @@ $("#ai-task").addEventListener("change", async (event) => {
|
|
|
15547
15851
|
$("#ai-scope").addEventListener("change", (event) => {
|
|
15548
15852
|
if (state.aiPromptSent) {
|
|
15549
15853
|
applyAiConversationContextScope(state.aiContextScope);
|
|
15550
|
-
return toast(
|
|
15854
|
+
return toast(aiConversationOptionLockedMessage);
|
|
15551
15855
|
}
|
|
15552
15856
|
event.currentTarget.title = "";
|
|
15553
15857
|
setAiContextMeter(null);
|
|
@@ -15860,9 +16164,37 @@ $("#ai-history-dialog").addEventListener("cancel", (event) => {
|
|
|
15860
16164
|
closeAiHistoryActionMenu(true);
|
|
15861
16165
|
});
|
|
15862
16166
|
$("#ai-history-action-menu").addEventListener("click", async (event) => {
|
|
15863
|
-
const option = event.target.closest("[data-ai-history-action
|
|
16167
|
+
const option = event.target.closest("[data-ai-history-action]");
|
|
15864
16168
|
const conversation = aiHistoryActionConversation;
|
|
15865
16169
|
if (!option || !conversation || option.disabled) return;
|
|
16170
|
+
const action = option.dataset.aiHistoryAction;
|
|
16171
|
+
if (action === "favorite") {
|
|
16172
|
+
option.disabled = true;
|
|
16173
|
+
try {
|
|
16174
|
+
await favoriteAiConversation(conversation);
|
|
16175
|
+
closeAiHistoryActionMenu(true);
|
|
16176
|
+
} catch (error) {
|
|
16177
|
+
toast(`对话收藏状态更新失败:${error.message}`, "error");
|
|
16178
|
+
option.focus();
|
|
16179
|
+
} finally {
|
|
16180
|
+
option.disabled = false;
|
|
16181
|
+
}
|
|
16182
|
+
return;
|
|
16183
|
+
}
|
|
16184
|
+
if (action === "delete") {
|
|
16185
|
+
option.disabled = true;
|
|
16186
|
+
try {
|
|
16187
|
+
await deleteAiConversation(conversation);
|
|
16188
|
+
closeAiHistoryActionMenu(true);
|
|
16189
|
+
} catch (error) {
|
|
16190
|
+
toast(`对话清理失败:${error.message}`, "error");
|
|
16191
|
+
option.focus();
|
|
16192
|
+
} finally {
|
|
16193
|
+
option.disabled = false;
|
|
16194
|
+
}
|
|
16195
|
+
return;
|
|
16196
|
+
}
|
|
16197
|
+
if (action !== "export") return;
|
|
15866
16198
|
const label = option.querySelector("span");
|
|
15867
16199
|
option.disabled = true;
|
|
15868
16200
|
if (label) label.textContent = "下载中";
|