@musnows/scriverse 0.8.7 → 0.9.0
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/README.en.md +3 -1
- package/README.md +3 -1
- package/dist/ai-analysis-timeout.js +12 -0
- package/dist/ai-analysis-timeout.js.map +1 -0
- package/dist/ai.js +943 -222
- package/dist/ai.js.map +1 -1
- package/dist/app.js +340 -28
- package/dist/app.js.map +1 -1
- package/dist/database.js +220 -2
- package/dist/database.js.map +1 -1
- package/dist/desktop-protocol.js +21 -0
- package/dist/desktop-protocol.js.map +1 -0
- package/dist/offline-sync.js +436 -0
- package/dist/offline-sync.js.map +1 -0
- package/dist/public/ai-connectivity-test.js +5 -2
- package/dist/public/app.js +470 -76
- package/dist/public/index.html +24 -11
- package/dist/public/roleplay-turn.js +100 -0
- package/dist/public/styles.css +76 -17
- package/dist/public/toast-layer.d.ts +4 -0
- package/dist/public/toast-layer.js +5 -0
- package/dist/roleplay-turn.js +98 -0
- package/dist/roleplay-turn.js.map +1 -0
- package/dist/security.js +36 -9
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +13 -3
- package/dist/server-runtime.js.map +1 -1
- package/dist/storage-manifest.js +116 -0
- package/dist/storage-manifest.js.map +1 -0
- package/dist/store.js +152 -24
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +194 -35
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/public/app.js
CHANGED
|
@@ -2,6 +2,14 @@ import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate,
|
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
3
|
import { renderMarkdown } from "/markdown.js?v=20260731-no-external-images-v1";
|
|
4
4
|
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
|
|
5
|
+
import {
|
|
6
|
+
emptyRoleplayScenePin,
|
|
7
|
+
normalizeRoleplayScenePin,
|
|
8
|
+
parseRoleplayUserTurn,
|
|
9
|
+
roleplayScenePinHasContent,
|
|
10
|
+
roleplayUserTurnDisplayText,
|
|
11
|
+
roleplayUserTurnTitleSource
|
|
12
|
+
} from "/roleplay-turn.js?v=20260823-ai-roleplay-scene-turn-v2";
|
|
5
13
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
14
|
import { createAiChatTabManager, normalizeAiChatTabLimit } from "/ai-chat-tabs.js?v=20260816-ai-chat-switcher-v2";
|
|
7
15
|
import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManager, isAiRequestCancellation } from "/ai-request-manager.js?v=20260816-ai-chat-tabs-v1";
|
|
@@ -17,7 +25,7 @@ import {
|
|
|
17
25
|
} from "/foreshadow-reminder.js?v=20260812-editor-reminder-v1";
|
|
18
26
|
import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
|
|
19
27
|
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, modelThinkingEffortLabel, supportsMultimodalModelProtocol } from "/model-config.js?v=20260822-ai-model-thinking-label-v3&feature=ai-provider-responses-v1";
|
|
20
|
-
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=
|
|
28
|
+
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260822-private-ai-endpoint-hint-v1";
|
|
21
29
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
22
30
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
23
31
|
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260818-ai-agent-turn-process-v1";
|
|
@@ -100,6 +108,7 @@ import { systemStatusPresentation } from "/system-status.js?v=20260801-system-he
|
|
|
100
108
|
import { collectS3BackupRunTransitions, s3BackupEncryptionKeyFile, s3BackupEncryptionPresentation, s3BackupFailureToast, s3BackupRootPrefix, s3BackupStatusLabel } from "/s3-backup-ui.js?v=20260810-backup-encryption-v1";
|
|
101
109
|
import { createPresenceClientId, stagePresenceClientIdForRelogin } from "/presence-client-id.js?v=20260810-presence-relogin-v1";
|
|
102
110
|
import { normalizeUploadProgress, uploadProgressText } from "/upload-progress.js?v=20260812-upload-progress-v1";
|
|
111
|
+
import { resolveToastRegionHost } from "/toast-layer.js?v=20260822-toast-modal-host-v1";
|
|
103
112
|
import { buildGlobalReplaceRefreshPlan, resolveGlobalReplaceChapterCount } from "/global-replace-refresh.js?v=20260812-global-replace-tree-v2";
|
|
104
113
|
import {
|
|
105
114
|
clampCropRect,
|
|
@@ -112,6 +121,10 @@ import {
|
|
|
112
121
|
resizeCropRect
|
|
113
122
|
} from "/avatar-crop.js?v=20260725-avatar-crop";
|
|
114
123
|
|
|
124
|
+
const DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS = 300;
|
|
125
|
+
const MIN_AI_ANALYSIS_TIMEOUT_SECONDS = 30;
|
|
126
|
+
const MAX_AI_ANALYSIS_TIMEOUT_SECONDS = 3_600;
|
|
127
|
+
|
|
115
128
|
const defaultPageSizes = Object.freeze({
|
|
116
129
|
drafts: 30,
|
|
117
130
|
settings: 30,
|
|
@@ -461,6 +474,10 @@ function applyWorkAccessMode() {
|
|
|
461
474
|
$(".ai-panel").classList.toggle("permission-hidden", aiHidden);
|
|
462
475
|
$("#ai-prompt").readOnly = aiReadOnly;
|
|
463
476
|
$("#ai-prompt").setAttribute("aria-readonly", String(aiReadOnly));
|
|
477
|
+
$("#ai-scene-direction").readOnly = aiReadOnly;
|
|
478
|
+
$("#ai-scene-location").readOnly = aiReadOnly;
|
|
479
|
+
$("#ai-scene-present").readOnly = aiReadOnly;
|
|
480
|
+
$("#ai-scene-time").readOnly = aiReadOnly;
|
|
464
481
|
$("#ai-send").classList.toggle("permission-hidden", aiReadOnly);
|
|
465
482
|
renderAiRoleplayCharacterSelect();
|
|
466
483
|
updateBackgroundTaskCenterVisibility();
|
|
@@ -2082,7 +2099,7 @@ function createAiChatTabState(input = {}) {
|
|
|
2082
2099
|
roleplayUserCharacter: input.roleplayUserCharacter ?? null,
|
|
2083
2100
|
citations: input.citations ?? [],
|
|
2084
2101
|
references: input.references ?? [],
|
|
2085
|
-
composer: input.composer ?? { text: "", citations: [], references: [], images: [] },
|
|
2102
|
+
composer: input.composer ?? { text: "", citations: [], references: [], images: [], sceneDirection: "", scenePin: emptyRoleplayScenePin() },
|
|
2086
2103
|
contextUsage: input.contextUsage ?? null,
|
|
2087
2104
|
contextWarning: input.contextWarning === true,
|
|
2088
2105
|
lastMessageAt: input.lastMessageAt ?? null,
|
|
@@ -2124,12 +2141,21 @@ function setAiChatTabComposerSnapshot(tab, snapshot) {
|
|
|
2124
2141
|
text: snapshot.text,
|
|
2125
2142
|
citations: tab.citations.map((citation) => ({ ...citation })),
|
|
2126
2143
|
references: tab.references.map((reference) => ({ ...reference })),
|
|
2127
|
-
images: normalizeAiChatImageAttachments(snapshot.images)
|
|
2144
|
+
images: normalizeAiChatImageAttachments(snapshot.images),
|
|
2145
|
+
sceneDirection: String(snapshot.sceneDirection ?? ""),
|
|
2146
|
+
scenePin: normalizeRoleplayScenePin(snapshot.scenePin)
|
|
2128
2147
|
};
|
|
2129
2148
|
}
|
|
2130
2149
|
|
|
2131
2150
|
function clearAiChatTabComposer(tab) {
|
|
2132
|
-
setAiChatTabComposerSnapshot(tab, {
|
|
2151
|
+
setAiChatTabComposerSnapshot(tab, {
|
|
2152
|
+
text: "",
|
|
2153
|
+
citations: [],
|
|
2154
|
+
references: [],
|
|
2155
|
+
images: [],
|
|
2156
|
+
sceneDirection: "",
|
|
2157
|
+
scenePin: normalizeRoleplayScenePin(tab.composer?.scenePin)
|
|
2158
|
+
});
|
|
2133
2159
|
}
|
|
2134
2160
|
|
|
2135
2161
|
function applyAiChatTabState(tab) {
|
|
@@ -2149,6 +2175,7 @@ function applyAiChatTabState(tab) {
|
|
|
2149
2175
|
if (selectedModelId && state.models.some((model) => model.id === selectedModelId)) $("#ai-model").value = selectedModelId;
|
|
2150
2176
|
syncAiModelPicker();
|
|
2151
2177
|
setAiPromptText(tab.composer.text);
|
|
2178
|
+
restoreAiSceneComposer(tab.composer);
|
|
2152
2179
|
renderAiCitations();
|
|
2153
2180
|
renderAiReferences();
|
|
2154
2181
|
renderAiImageAttachments();
|
|
@@ -2170,7 +2197,9 @@ function aiChatTabIsReplaceable(tab = activeAiChatTab()) {
|
|
|
2170
2197
|
&& !(tab.composer?.text || "").trim()
|
|
2171
2198
|
&& !(tab.composer?.citations?.length)
|
|
2172
2199
|
&& !(tab.composer?.references?.length)
|
|
2173
|
-
&& !(tab.composer?.images?.length)
|
|
2200
|
+
&& !(tab.composer?.images?.length)
|
|
2201
|
+
&& !(tab.composer?.sceneDirection || "").trim()
|
|
2202
|
+
&& !roleplayScenePinHasContent(tab.composer?.scenePin));
|
|
2174
2203
|
}
|
|
2175
2204
|
|
|
2176
2205
|
function aiChatTabOpeningSlot() {
|
|
@@ -2443,7 +2472,7 @@ function resetAiFeed(
|
|
|
2443
2472
|
const roleplayName = roleplayCharacter?.name;
|
|
2444
2473
|
const roleplayUserName = roleplayUserCharacter?.name;
|
|
2445
2474
|
feed.innerHTML = roleplayName
|
|
2446
|
-
? `<div class="assistant-message"><span class="message-heading"><span>${esc(roleplayName)}</span></span><div class="message-body"><p>正在扮演 ${esc(roleplayName)}。${roleplayUserName ? `你将以 ${esc(roleplayUserName)} 的身份与我互动。` : "
|
|
2475
|
+
? `<div class="assistant-message"><span class="message-heading"><span>${esc(roleplayName)}</span></span><div class="message-body"><p>正在扮演 ${esc(roleplayName)}。${roleplayUserName ? `你将以 ${esc(roleplayUserName)} 的身份与我互动。` : "我可以通过角色卡、人物关系、知情设定和故事正文回答。"}</p></div></div>`
|
|
2447
2476
|
: '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
2448
2477
|
}
|
|
2449
2478
|
|
|
@@ -2521,8 +2550,9 @@ async function retryAiMessage(message) {
|
|
|
2521
2550
|
const sourceTab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
|
|
2522
2551
|
const userMessage = findAiRetryUserMessage(message);
|
|
2523
2552
|
const prompt = userMessage?.dataset.copyText ?? "";
|
|
2553
|
+
const sceneDirection = userMessage?.dataset.sceneDirection ?? "";
|
|
2524
2554
|
const userMessageId = userMessage?.dataset.messageId ?? "";
|
|
2525
|
-
if (!sourceTab?.conversationId || !userMessageId || !prompt.trim()) {
|
|
2555
|
+
if (!sourceTab?.conversationId || !userMessageId || !(prompt.trim() || sceneDirection.trim())) {
|
|
2526
2556
|
toast("找不到需要重试的用户指令", "error");
|
|
2527
2557
|
return;
|
|
2528
2558
|
}
|
|
@@ -2535,6 +2565,7 @@ async function retryAiMessage(message) {
|
|
|
2535
2565
|
retry: {
|
|
2536
2566
|
message,
|
|
2537
2567
|
prompt,
|
|
2568
|
+
sceneDirection,
|
|
2538
2569
|
citations: aiMessageCitations(userMessage),
|
|
2539
2570
|
images: aiMessageImageAttachments(userMessage),
|
|
2540
2571
|
userMessageId
|
|
@@ -2670,6 +2701,8 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
2670
2701
|
recall_self: "回忆自身",
|
|
2671
2702
|
image: "读取设定图片",
|
|
2672
2703
|
recall_relationship: "回忆人物关系",
|
|
2704
|
+
recall_other: "回忆相识角色",
|
|
2705
|
+
recall_known: "回忆知情设定",
|
|
2673
2706
|
recall_story: "回忆故事",
|
|
2674
2707
|
calculate_time: "计算日期"
|
|
2675
2708
|
};
|
|
@@ -2683,9 +2716,11 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
2683
2716
|
search_drafts: "搜索可能采用、也可能永远不会进入正文或正式设定的未确认临时想法。",
|
|
2684
2717
|
recall_self: "读取当前扮演角色自己的角色卡、档案,以及自己参与的关系、时间线和正文记忆。",
|
|
2685
2718
|
image: "读取设定正文引用的图片附件,并返回多模态模型的理解内容。",
|
|
2686
|
-
recall_relationship: "
|
|
2687
|
-
|
|
2688
|
-
|
|
2719
|
+
recall_relationship: "不传角色列表时读取有关系的角色公开摘要;传入一个或多个角色后读取当前角色与这些角色之间的关系详情。",
|
|
2720
|
+
recall_other: "读取自己通过关系、同一组织或共同参与时间线而认识的其他角色公开摘要。",
|
|
2721
|
+
recall_known: "读取自己所属种族、组织,以及与自己身份相关的世界设定。",
|
|
2722
|
+
recall_story: "查询自己姓名或别名出现过的正文段落,避免全知回忆。",
|
|
2723
|
+
calculate_time: "计算两个 YYYY-MM-DD 日期之间的天数差。"
|
|
2689
2724
|
};
|
|
2690
2725
|
|
|
2691
2726
|
const aiFeedScrollFrames = new WeakMap();
|
|
@@ -3183,7 +3218,7 @@ function renderAiConversationHistory() {
|
|
|
3183
3218
|
}
|
|
3184
3219
|
|
|
3185
3220
|
function defaultAiConversationTitle(prompt) {
|
|
3186
|
-
const normalized = String(prompt ?? "").replace(/\s+/gu, " ").trim();
|
|
3221
|
+
const normalized = roleplayUserTurnTitleSource(String(prompt ?? "")).replace(/\s+/gu, " ").trim();
|
|
3187
3222
|
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
3188
3223
|
}
|
|
3189
3224
|
|
|
@@ -3191,7 +3226,7 @@ function upsertAiConversationSummary(conversation) {
|
|
|
3191
3226
|
if (!conversation?.id) return;
|
|
3192
3227
|
const current = state.aiConversations.find((item) => item.id === conversation.id);
|
|
3193
3228
|
const lastMessage = Array.isArray(conversation.messages) ? conversation.messages.at(-1) : null;
|
|
3194
|
-
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: lastMessage.content } : {}) };
|
|
3229
|
+
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: roleplayUserTurnDisplayText(lastMessage.content) } : {}) };
|
|
3195
3230
|
delete summary.messages;
|
|
3196
3231
|
delete summary.messagesPage;
|
|
3197
3232
|
if (!current && loadedAiConversationsWorkId === state.work?.id) {
|
|
@@ -3214,7 +3249,7 @@ function updateAiConversationSummaryFromMessage(message) {
|
|
|
3214
3249
|
...current,
|
|
3215
3250
|
title: current.title === "新对话" && message.role === "user" ? defaultAiConversationTitle(message.content) : current.title,
|
|
3216
3251
|
messageCount: Number(current.messageCount ?? 0) + 1,
|
|
3217
|
-
preview: message.content,
|
|
3252
|
+
preview: roleplayUserTurnDisplayText(message.content),
|
|
3218
3253
|
...(aiConversationMessageHasImages(message) ? { hasImageAttachments: true, modelLockedByImage: true } : {}),
|
|
3219
3254
|
updatedAt: message.createdAt ?? current.updatedAt
|
|
3220
3255
|
});
|
|
@@ -3349,7 +3384,14 @@ function applyConversationToAiChatTab(tab, conversation) {
|
|
|
3349
3384
|
tab.roleplayUserCharacter = conversation.roleplayUserCharacter ?? null;
|
|
3350
3385
|
tab.citations = [];
|
|
3351
3386
|
tab.references = [];
|
|
3352
|
-
tab.composer = {
|
|
3387
|
+
tab.composer = {
|
|
3388
|
+
text: "",
|
|
3389
|
+
citations: [],
|
|
3390
|
+
references: [],
|
|
3391
|
+
images: [],
|
|
3392
|
+
sceneDirection: "",
|
|
3393
|
+
scenePin: normalizeRoleplayScenePin(conversation.scenePin)
|
|
3394
|
+
};
|
|
3353
3395
|
tab.contextUsage = null;
|
|
3354
3396
|
tab.contextWarning = conversation.contextWarningPending === true;
|
|
3355
3397
|
resetAiFeed(tab.feed, tab.roleplayCharacter, tab.roleplayUserCharacter);
|
|
@@ -3400,16 +3442,30 @@ async function createNewAiConversation(taskType = "chat") {
|
|
|
3400
3442
|
}
|
|
3401
3443
|
}
|
|
3402
3444
|
|
|
3445
|
+
function favoriteCharactersFirst(characters) {
|
|
3446
|
+
const items = Array.isArray(characters) ? characters : [];
|
|
3447
|
+
return [
|
|
3448
|
+
...items.filter((character) => character.isFavorite === true),
|
|
3449
|
+
...items.filter((character) => character.isFavorite !== true)
|
|
3450
|
+
];
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
function roleplayCharacterOptionLabel(character) {
|
|
3454
|
+
const favoriteLabel = character?.isFavorite === true ? "[已收藏] " : "";
|
|
3455
|
+
const deathLabel = character?.isDead ? "(已死亡)" : "";
|
|
3456
|
+
return `${favoriteLabel}${String(character?.name ?? "")}${deathLabel}`;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3403
3459
|
function renderAiRoleplayCharacterSelect() {
|
|
3404
3460
|
const select = $("#ai-roleplay-character");
|
|
3405
3461
|
const selectedId = String(state.aiRoleplayCharacter?.id ?? "");
|
|
3406
|
-
const availableCharacters = state.characters.filter((character) => !character.mergedIntoCharacterId);
|
|
3462
|
+
const availableCharacters = favoriteCharactersFirst(state.characters.filter((character) => !character.mergedIntoCharacterId));
|
|
3407
3463
|
const options = [{ id: "", name: "选择角色卡" }, ...availableCharacters.map((character) => ({
|
|
3408
3464
|
id: String(character.id),
|
|
3409
|
-
name:
|
|
3465
|
+
name: roleplayCharacterOptionLabel(character)
|
|
3410
3466
|
}))];
|
|
3411
3467
|
if (selectedId && !options.some((option) => option.id === selectedId)) {
|
|
3412
|
-
options.push({ id: selectedId, name:
|
|
3468
|
+
options.push({ id: selectedId, name: roleplayCharacterOptionLabel(state.aiRoleplayCharacter) });
|
|
3413
3469
|
}
|
|
3414
3470
|
select.replaceChildren(...options.map((option) => {
|
|
3415
3471
|
const element = document.createElement("option");
|
|
@@ -3425,7 +3481,7 @@ function renderAiRoleplayCharacterSelect() {
|
|
|
3425
3481
|
select.title = canSelectCharacter
|
|
3426
3482
|
? state.aiPromptSent
|
|
3427
3483
|
? aiConversationOptionLockedMessage
|
|
3428
|
-
: "为当前对话选择角色卡;角色扮演时 Agent
|
|
3484
|
+
: "为当前对话选择角色卡;角色扮演时 Agent 可以查询角色记忆、相识角色、知情设定、故事正文和设定图片"
|
|
3429
3485
|
: "当前账户没有角色模块读取权限";
|
|
3430
3486
|
renderAiRoleplayUserCharacterSelect();
|
|
3431
3487
|
}
|
|
@@ -3434,15 +3490,15 @@ function renderAiRoleplayUserCharacterSelect() {
|
|
|
3434
3490
|
const select = $("#ai-roleplay-user-character");
|
|
3435
3491
|
const selectedId = String(state.aiRoleplayUserCharacter?.id ?? "");
|
|
3436
3492
|
const aiCharacterId = String(state.aiRoleplayCharacter?.id ?? "");
|
|
3437
|
-
const availableCharacters = state.characters.filter((character) => (
|
|
3493
|
+
const availableCharacters = favoriteCharactersFirst(state.characters.filter((character) => (
|
|
3438
3494
|
!character.mergedIntoCharacterId && String(character.id) !== aiCharacterId
|
|
3439
|
-
));
|
|
3495
|
+
)));
|
|
3440
3496
|
const options = [{ id: "", name: "选择我的角色(可选)" }, ...availableCharacters.map((character) => ({
|
|
3441
3497
|
id: String(character.id),
|
|
3442
|
-
name:
|
|
3498
|
+
name: roleplayCharacterOptionLabel(character)
|
|
3443
3499
|
}))];
|
|
3444
3500
|
if (selectedId && !options.some((option) => option.id === selectedId)) {
|
|
3445
|
-
options.push({ id: selectedId, name:
|
|
3501
|
+
options.push({ id: selectedId, name: roleplayCharacterOptionLabel(state.aiRoleplayUserCharacter) });
|
|
3446
3502
|
}
|
|
3447
3503
|
select.replaceChildren(...options.map((option) => {
|
|
3448
3504
|
const element = document.createElement("option");
|
|
@@ -3461,7 +3517,7 @@ function renderAiRoleplayUserCharacterSelect() {
|
|
|
3461
3517
|
? "请先选择 AI 扮演的角色"
|
|
3462
3518
|
: state.aiPromptSent
|
|
3463
3519
|
? aiConversationOptionLockedMessage
|
|
3464
|
-
: "
|
|
3520
|
+
: "选择后,主输入是该角色的台词或行动;旁白请写在场景框";
|
|
3465
3521
|
}
|
|
3466
3522
|
|
|
3467
3523
|
const aiConversationOptionLockedMessage = "会话选项在会话开始后不支持修改,若需要修改,请新建会话";
|
|
@@ -3600,8 +3656,9 @@ function syncAiTaskOptions() {
|
|
|
3600
3656
|
$("#ai-scope").disabled = interactionBusy || roleplaySelected;
|
|
3601
3657
|
$("#ai-scope").title = state.aiPromptSent
|
|
3602
3658
|
? aiConversationOptionLockedMessage
|
|
3603
|
-
: roleplaySelected ? "
|
|
3659
|
+
: roleplaySelected ? "角色扮演模式可以查询角色记忆、相识角色、知情设定、故事正文和设定图片" : "";
|
|
3604
3660
|
syncAiModelPicker();
|
|
3661
|
+
syncAiSceneComposer();
|
|
3605
3662
|
}
|
|
3606
3663
|
|
|
3607
3664
|
function applyAiConversationTaskType(taskType) {
|
|
@@ -3638,8 +3695,8 @@ function applyAiRoleplayCharacter(character, userCharacter = null) {
|
|
|
3638
3695
|
$(".ai-panel").classList.toggle("is-roleplaying", active);
|
|
3639
3696
|
$("#ai-prompt").dataset.placeholder = active
|
|
3640
3697
|
? state.aiRoleplayUserCharacter
|
|
3641
|
-
? `以 ${String(state.aiRoleplayUserCharacter.name)}
|
|
3642
|
-
:
|
|
3698
|
+
? `以 ${String(state.aiRoleplayUserCharacter.name)} 的身份输入台词或行动……`
|
|
3699
|
+
: `输入对 ${String(state.aiRoleplayCharacter.name)} 说的台词或行动……`
|
|
3643
3700
|
: "告诉 AI 你想讨论或修改什么……";
|
|
3644
3701
|
renderAiRoleplayCharacterSelect();
|
|
3645
3702
|
syncAiTaskOptions();
|
|
@@ -3946,9 +4003,11 @@ function clearAiPromptComposer() {
|
|
|
3946
4003
|
state.aiReferences = [];
|
|
3947
4004
|
state.aiImageAttachments = [];
|
|
3948
4005
|
setAiPromptText("");
|
|
4006
|
+
setAiSceneDirection("");
|
|
3949
4007
|
renderAiCitations();
|
|
3950
4008
|
renderAiImageAttachments();
|
|
3951
4009
|
hideAiMentionMenu();
|
|
4010
|
+
syncAiSceneComposer();
|
|
3952
4011
|
}
|
|
3953
4012
|
|
|
3954
4013
|
function captureAiPromptComposer() {
|
|
@@ -3956,7 +4015,9 @@ function captureAiPromptComposer() {
|
|
|
3956
4015
|
text: aiPromptText(),
|
|
3957
4016
|
citations: state.aiCitations.map((citation) => ({ ...citation })),
|
|
3958
4017
|
references: state.aiReferences.map((reference) => ({ ...reference })),
|
|
3959
|
-
images: normalizeAiChatImageAttachments(state.aiImageAttachments)
|
|
4018
|
+
images: normalizeAiChatImageAttachments(state.aiImageAttachments),
|
|
4019
|
+
sceneDirection: aiSceneDirectionText(),
|
|
4020
|
+
scenePin: captureAiScenePin()
|
|
3960
4021
|
};
|
|
3961
4022
|
}
|
|
3962
4023
|
|
|
@@ -3965,11 +4026,77 @@ function restoreAiPromptComposer(snapshot) {
|
|
|
3965
4026
|
state.aiReferences = snapshot.references.map((reference) => ({ ...reference }));
|
|
3966
4027
|
state.aiImageAttachments = normalizeAiChatImageAttachments(snapshot.images);
|
|
3967
4028
|
setAiPromptText(snapshot.text);
|
|
4029
|
+
restoreAiSceneComposer(snapshot);
|
|
3968
4030
|
renderAiCitations();
|
|
3969
4031
|
renderAiImageAttachments();
|
|
3970
4032
|
hideAiMentionMenu();
|
|
3971
4033
|
}
|
|
3972
4034
|
|
|
4035
|
+
function aiSceneDirectionText() {
|
|
4036
|
+
return String($("#ai-scene-direction")?.value ?? "");
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
function setAiSceneDirection(value) {
|
|
4040
|
+
const input = $("#ai-scene-direction");
|
|
4041
|
+
if (input) input.value = String(value ?? "");
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
function captureAiScenePin() {
|
|
4045
|
+
return normalizeRoleplayScenePin({
|
|
4046
|
+
location: $("#ai-scene-location")?.value ?? "",
|
|
4047
|
+
present: $("#ai-scene-present")?.value ?? "",
|
|
4048
|
+
timeLabel: $("#ai-scene-time")?.value ?? ""
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
function setAiScenePin(pin) {
|
|
4053
|
+
const normalized = normalizeRoleplayScenePin(pin);
|
|
4054
|
+
const location = $("#ai-scene-location");
|
|
4055
|
+
const present = $("#ai-scene-present");
|
|
4056
|
+
const timeLabel = $("#ai-scene-time");
|
|
4057
|
+
if (location) location.value = normalized.location;
|
|
4058
|
+
if (present) present.value = normalized.present;
|
|
4059
|
+
if (timeLabel) timeLabel.value = normalized.timeLabel;
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
function restoreAiSceneComposer(snapshot = {}) {
|
|
4063
|
+
setAiSceneDirection(snapshot.sceneDirection ?? "");
|
|
4064
|
+
setAiScenePin(snapshot.scenePin);
|
|
4065
|
+
syncAiSceneComposer();
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
function roleplaySceneComposerVisible() {
|
|
4069
|
+
return $("#ai-task").value === "roleplay" && Boolean(state.aiRoleplayCharacter);
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
function syncAiSceneComposer() {
|
|
4073
|
+
const button = $("#ai-scene-button");
|
|
4074
|
+
const panel = $("#ai-scene-panel");
|
|
4075
|
+
if (!button || !panel) return;
|
|
4076
|
+
const visible = roleplaySceneComposerVisible();
|
|
4077
|
+
const busy = aiInteractionBusy();
|
|
4078
|
+
const readOnly = Boolean(state.work) && !canWritePermissionModule(state.work, "ai-chat");
|
|
4079
|
+
button.classList.toggle("hidden", !visible);
|
|
4080
|
+
button.disabled = !visible || busy || readOnly;
|
|
4081
|
+
button.setAttribute("aria-hidden", String(!visible));
|
|
4082
|
+
if (!visible) {
|
|
4083
|
+
panel.classList.add("hidden");
|
|
4084
|
+
button.setAttribute("aria-expanded", "false");
|
|
4085
|
+
}
|
|
4086
|
+
const hasContent = Boolean(aiSceneDirectionText().trim()) || roleplayScenePinHasContent(captureAiScenePin());
|
|
4087
|
+
button.classList.toggle("is-active", hasContent);
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
function toggleAiScenePanel() {
|
|
4091
|
+
const button = $("#ai-scene-button");
|
|
4092
|
+
const panel = $("#ai-scene-panel");
|
|
4093
|
+
if (!button || !panel || button.classList.contains("hidden")) return;
|
|
4094
|
+
const willOpen = panel.classList.contains("hidden");
|
|
4095
|
+
panel.classList.toggle("hidden", !willOpen);
|
|
4096
|
+
button.setAttribute("aria-expanded", String(willOpen));
|
|
4097
|
+
if (willOpen) $("#ai-scene-direction")?.focus();
|
|
4098
|
+
}
|
|
4099
|
+
|
|
3973
4100
|
function aiPromptTextBeforeCursor() {
|
|
3974
4101
|
const prompt = $("#ai-prompt");
|
|
3975
4102
|
const selection = window.getSelection();
|
|
@@ -5020,11 +5147,17 @@ function showAuth(setupRequired, registrationOpen = false, setupTokenRequired =
|
|
|
5020
5147
|
function applyAuthenticatedUser(session) {
|
|
5021
5148
|
state.user = session.user;
|
|
5022
5149
|
state.csrfToken = session.csrfToken;
|
|
5150
|
+
const isSystemAdmin = session.user.isSystemAdmin === true;
|
|
5151
|
+
const accountButton = $("#account-button");
|
|
5023
5152
|
$("#account-name").textContent = session.user.displayName;
|
|
5024
5153
|
renderUserAvatar($("#account-avatar"), session.user);
|
|
5154
|
+
accountButton.setAttribute("aria-label", isSystemAdmin
|
|
5155
|
+
? `账户:${session.user.displayName},系统管理员`
|
|
5156
|
+
: `账户:${session.user.displayName}`);
|
|
5157
|
+
$("#account-admin-mark").classList.toggle("hidden", !isSystemAdmin);
|
|
5025
5158
|
$("#account-menu-display-name").textContent = session.user.displayName;
|
|
5026
5159
|
$("#account-menu-username").textContent = `@${session.user.username}`;
|
|
5027
|
-
$("#account-menu-role").textContent =
|
|
5160
|
+
$("#account-menu-role").textContent = isSystemAdmin ? "系统管理员" : "普通用户";
|
|
5028
5161
|
$("#auth-view").classList.add("hidden");
|
|
5029
5162
|
document.documentElement.classList.remove("login-route");
|
|
5030
5163
|
if (!session.csrfToken) document.body.classList.remove("auth-pending");
|
|
@@ -5109,8 +5242,18 @@ async function initializeAuthentication() {
|
|
|
5109
5242
|
return true;
|
|
5110
5243
|
}
|
|
5111
5244
|
|
|
5245
|
+
function syncToastRegionHost() {
|
|
5246
|
+
const region = $("#toast-region");
|
|
5247
|
+
const host = resolveToastRegionHost([...document.querySelectorAll("dialog[open]")], document.body);
|
|
5248
|
+
if (!host || region.parentElement === host) return false;
|
|
5249
|
+
if (typeof region.hidePopover === "function" && region.matches(":popover-open")) region.hidePopover();
|
|
5250
|
+
host.append(region);
|
|
5251
|
+
return true;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5112
5254
|
function raiseToastRegion() {
|
|
5113
5255
|
const region = $("#toast-region");
|
|
5256
|
+
syncToastRegionHost();
|
|
5114
5257
|
if (typeof region.showPopover !== "function") return;
|
|
5115
5258
|
if (region.matches(":popover-open")) region.hidePopover();
|
|
5116
5259
|
region.showPopover();
|
|
@@ -5263,9 +5406,6 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
5263
5406
|
confirm.textContent = confirmLabel;
|
|
5264
5407
|
actions.append(cancel, confirm);
|
|
5265
5408
|
element.append(heading, description, actions);
|
|
5266
|
-
region.append(element);
|
|
5267
|
-
raiseToastRegion();
|
|
5268
|
-
cancel.focus();
|
|
5269
5409
|
return new Promise((resolve) => {
|
|
5270
5410
|
const finish = (confirmed) => {
|
|
5271
5411
|
element.remove();
|
|
@@ -5280,6 +5420,9 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
5280
5420
|
event.preventDefault();
|
|
5281
5421
|
finish(false);
|
|
5282
5422
|
});
|
|
5423
|
+
region.append(element);
|
|
5424
|
+
raiseToastRegion();
|
|
5425
|
+
cancel.focus();
|
|
5283
5426
|
});
|
|
5284
5427
|
}
|
|
5285
5428
|
|
|
@@ -5340,8 +5483,15 @@ function inputToast(message, { title = "请输入", inputLabel = title, value =
|
|
|
5340
5483
|
|
|
5341
5484
|
document.addEventListener("toggle", (event) => {
|
|
5342
5485
|
const target = event.target;
|
|
5343
|
-
if (target instanceof HTMLDialogElement
|
|
5486
|
+
if (!(target instanceof HTMLDialogElement)) return;
|
|
5487
|
+
const region = $("#toast-region");
|
|
5488
|
+
if (target.open && region.childElementCount) {
|
|
5344
5489
|
raiseToastRegion();
|
|
5490
|
+
return;
|
|
5491
|
+
}
|
|
5492
|
+
if (!target.open && (region.parentElement !== document.body || region.childElementCount)) {
|
|
5493
|
+
const moved = syncToastRegionHost();
|
|
5494
|
+
if (moved && region.childElementCount && typeof region.showPopover === "function") region.showPopover();
|
|
5345
5495
|
}
|
|
5346
5496
|
}, true);
|
|
5347
5497
|
|
|
@@ -8533,6 +8683,116 @@ function pencilIconMarkup() {
|
|
|
8533
8683
|
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20h9"></path><path d="m16.5 3.5 1.4-1.4a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4L16.5 3.5Z"></path></svg>';
|
|
8534
8684
|
}
|
|
8535
8685
|
|
|
8686
|
+
function characterFavoriteIconMarkup() {
|
|
8687
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2L3 9.6l6.2-.9L12 3Z"></path></svg>';
|
|
8688
|
+
}
|
|
8689
|
+
|
|
8690
|
+
function characterFavoriteButton(item) {
|
|
8691
|
+
const isFavorite = item.isFavorite === true;
|
|
8692
|
+
const canFavorite = canEditModule("characters");
|
|
8693
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8694
|
+
const title = canFavorite ? action : "当前账户没有角色模块写入权限";
|
|
8695
|
+
return `<button class="character-favorite-button${isFavorite ? " is-favorite" : ""}" type="button" data-character-favorite="${esc(item.id)}" aria-label="${action}角色“${esc(item.name)}”" aria-pressed="${isFavorite}" title="${title}" ${canFavorite ? "" : "disabled"}>${characterFavoriteIconMarkup()}</button>`;
|
|
8696
|
+
}
|
|
8697
|
+
|
|
8698
|
+
const recordFavoriteConfigs = Object.freeze({
|
|
8699
|
+
character: { module: "characters", resource: "characters", label: "角色", nameField: "name" },
|
|
8700
|
+
draft: { module: "drafts", resource: "drafts", label: "想法", nameField: "title" },
|
|
8701
|
+
setting: { module: "settings", resource: "settings", label: "设定", nameField: "title" },
|
|
8702
|
+
organization: { module: "organizations", resource: "organizations", label: "组织", nameField: "name" }
|
|
8703
|
+
});
|
|
8704
|
+
|
|
8705
|
+
function recordFavoriteButton(type, item, { cardControl = false } = {}) {
|
|
8706
|
+
const config = recordFavoriteConfigs[type];
|
|
8707
|
+
if (!config) return "";
|
|
8708
|
+
const isFavorite = item.isFavorite === true;
|
|
8709
|
+
const canFavorite = canEditModule(config.module);
|
|
8710
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8711
|
+
const title = canFavorite ? action : `当前账户没有${config.label}模块写入权限`;
|
|
8712
|
+
const name = String(item[config.nameField] ?? "");
|
|
8713
|
+
return `<button class="record-favorite-button${cardControl ? " is-card-control" : ""}${isFavorite ? " is-favorite" : ""}" type="button" data-record-favorite="${esc(item.id)}" data-record-favorite-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isFavorite}" title="${title}" ${canFavorite ? "" : "disabled"}>${characterFavoriteIconMarkup()}</button>`;
|
|
8714
|
+
}
|
|
8715
|
+
|
|
8716
|
+
async function toggleRecordFavorite(type, item, render) {
|
|
8717
|
+
const config = recordFavoriteConfigs[type];
|
|
8718
|
+
if (!config) return;
|
|
8719
|
+
const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/favorite`, {
|
|
8720
|
+
method: "PATCH",
|
|
8721
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
8722
|
+
});
|
|
8723
|
+
await render();
|
|
8724
|
+
const favoriteButton = [...$("#module-content").querySelectorAll("[data-record-favorite]")].find((button) => (
|
|
8725
|
+
button.dataset.recordFavoriteType === type && button.dataset.recordFavorite === String(updated.id)
|
|
8726
|
+
));
|
|
8727
|
+
favoriteButton?.focus({ preventScroll: true });
|
|
8728
|
+
const name = String(updated[config.nameField] ?? "");
|
|
8729
|
+
toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
|
|
8730
|
+
}
|
|
8731
|
+
|
|
8732
|
+
function bindRecordFavoriteButtons(type, items, render) {
|
|
8733
|
+
const config = recordFavoriteConfigs[type];
|
|
8734
|
+
if (!config) return;
|
|
8735
|
+
$("#module-content").querySelectorAll(`[data-record-favorite-type="${type}"]`).forEach((button) => button.addEventListener("click", async () => {
|
|
8736
|
+
const item = items.find((candidate) => candidate.id === button.dataset.recordFavorite);
|
|
8737
|
+
if (!item || !canEditModule(config.module)) return;
|
|
8738
|
+
button.disabled = true;
|
|
8739
|
+
try {
|
|
8740
|
+
await toggleRecordFavorite(type, item, render);
|
|
8741
|
+
} catch (error) {
|
|
8742
|
+
button.disabled = false;
|
|
8743
|
+
toast(`${config.label}收藏状态更新失败:${error.message}`, "error");
|
|
8744
|
+
}
|
|
8745
|
+
}));
|
|
8746
|
+
}
|
|
8747
|
+
|
|
8748
|
+
function syncEntityDetailFavoriteButton(button, type, item) {
|
|
8749
|
+
const config = recordFavoriteConfigs[type];
|
|
8750
|
+
const visible = Boolean(config && item);
|
|
8751
|
+
button.classList.toggle("hidden", !visible);
|
|
8752
|
+
button.innerHTML = characterFavoriteIconMarkup();
|
|
8753
|
+
if (!visible) {
|
|
8754
|
+
button.disabled = true;
|
|
8755
|
+
button.classList.remove("is-favorite");
|
|
8756
|
+
button.setAttribute("aria-pressed", "false");
|
|
8757
|
+
button.removeAttribute("aria-label");
|
|
8758
|
+
button.removeAttribute("title");
|
|
8759
|
+
return;
|
|
8760
|
+
}
|
|
8761
|
+
const isFavorite = item.isFavorite === true;
|
|
8762
|
+
const canFavorite = canEditModule(config.module);
|
|
8763
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8764
|
+
const name = String(item[config.nameField] ?? "");
|
|
8765
|
+
button.classList.toggle("is-favorite", isFavorite);
|
|
8766
|
+
button.setAttribute("aria-pressed", String(isFavorite));
|
|
8767
|
+
button.setAttribute("aria-label", `${action}${config.label}“${name}”`);
|
|
8768
|
+
button.title = canFavorite ? action : `当前账户没有${config.label}模块写入权限`;
|
|
8769
|
+
button.disabled = !canFavorite;
|
|
8770
|
+
}
|
|
8771
|
+
|
|
8772
|
+
function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
|
|
8773
|
+
syncEntityDetailFavoriteButton(button, type, getItem());
|
|
8774
|
+
button.onclick = async () => {
|
|
8775
|
+
const config = recordFavoriteConfigs[type];
|
|
8776
|
+
const item = getItem();
|
|
8777
|
+
if (!config || !item || !canEditModule(config.module)) return;
|
|
8778
|
+
button.disabled = true;
|
|
8779
|
+
try {
|
|
8780
|
+
const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/favorite`, {
|
|
8781
|
+
method: "PATCH",
|
|
8782
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
8783
|
+
});
|
|
8784
|
+
onUpdated(updated);
|
|
8785
|
+
syncEntityDetailFavoriteButton(button, type, getItem() ?? updated);
|
|
8786
|
+
button.focus({ preventScroll: true });
|
|
8787
|
+
const name = String(updated[config.nameField] ?? "");
|
|
8788
|
+
toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
|
|
8789
|
+
} catch (error) {
|
|
8790
|
+
syncEntityDetailFavoriteButton(button, type, getItem());
|
|
8791
|
+
toast(`${config.label}收藏状态更新失败:${error instanceof Error ? error.message : "未知错误"}`, "error");
|
|
8792
|
+
}
|
|
8793
|
+
};
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8536
8796
|
function recordCardEditButton(attribute, id, label) {
|
|
8537
8797
|
return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
|
|
8538
8798
|
}
|
|
@@ -8868,9 +9128,10 @@ function openReviewDetailDialog(item) {
|
|
|
8868
9128
|
}
|
|
8869
9129
|
|
|
8870
9130
|
function settingRecordActions(item) {
|
|
8871
|
-
|
|
9131
|
+
const recordAction = canEditModule("settings")
|
|
8872
9132
|
? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
|
|
8873
9133
|
: recordHistoryButton("setting", item.id, item.title);
|
|
9134
|
+
return `${recordFavoriteButton("setting", item)}${recordAction}`;
|
|
8874
9135
|
}
|
|
8875
9136
|
|
|
8876
9137
|
function draftTypeLabel(draftType) {
|
|
@@ -8924,6 +9185,7 @@ async function deleteDraft(item) {
|
|
|
8924
9185
|
}
|
|
8925
9186
|
|
|
8926
9187
|
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
9188
|
+
let draftDialogItem = item;
|
|
8927
9189
|
const viewOnly = readOnly || !canEditModule("drafts");
|
|
8928
9190
|
const volumeOptions = [["", "全局(不绑定分卷)"], ...(state.work?.volumes ?? []).map((volume) => [volume.id, volume.title])];
|
|
8929
9191
|
const settingModuleOptions = [["", "全局(不绑定设定模块)"], ...draftSettingModules];
|
|
@@ -8981,6 +9243,18 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
8981
9243
|
else control.readOnly = true;
|
|
8982
9244
|
});
|
|
8983
9245
|
}
|
|
9246
|
+
if (item && viewOnly) {
|
|
9247
|
+
const headerActions = $("#dialog-context-actions");
|
|
9248
|
+
headerActions.innerHTML = `<button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
|
|
9249
|
+
bindEntityDetailFavoriteButton($("#draft-dialog-favorite"), "draft", () => draftDialogItem, (updated) => {
|
|
9250
|
+
draftDialogItem = updated;
|
|
9251
|
+
void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
|
|
9252
|
+
});
|
|
9253
|
+
$("#draft-dialog-edit")?.addEventListener("click", () => {
|
|
9254
|
+
$("#form-dialog").close();
|
|
9255
|
+
openDraftDialog(draftDialogItem);
|
|
9256
|
+
});
|
|
9257
|
+
}
|
|
8984
9258
|
}
|
|
8985
9259
|
|
|
8986
9260
|
async function renderDrafts(page = moduleListPages.drafts) {
|
|
@@ -9014,9 +9288,9 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
9014
9288
|
<details class="character-filter-dropdown"><summary><span>按绑定位置筛选</span><strong>${selectedBindingKeys.size ? `已选 ${selectedBindingKeys.size} 项` : "全部位置"}</strong></summary><div id="draft-binding-filter" class="character-filter-options">${bindingOptions.map(([value, label]) => `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedBindingKeys.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`).join("")}</div></details>
|
|
9015
9289
|
<div class="character-filter-toolbar-actions">${hasDraftFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${drafts.length} 条想法</span>` : ""}<button id="clear-draft-filters" class="ghost-button" type="button" ${hasDraftFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
9016
9290
|
</section>`;
|
|
9017
|
-
const actions = (item) => canEditModule("drafts")
|
|
9291
|
+
const actions = (item) => `${recordFavoriteButton("draft", item)}${canEditModule("drafts")
|
|
9018
9292
|
? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
|
|
9019
|
-
: recordHistoryButton("draft", item.id, item.title)
|
|
9293
|
+
: recordHistoryButton("draft", item.id, item.title)}`;
|
|
9020
9294
|
const cards = `<div class="card-grid">${pageResult.items.map((item) => `
|
|
9021
9295
|
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看想法 ${esc(item.title)}">
|
|
9022
9296
|
<small>${esc(draftTypeLabel(item.draftType))} · ${esc(draftBindingLabel(item))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
@@ -9072,6 +9346,10 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
9072
9346
|
$("#module-content").querySelectorAll("[data-edit-draft]").forEach((button) => button.addEventListener("click", async () => {
|
|
9073
9347
|
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(button.dataset.editDraft)}`));
|
|
9074
9348
|
}));
|
|
9349
|
+
bindRecordFavoriteButtons("draft", pageResult.items, async () => {
|
|
9350
|
+
moduleListPages.drafts = 1;
|
|
9351
|
+
await renderDrafts(1);
|
|
9352
|
+
});
|
|
9075
9353
|
bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
|
|
9076
9354
|
}
|
|
9077
9355
|
|
|
@@ -9135,6 +9413,10 @@ async function renderSettings(page = moduleListPages.settings) {
|
|
|
9135
9413
|
bindModulePagination("settings", renderSettingResults);
|
|
9136
9414
|
const openSetting = async (id, readOnly) => openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly });
|
|
9137
9415
|
$("#setting-filter-results").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => { void openSetting(button.dataset.editSetting, false); }));
|
|
9416
|
+
bindRecordFavoriteButtons("setting", pageResult.items, async () => {
|
|
9417
|
+
moduleListPages.settings = 1;
|
|
9418
|
+
await renderSettings(1);
|
|
9419
|
+
});
|
|
9138
9420
|
bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
|
|
9139
9421
|
};
|
|
9140
9422
|
|
|
@@ -9171,6 +9453,21 @@ async function renderSettings(page = moduleListPages.settings) {
|
|
|
9171
9453
|
renderSettingResults(page);
|
|
9172
9454
|
}
|
|
9173
9455
|
|
|
9456
|
+
async function toggleCharacterFavorite(item) {
|
|
9457
|
+
const updated = await api(`/api/characters/${encodeURIComponent(item.id)}/favorite`, {
|
|
9458
|
+
method: "PATCH",
|
|
9459
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
9460
|
+
});
|
|
9461
|
+
if (loadedAiReferencesWorkId === state.work?.id) {
|
|
9462
|
+
state.characters = upsertEntityCollection(state.characters, updated);
|
|
9463
|
+
renderAiRoleplayCharacterSelect();
|
|
9464
|
+
}
|
|
9465
|
+
characterListPage = 1;
|
|
9466
|
+
await renderCharacters(1);
|
|
9467
|
+
$("#module-content").querySelector(`[data-character-favorite="${CSS.escape(String(updated.id))}"]`)?.focus({ preventScroll: true });
|
|
9468
|
+
toast(updated.isFavorite ? `已收藏角色“${updated.name}”` : `已取消收藏角色“${updated.name}”`);
|
|
9469
|
+
}
|
|
9470
|
+
|
|
9174
9471
|
async function renderCharacters(page = characterListPage) {
|
|
9175
9472
|
const hasCharacterFilters = characterFilters.raceIds.length > 0
|
|
9176
9473
|
|| characterFilters.organizationIds.length > 0
|
|
@@ -9193,14 +9490,14 @@ async function renderCharacters(page = characterListPage) {
|
|
|
9193
9490
|
[state.races, state.organizations] = [races, organizations];
|
|
9194
9491
|
mountModuleCount(characterPage.total);
|
|
9195
9492
|
const layout = readModuleLayout();
|
|
9196
|
-
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)
|
|
9493
|
+
const characterActions = (item) => `${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
|
|
9197
9494
|
const characterLockBadge = (item) => item.lockedFields.length
|
|
9198
9495
|
? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
|
|
9199
9496
|
: "";
|
|
9200
9497
|
const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
|
|
9201
9498
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
9202
9499
|
return `
|
|
9203
|
-
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
9500
|
+
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
9204
9501
|
<div class="character-card-heading">${characterAvatarHtml(item)}<h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
|
|
9205
9502
|
${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
|
|
9206
9503
|
<div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
|
|
@@ -9266,6 +9563,17 @@ async function renderCharacters(page = characterListPage) {
|
|
|
9266
9563
|
? `${layout === "rows" ? characterRows() : characterCards()}${pagination}`
|
|
9267
9564
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
9268
9565
|
bindModuleLayoutToggle(renderCharacters);
|
|
9566
|
+
$("#module-content").querySelectorAll("[data-character-favorite]").forEach((button) => button.addEventListener("click", async () => {
|
|
9567
|
+
const item = pageCharacters.find((candidate) => candidate.id === button.dataset.characterFavorite);
|
|
9568
|
+
if (!item || !canEditModule("characters")) return;
|
|
9569
|
+
button.disabled = true;
|
|
9570
|
+
try {
|
|
9571
|
+
await toggleCharacterFavorite(item);
|
|
9572
|
+
} catch (error) {
|
|
9573
|
+
button.disabled = false;
|
|
9574
|
+
toast(`角色收藏状态更新失败:${error.message}`, "error");
|
|
9575
|
+
}
|
|
9576
|
+
}));
|
|
9269
9577
|
const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
9270
9578
|
$("#character-race-filter").addEventListener("change", async () => {
|
|
9271
9579
|
characterFiltersPanelOpen = true;
|
|
@@ -9420,14 +9728,14 @@ async function renderOrganizations(page = moduleListPages.organizations) {
|
|
|
9420
9728
|
moduleListPages.organizations = pageResult.page;
|
|
9421
9729
|
const layout = readModuleLayout();
|
|
9422
9730
|
const canEditOrganizations = canEditModule("organizations");
|
|
9423
|
-
const organizationActions = (item) => canEditOrganizations
|
|
9731
|
+
const organizationActions = (item, { cardControl = false } = {}) => `${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
|
|
9424
9732
|
? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
|
|
9425
|
-
: recordHistoryButton("organization", item.id, item.name)
|
|
9733
|
+
: recordHistoryButton("organization", item.id, item.name)}`;
|
|
9426
9734
|
const organizationCardActions = (item) => canEditOrganizations
|
|
9427
|
-
? organizationActions(item)
|
|
9735
|
+
? organizationActions(item, { cardControl: true })
|
|
9428
9736
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
9429
9737
|
const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
|
|
9430
|
-
<article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
9738
|
+
<article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
9431
9739
|
<h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
9432
9740
|
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
9433
9741
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
@@ -9452,6 +9760,10 @@ async function renderOrganizations(page = moduleListPages.organizations) {
|
|
|
9452
9760
|
bindModulePagination("organizations", renderOrganizations);
|
|
9453
9761
|
const openOrganization = async (id, readOnly) => openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly });
|
|
9454
9762
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => { void openOrganization(button.dataset.editOrganization, false); }));
|
|
9763
|
+
bindRecordFavoriteButtons("organization", pageResult.items, async () => {
|
|
9764
|
+
moduleListPages.organizations = 1;
|
|
9765
|
+
await renderOrganizations(1);
|
|
9766
|
+
});
|
|
9455
9767
|
bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
|
|
9456
9768
|
}
|
|
9457
9769
|
|
|
@@ -11073,7 +11385,7 @@ function openTaskDetailDialog(task, trace) {
|
|
|
11073
11385
|
<p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
|
|
11074
11386
|
<p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
|
|
11075
11387
|
<div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
|
|
11076
|
-
<div><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
|
|
11388
|
+
<div class="task-detail-failures"><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
|
|
11077
11389
|
<div><strong>结果摘要</strong>${resultPreview}</div>
|
|
11078
11390
|
${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><button class="ghost-button" type="button" data-rerun-task-model="${esc(task.id)}">换模型重试</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
|
|
11079
11391
|
</section>
|
|
@@ -11112,7 +11424,7 @@ function renderProviderCards(providers, models, protocolOptions) {
|
|
|
11112
11424
|
: "";
|
|
11113
11425
|
return `
|
|
11114
11426
|
<article class="record-card provider-card ${provider.status === "disabled" ? "is-disabled" : ""}"><div class="provider-card-meta"><small>平台级 · ${esc(providerProtocolLabel(provider.protocol, protocolOptions))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><span class="provider-status-badge ${providerStatusClass}">${esc(providerStatusLabel(provider.status))}</span></div><h3>${esc(provider.name)}</h3>
|
|
11115
|
-
${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n最大输出参数:${esc(provider.maxTokensParameter ?? "max_tokens")}\n思考类型:${esc(provider.thinkingType ?? "enabled")}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}\n每日 Token 额度:${provider.dailyTokenQuota === null || provider.dailyTokenQuota === undefined ? "未限制" : Number(provider.dailyTokenQuota).toLocaleString("zh-CN")} · 每月 Token 额度:${provider.monthlyTokenQuota === null || provider.monthlyTokenQuota === undefined ? "未限制" : Number(provider.monthlyTokenQuota).toLocaleString("zh-CN")}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
|
|
11427
|
+
${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n最大输出参数:${esc(provider.maxTokensParameter ?? "max_tokens")}\n思考类型:${esc(provider.thinkingType ?? "enabled")}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}\n分析请求超时:${Number(provider.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS).toLocaleString("zh-CN")} 秒\n每日 Token 额度:${provider.dailyTokenQuota === null || provider.dailyTokenQuota === undefined ? "未限制" : Number(provider.dailyTokenQuota).toLocaleString("zh-CN")} · 每月 Token 额度:${provider.monthlyTokenQuota === null || provider.monthlyTokenQuota === undefined ? "未限制" : Number(provider.monthlyTokenQuota).toLocaleString("zh-CN")}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
|
|
11116
11428
|
<div class="provider-models">${providerModels.map((model) => {
|
|
11117
11429
|
const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
|
|
11118
11430
|
const modelStatus = !model.enabled
|
|
@@ -11743,6 +12055,9 @@ function appendTokenUsageDetailRow(parent, label, usage, isSummary = false) {
|
|
|
11743
12055
|
cell.textContent = tokenUsageDetailCount(usage?.[key]);
|
|
11744
12056
|
row.append(cell);
|
|
11745
12057
|
});
|
|
12058
|
+
const price = document.createElement("td");
|
|
12059
|
+
price.textContent = formatEstimatedCost(usage?.estimatedCost);
|
|
12060
|
+
row.append(price);
|
|
11746
12061
|
parent.append(row);
|
|
11747
12062
|
}
|
|
11748
12063
|
|
|
@@ -11778,7 +12093,7 @@ function showTokenUsageDetails(usage, title, trigger) {
|
|
|
11778
12093
|
const description = document.createElement("p");
|
|
11779
12094
|
description.id = "token-usage-details-description";
|
|
11780
12095
|
description.className = "token-usage-details-note";
|
|
11781
|
-
description.textContent = "Raw Input 为不含缓存的输入 Token;Cache Write 与 Cache Read
|
|
12096
|
+
description.textContent = "Raw Input 为不含缓存的输入 Token;Cache Write 与 Cache Read 也是输入组成部分,并已包含在总计中。估算价格按美元显示,未匹配价格的模型显示为暂无价格。汇总行包含当前范围内全部模型。";
|
|
11782
12097
|
body.append(description);
|
|
11783
12098
|
|
|
11784
12099
|
const tableScroll = document.createElement("div");
|
|
@@ -11790,7 +12105,7 @@ function showTokenUsageDetails(usage, title, trigger) {
|
|
|
11790
12105
|
table.append(caption);
|
|
11791
12106
|
const head = document.createElement("thead");
|
|
11792
12107
|
const headRow = document.createElement("tr");
|
|
11793
|
-
["模型", "Raw Input", "Cache Write", "Cache Read", "Output", "总计", "调用", "估算调用"].forEach((label) => {
|
|
12108
|
+
["模型", "Raw Input", "Cache Write", "Cache Read", "Output", "总计", "调用", "估算调用", "估算价格"].forEach((label) => {
|
|
11794
12109
|
const cell = document.createElement("th");
|
|
11795
12110
|
cell.scope = "col";
|
|
11796
12111
|
cell.textContent = label;
|
|
@@ -11804,7 +12119,7 @@ function showTokenUsageDetails(usage, title, trigger) {
|
|
|
11804
12119
|
if (!models.length) {
|
|
11805
12120
|
const emptyRow = document.createElement("tr");
|
|
11806
12121
|
const emptyCell = document.createElement("td");
|
|
11807
|
-
emptyCell.colSpan =
|
|
12122
|
+
emptyCell.colSpan = 9;
|
|
11808
12123
|
emptyCell.textContent = "还没有模型用量记录。";
|
|
11809
12124
|
emptyRow.append(emptyCell);
|
|
11810
12125
|
bodyRows.append(emptyRow);
|
|
@@ -11977,7 +12292,7 @@ async function renderBookAiSettings() {
|
|
|
11977
12292
|
);
|
|
11978
12293
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
11979
12294
|
"beforeend",
|
|
11980
|
-
`<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><label><input name="agent-tool" type="checkbox" value="calculate_time" ${agentTools.has("calculate_time") ? "checked" : ""}><span><strong>计算日期</strong><small
|
|
12295
|
+
`<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><label><input name="agent-tool" type="checkbox" value="calculate_time" ${agentTools.has("calculate_time") ? "checked" : ""}><span><strong>计算日期</strong><small>计算两个 YYYY-MM-DD 日期之间的天数差,不读取作品内容。</small></span></label>`
|
|
11981
12296
|
);
|
|
11982
12297
|
if (!canEditModule("ai-settings")) {
|
|
11983
12298
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -12691,6 +13006,7 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
12691
13006
|
const submitLabel = options.submitLabel ?? "保存";
|
|
12692
13007
|
let submitting = false;
|
|
12693
13008
|
let disabledStates = [];
|
|
13009
|
+
$("#dialog-context-actions").replaceChildren();
|
|
12694
13010
|
$("#dialog-title").textContent = title;
|
|
12695
13011
|
$("#dialog-title").classList.toggle("hidden", Boolean(options.titleInput));
|
|
12696
13012
|
titleInput.classList.toggle("hidden", !options.titleInput);
|
|
@@ -13160,6 +13476,10 @@ function openSettingEditor(item = null, { readOnly = false } = {}) {
|
|
|
13160
13476
|
const editButton = $("#setting-editor-edit");
|
|
13161
13477
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule("settings"));
|
|
13162
13478
|
editButton.onclick = () => openSettingEditor(settingEditorItem);
|
|
13479
|
+
bindEntityDetailFavoriteButton($("#setting-editor-favorite"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
|
|
13480
|
+
settingEditorItem = updated;
|
|
13481
|
+
state.settings = upsertEntityCollection(state.settings, updated);
|
|
13482
|
+
});
|
|
13163
13483
|
const statusButtons = [$("#setting-editor-confirm"), $("#setting-editor-deprecate")];
|
|
13164
13484
|
syncSettingEditorChrome(viewOnly);
|
|
13165
13485
|
$("#setting-editor-history").onclick = async () => {
|
|
@@ -14186,11 +14506,12 @@ function renderCharacterEditorFields(item) {
|
|
|
14186
14506
|
(canReadModule("editor")
|
|
14187
14507
|
? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
|
|
14188
14508
|
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
14189
|
-
characterEditorSection("profile", "人物档案", "
|
|
14509
|
+
characterEditorSection("profile", "人物档案", "记录人物定位、行为动力、公开人设和便于创作时快速理解的简介。",
|
|
14190
14510
|
field("code", "编号", "text", item?.code) +
|
|
14191
14511
|
field("identity", "身份与定位", "text", item?.attributes?.identity) +
|
|
14192
14512
|
field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
|
|
14193
|
-
field("summary", "人物简介", "textarea", item?.profile?.summary)
|
|
14513
|
+
field("summary", "人物简介", "textarea", item?.profile?.summary) +
|
|
14514
|
+
'<div class="form-field"><span>人设摘要</span><small>关系扮演时作为公开人设注入对方可见的角色卡,不会包含私密档案或 Markdown 章节。</small><textarea name="personaSummary" maxlength="20000" aria-label="人设摘要">' + esc(item?.profile?.personaSummary ?? "") + "</textarea></div>"),
|
|
14194
14515
|
characterEditorSection("settings", "扩展设定", "可用短属性和 Markdown 长章节承载形态、能力、生态、经历与研究记录。",
|
|
14195
14516
|
field("details", "扩展属性", "key-value-list", item?.attributes?.details) +
|
|
14196
14517
|
'<div id="character-markdown-sections" class="character-markdown-sections"></div>'),
|
|
@@ -14238,7 +14559,8 @@ function collectCharacterBody(form) {
|
|
|
14238
14559
|
profile: {
|
|
14239
14560
|
...profile,
|
|
14240
14561
|
motivation: String(form.get("motivation") ?? "").trim(),
|
|
14241
|
-
summary: String(form.get("summary") ?? "").trim()
|
|
14562
|
+
summary: String(form.get("summary") ?? "").trim(),
|
|
14563
|
+
personaSummary: String(form.get("personaSummary") ?? "").trim()
|
|
14242
14564
|
},
|
|
14243
14565
|
currentState: buildCharacterState(form.getAll("stateKey"), form.getAll("stateValue"), item?.currentState ?? {}),
|
|
14244
14566
|
lockedFields: form.getAll("lockedFields").map((value) => String(value).trim()).filter(Boolean),
|
|
@@ -14390,6 +14712,13 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
|
14390
14712
|
const editButton = $("#character-editor-edit");
|
|
14391
14713
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule("characters"));
|
|
14392
14714
|
editButton.onclick = () => void openCharacterEditor(characterEditorItem);
|
|
14715
|
+
bindEntityDetailFavoriteButton($("#character-editor-favorite"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
|
|
14716
|
+
characterEditorItem = updated;
|
|
14717
|
+
if (loadedAiReferencesWorkId === state.work?.id) {
|
|
14718
|
+
state.characters = upsertEntityCollection(state.characters, updated);
|
|
14719
|
+
renderAiRoleplayCharacterSelect();
|
|
14720
|
+
}
|
|
14721
|
+
});
|
|
14393
14722
|
document.querySelectorAll("[data-character-editor-tab]").forEach((button) => {
|
|
14394
14723
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
14395
14724
|
});
|
|
@@ -14576,6 +14905,10 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
|
|
|
14576
14905
|
editButton.textContent = `编辑${label}`;
|
|
14577
14906
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule(module));
|
|
14578
14907
|
editButton.onclick = () => void openKnowledgeEditor(kind, knowledgeEditorItem);
|
|
14908
|
+
bindEntityDetailFavoriteButton($("#knowledge-editor-favorite"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
|
|
14909
|
+
knowledgeEditorItem = updated;
|
|
14910
|
+
state.organizations = upsertEntityCollection(state.organizations, updated);
|
|
14911
|
+
});
|
|
14579
14912
|
const form = $("#knowledge-editor-form");
|
|
14580
14913
|
form.onsubmit = async (event) => {
|
|
14581
14914
|
event.preventDefault();
|
|
@@ -15487,6 +15820,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
|
|
|
15487
15820
|
const monthlyTokenQuota = item?.monthlyTokenQuota ?? null;
|
|
15488
15821
|
const providerTokenQuotaFields = `<div class="form-field provider-token-quota-fields" data-provider-token-quota-fields><span>供应商 Token 额度</span><small>按服务器部署时区统计该供应商跨所有小说的输入与输出 Token;与单个小说额度独立。额度必须设置为大于 0;低于每日 10,000 或每月 1,000,000 时仅提示风险。</small><div class="provider-token-quota-row"><label class="checkbox-field provider-token-quota-toggle"><input name="dailyTokenQuotaEnabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}><span>启用每日额度</span></label><label class="provider-token-quota-input">每日额度<input name="dailyTokenQuota" type="number" min="1" max="2000000000" step="1" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="供应商每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label></div><div class="provider-token-quota-row"><label class="checkbox-field provider-token-quota-toggle"><input name="monthlyTokenQuotaEnabled" type="checkbox" ${monthlyTokenQuota === null ? "" : "checked"}><span>启用每月额度</span></label><label class="provider-token-quota-input">每月额度<input name="monthlyTokenQuota" type="number" min="1" max="2000000000" step="1" value="${esc(String(monthlyTokenQuota ?? 10000))}" aria-label="供应商每月 Token 额度" ${monthlyTokenQuota === null ? "disabled" : ""}></label></div></div>`;
|
|
15489
15822
|
const thinkingTypeField = `<div class="form-field provider-thinking-type-field" data-provider-thinking-type-field><span>思考类型(开启时)</span><label class="checkbox-field"><input name="useAdaptiveThinking" type="checkbox" ${useAdaptiveThinking ? "checked" : ""} aria-describedby="provider-thinking-type-hint"><span>使用 adaptive(关闭时发送 enabled)</span></label><small id="provider-thinking-type-hint">关闭模型思考时仍发送 disabled;请只在供应商支持 adaptive 时开启。</small></div>`;
|
|
15823
|
+
const analysisTimeoutField = `<div class="form-field provider-analysis-timeout-field" data-provider-analysis-timeout-field><span>分析请求超时</span><label>单次请求(秒)<input name="analysisTimeoutSeconds" type="number" min="${MIN_AI_ANALYSIS_TIMEOUT_SECONDS}" max="${MAX_AI_ANALYSIS_TIMEOUT_SECONDS}" step="1" value="${esc(String(item?.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS))}" aria-describedby="provider-analysis-timeout-hint"></label><small id="provider-analysis-timeout-hint">用于全书分析和关系分析的单次非流式请求;默认 300 秒,可设置 30–3600 秒。</small></div>`;
|
|
15490
15824
|
openDialog(
|
|
15491
15825
|
item ? "编辑 AI 供应商" : "新建 AI 供应商",
|
|
15492
15826
|
field("name", "显示名称", "text", item?.name)
|
|
@@ -15497,6 +15831,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
|
|
|
15497
15831
|
+ thinkingTypeField
|
|
15498
15832
|
+ field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10)
|
|
15499
15833
|
+ field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10)
|
|
15834
|
+
+ analysisTimeoutField
|
|
15500
15835
|
+ providerTokenQuotaFields
|
|
15501
15836
|
+ field("note", "用途备注", "textarea", item?.note)
|
|
15502
15837
|
+ field("enabled", item ? "启用供应商" : "立即启用", "checkbox", item ? item.status === "enabled" : true),
|
|
@@ -15509,11 +15844,17 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
|
|
|
15509
15844
|
thinkingType: form.get("useAdaptiveThinking") === "on" ? "adaptive" : "enabled",
|
|
15510
15845
|
concurrencyLimit: Number(form.get("concurrencyLimit")),
|
|
15511
15846
|
rpmLimit: Number(form.get("rpmLimit")),
|
|
15847
|
+
analysisTimeoutSeconds: Number(form.get("analysisTimeoutSeconds")),
|
|
15512
15848
|
dailyTokenQuota: form.get("dailyTokenQuotaEnabled") === "on" ? Number(form.get("dailyTokenQuota")) : null,
|
|
15513
15849
|
monthlyTokenQuota: form.get("monthlyTokenQuotaEnabled") === "on" ? Number(form.get("monthlyTokenQuota")) : null,
|
|
15514
15850
|
note: form.get("note"),
|
|
15515
15851
|
status: form.get("enabled") === "on" ? "enabled" : "disabled"
|
|
15516
15852
|
};
|
|
15853
|
+
if (
|
|
15854
|
+
!Number.isInteger(body.analysisTimeoutSeconds)
|
|
15855
|
+
|| body.analysisTimeoutSeconds < MIN_AI_ANALYSIS_TIMEOUT_SECONDS
|
|
15856
|
+
|| body.analysisTimeoutSeconds > MAX_AI_ANALYSIS_TIMEOUT_SECONDS
|
|
15857
|
+
) throw new Error("分析请求超时必须设置为 30–3600 秒的整数");
|
|
15517
15858
|
if (!item || String(form.get("apiKey") ?? "").trim()) body.apiKey = form.get("apiKey");
|
|
15518
15859
|
for (const [period, label] of [["daily", "每日"], ["monthly", "每月"]]) {
|
|
15519
15860
|
const value = body[`${period}TokenQuota`];
|
|
@@ -15526,7 +15867,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
|
|
|
15526
15867
|
await loadModels();
|
|
15527
15868
|
if (item) toast(connectivityConfigurationSavedToast("provider"));
|
|
15528
15869
|
},
|
|
15529
|
-
item ? "
|
|
15870
|
+
item ? "协议、超时、限流与凭据" : "供应商协议、超时、限流与凭据", {
|
|
15530
15871
|
dangerAction: item ? { label: "删除供应商", onClick: () => deletePlatformProvider(item) } : null
|
|
15531
15872
|
}
|
|
15532
15873
|
);
|
|
@@ -15679,10 +16020,25 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
15679
16020
|
if (aiRequestManager.hasActive(tab.id)) return;
|
|
15680
16021
|
const composerSnapshot = captureAiPromptComposer();
|
|
15681
16022
|
const requestComposerSnapshot = retry
|
|
15682
|
-
? {
|
|
16023
|
+
? {
|
|
16024
|
+
text: retry.prompt,
|
|
16025
|
+
citations: retry.citations ?? [],
|
|
16026
|
+
references: [],
|
|
16027
|
+
images: retry.images ?? [],
|
|
16028
|
+
sceneDirection: retry.sceneDirection ?? "",
|
|
16029
|
+
scenePin: captureAiScenePin()
|
|
16030
|
+
}
|
|
15683
16031
|
: composerSnapshot;
|
|
15684
16032
|
const instruction = requestComposerSnapshot.text.trim();
|
|
15685
|
-
|
|
16033
|
+
const sceneDirection = $("#ai-task").value === "roleplay"
|
|
16034
|
+
? String(requestComposerSnapshot.sceneDirection ?? "").trim()
|
|
16035
|
+
: "";
|
|
16036
|
+
const scenePin = $("#ai-task").value === "roleplay"
|
|
16037
|
+
? normalizeRoleplayScenePin(requestComposerSnapshot.scenePin)
|
|
16038
|
+
: emptyRoleplayScenePin();
|
|
16039
|
+
if (!instruction && !sceneDirection) {
|
|
16040
|
+
return toast($("#ai-task").value === "roleplay" ? "请输入台词或场景旁白" : "请输入指令", "error");
|
|
16041
|
+
}
|
|
15686
16042
|
if ($("#ai-task").value === "roleplay" && !state.aiRoleplayCharacter) return toast("请先选择角色卡", "error");
|
|
15687
16043
|
const requestScope = currentAiRequestScope();
|
|
15688
16044
|
if (!requestScope) return toast("请先选择章节", "error");
|
|
@@ -15779,6 +16135,8 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
15779
16135
|
if (taskType === "chat") {
|
|
15780
16136
|
const streamed = await streamChat(requestHolder, aiRetryStreamRequestBody({
|
|
15781
16137
|
instruction,
|
|
16138
|
+
...(sceneDirection ? { sceneDirection } : {}),
|
|
16139
|
+
...($("#ai-task").value === "roleplay" ? { scenePin } : {}),
|
|
15782
16140
|
scope,
|
|
15783
16141
|
modelId,
|
|
15784
16142
|
citations,
|
|
@@ -16269,9 +16627,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16269
16627
|
const isInterrupted = role === "assistant" && metadata?.interrupted === true;
|
|
16270
16628
|
const interruptionCode = typeof metadata?.interruptionCode === "string" ? metadata.interruptionCode : "AI_STREAM_FAILED";
|
|
16271
16629
|
message.className = `${role === "user" ? "user-message" : "assistant-message"}${isFailure || isInterrupted ? " is-error" : ""}`;
|
|
16630
|
+
const parsedUserTurn = role === "user" ? parseRoleplayUserTurn(text) : null;
|
|
16272
16631
|
const messageBody = isFailure
|
|
16273
16632
|
? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
|
|
16274
|
-
: renderMarkdown(text);
|
|
16633
|
+
: renderMarkdown(parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
|
|
16275
16634
|
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
16276
16635
|
message.querySelector("[data-ai-tool-call-settings-link]")?.addEventListener("click", (event) => {
|
|
16277
16636
|
event.preventDefault();
|
|
@@ -16293,6 +16652,18 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16293
16652
|
failureBadge.setAttribute("aria-label", `消息状态:${isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : "失败"}`);
|
|
16294
16653
|
heading.firstElementChild?.append(failureBadge);
|
|
16295
16654
|
}
|
|
16655
|
+
if (parsedUserTurn?.hasMarkup && parsedUserTurn.sceneDirection) {
|
|
16656
|
+
const scene = document.createElement("div");
|
|
16657
|
+
scene.className = "user-message-scene";
|
|
16658
|
+
const sceneLabel = document.createElement("span");
|
|
16659
|
+
sceneLabel.className = "user-message-scene-label";
|
|
16660
|
+
sceneLabel.textContent = "旁白 / 场景";
|
|
16661
|
+
const sceneBody = document.createElement("div");
|
|
16662
|
+
sceneBody.className = "user-message-scene-body";
|
|
16663
|
+
sceneBody.textContent = parsedUserTurn.sceneDirection;
|
|
16664
|
+
scene.append(sceneLabel, sceneBody);
|
|
16665
|
+
message.querySelector(".message-body")?.before(scene);
|
|
16666
|
+
}
|
|
16296
16667
|
const mentionGroups = role === "user"
|
|
16297
16668
|
? [
|
|
16298
16669
|
["角色", metadata?.mentionCharacterIds, state.characters],
|
|
@@ -16348,7 +16719,8 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16348
16719
|
);
|
|
16349
16720
|
message.dataset.aiImageAttachmentIds = JSON.stringify(imageAttachments.map((attachment) => attachment.id));
|
|
16350
16721
|
appendAiMessageImageAttachments(message, imageAttachments);
|
|
16351
|
-
attachUserCopyAction(message, text);
|
|
16722
|
+
attachUserCopyAction(message, parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
|
|
16723
|
+
if (parsedUserTurn?.hasMarkup) message.dataset.sceneDirection = parsedUserTurn.sceneDirection;
|
|
16352
16724
|
}
|
|
16353
16725
|
if (role === "assistant" && !text.startsWith("调用失败:")) {
|
|
16354
16726
|
const selectedModelId = tab?.modelId ?? tab?.selectedModelId ?? $("#ai-model").value;
|
|
@@ -17012,16 +17384,12 @@ $("#account-settings-button").addEventListener("click", () => {
|
|
|
17012
17384
|
$("#profile-display-name").value = state.user?.displayName ?? "";
|
|
17013
17385
|
renderProfileAvatar();
|
|
17014
17386
|
$("#password-form").reset();
|
|
17015
|
-
$("#api-key-result").classList.add("hidden");
|
|
17016
|
-
$("#api-key-value").value = "";
|
|
17017
17387
|
$("#account-dialog").showModal();
|
|
17018
17388
|
api("/api/auth/api-key").then((status) => {
|
|
17019
|
-
|
|
17020
|
-
? `已配置 ${status.prefix}…${status.lastUsedAt ? `,最近使用:${formatDateTime(status.lastUsedAt)}` : ",尚未使用"}`
|
|
17021
|
-
: "尚未生成 API Key。";
|
|
17022
|
-
$("#api-key-reset-button").textContent = status.configured ? "重置 API Key" : "生成 API Key";
|
|
17389
|
+
paintApiKeyStatus(status);
|
|
17023
17390
|
}).catch((error) => {
|
|
17024
17391
|
$("#api-key-status").textContent = error.message;
|
|
17392
|
+
$("#api-key-copy-button").hidden = true;
|
|
17025
17393
|
});
|
|
17026
17394
|
});
|
|
17027
17395
|
$("#account-dialog-close").addEventListener("click", () => $("#account-dialog").close());
|
|
@@ -17458,6 +17826,17 @@ function validatePasswordChangeConfirmation() {
|
|
|
17458
17826
|
}
|
|
17459
17827
|
$("#password-form input[name='newPassword']").addEventListener("input", validatePasswordChangeConfirmation);
|
|
17460
17828
|
$("#password-form input[name='passwordConfirmation']").addEventListener("input", validatePasswordChangeConfirmation);
|
|
17829
|
+
function paintApiKeyStatus(status) {
|
|
17830
|
+
const copyButton = $("#api-key-copy-button");
|
|
17831
|
+
copyButton.hidden = !status.configured;
|
|
17832
|
+
if (status.configured) {
|
|
17833
|
+
$("#api-key-status").textContent = `已配置 ${status.prefix}…${status.lastUsedAt ? `,最近使用:${formatDateTime(status.lastUsedAt)}` : ",尚未使用"}`;
|
|
17834
|
+
$("#api-key-reset-button").textContent = "重置 API Key";
|
|
17835
|
+
return;
|
|
17836
|
+
}
|
|
17837
|
+
$("#api-key-status").textContent = "尚未生成 API Key。";
|
|
17838
|
+
$("#api-key-reset-button").textContent = "生成 API Key";
|
|
17839
|
+
}
|
|
17461
17840
|
$("#api-key-reset-button").addEventListener("click", async () => {
|
|
17462
17841
|
if ($("#api-key-reset-button").textContent.includes("重置") && !(await confirmToast(
|
|
17463
17842
|
"重置后,所有使用旧 API Key 的 CLI 会立刻退出登录。确定继续吗?",
|
|
@@ -17465,25 +17844,31 @@ $("#api-key-reset-button").addEventListener("click", async () => {
|
|
|
17465
17844
|
))) return;
|
|
17466
17845
|
try {
|
|
17467
17846
|
const result = await api("/api/auth/api-key/reset", { method: "POST", body: {} });
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
toast("新的 API Key
|
|
17847
|
+
paintApiKeyStatus({
|
|
17848
|
+
configured: true,
|
|
17849
|
+
prefix: result.prefix,
|
|
17850
|
+
lastUsedAt: null,
|
|
17851
|
+
copyable: result.copyable !== false
|
|
17852
|
+
});
|
|
17853
|
+
toast("新的 API Key 已生成,可点击复制");
|
|
17475
17854
|
} catch (error) { toast(error.message, "error"); }
|
|
17476
17855
|
});
|
|
17477
17856
|
$("#api-key-copy-button").addEventListener("click", async () => {
|
|
17478
|
-
const
|
|
17479
|
-
if (
|
|
17857
|
+
const button = $("#api-key-copy-button");
|
|
17858
|
+
if (button.hidden || button.disabled) return;
|
|
17859
|
+
button.disabled = true;
|
|
17480
17860
|
try {
|
|
17481
|
-
await
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17861
|
+
const result = await api("/api/auth/api-key/reveal", { method: "POST", body: {} });
|
|
17862
|
+
try {
|
|
17863
|
+
await navigator.clipboard.writeText(result.apiKey);
|
|
17864
|
+
toast("API Key 已复制");
|
|
17865
|
+
} catch {
|
|
17866
|
+
toast("无法自动复制,请检查浏览器剪贴板权限", "error");
|
|
17867
|
+
}
|
|
17868
|
+
} catch (error) {
|
|
17869
|
+
toast(error.message, "error");
|
|
17870
|
+
} finally {
|
|
17871
|
+
button.disabled = false;
|
|
17487
17872
|
}
|
|
17488
17873
|
});
|
|
17489
17874
|
$("#logout-button").addEventListener("click", async () => {
|
|
@@ -18087,6 +18472,15 @@ $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftD
|
|
|
18087
18472
|
}
|
|
18088
18473
|
$("#ai-attachment-input").click();
|
|
18089
18474
|
});
|
|
18475
|
+
$("#ai-scene-button").addEventListener("click", () => {
|
|
18476
|
+
toggleAiScenePanel();
|
|
18477
|
+
});
|
|
18478
|
+
for (const field of ["#ai-scene-direction", "#ai-scene-location", "#ai-scene-present", "#ai-scene-time"]) {
|
|
18479
|
+
$(field)?.addEventListener("input", () => {
|
|
18480
|
+
syncAiSceneComposer();
|
|
18481
|
+
persistActiveAiChatTab();
|
|
18482
|
+
});
|
|
18483
|
+
}
|
|
18090
18484
|
$("#ai-attachment-input").addEventListener("change", (event) => {
|
|
18091
18485
|
const input = event.currentTarget;
|
|
18092
18486
|
void addAiImageFiles(input.files).finally(() => { input.value = ""; });
|