@musnows/scriverse 0.8.6 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/dist/ai-model-pricing.js +297 -62
- package/dist/ai-model-pricing.js.map +1 -1
- package/dist/ai.js +543 -133
- package/dist/ai.js.map +1 -1
- package/dist/app.js +77 -11
- package/dist/app.js.map +1 -1
- package/dist/database.js +151 -3
- package/dist/database.js.map +1 -1
- package/dist/public/ai-connectivity-test.js +5 -2
- package/dist/public/app.js +601 -74
- package/dist/public/index.html +23 -10
- package/dist/public/model-config.d.ts +3 -3
- package/dist/public/model-config.js +2 -1
- package/dist/public/roleplay-turn.js +100 -0
- package/dist/public/styles.css +109 -12
- 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 +33 -7
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +2 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +95 -12
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +36 -6
- 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
|
@@ -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";
|
|
@@ -16,8 +24,8 @@ import {
|
|
|
16
24
|
visibleForeshadowReminders
|
|
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
|
-
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, modelThinkingEffortLabel, supportsMultimodalModelProtocol } from "/model-config.js?v=
|
|
20
|
-
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=
|
|
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";
|
|
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,
|
|
@@ -461,6 +470,10 @@ function applyWorkAccessMode() {
|
|
|
461
470
|
$(".ai-panel").classList.toggle("permission-hidden", aiHidden);
|
|
462
471
|
$("#ai-prompt").readOnly = aiReadOnly;
|
|
463
472
|
$("#ai-prompt").setAttribute("aria-readonly", String(aiReadOnly));
|
|
473
|
+
$("#ai-scene-direction").readOnly = aiReadOnly;
|
|
474
|
+
$("#ai-scene-location").readOnly = aiReadOnly;
|
|
475
|
+
$("#ai-scene-present").readOnly = aiReadOnly;
|
|
476
|
+
$("#ai-scene-time").readOnly = aiReadOnly;
|
|
464
477
|
$("#ai-send").classList.toggle("permission-hidden", aiReadOnly);
|
|
465
478
|
renderAiRoleplayCharacterSelect();
|
|
466
479
|
updateBackgroundTaskCenterVisibility();
|
|
@@ -2082,7 +2095,7 @@ function createAiChatTabState(input = {}) {
|
|
|
2082
2095
|
roleplayUserCharacter: input.roleplayUserCharacter ?? null,
|
|
2083
2096
|
citations: input.citations ?? [],
|
|
2084
2097
|
references: input.references ?? [],
|
|
2085
|
-
composer: input.composer ?? { text: "", citations: [], references: [], images: [] },
|
|
2098
|
+
composer: input.composer ?? { text: "", citations: [], references: [], images: [], sceneDirection: "", scenePin: emptyRoleplayScenePin() },
|
|
2086
2099
|
contextUsage: input.contextUsage ?? null,
|
|
2087
2100
|
contextWarning: input.contextWarning === true,
|
|
2088
2101
|
lastMessageAt: input.lastMessageAt ?? null,
|
|
@@ -2124,12 +2137,21 @@ function setAiChatTabComposerSnapshot(tab, snapshot) {
|
|
|
2124
2137
|
text: snapshot.text,
|
|
2125
2138
|
citations: tab.citations.map((citation) => ({ ...citation })),
|
|
2126
2139
|
references: tab.references.map((reference) => ({ ...reference })),
|
|
2127
|
-
images: normalizeAiChatImageAttachments(snapshot.images)
|
|
2140
|
+
images: normalizeAiChatImageAttachments(snapshot.images),
|
|
2141
|
+
sceneDirection: String(snapshot.sceneDirection ?? ""),
|
|
2142
|
+
scenePin: normalizeRoleplayScenePin(snapshot.scenePin)
|
|
2128
2143
|
};
|
|
2129
2144
|
}
|
|
2130
2145
|
|
|
2131
2146
|
function clearAiChatTabComposer(tab) {
|
|
2132
|
-
setAiChatTabComposerSnapshot(tab, {
|
|
2147
|
+
setAiChatTabComposerSnapshot(tab, {
|
|
2148
|
+
text: "",
|
|
2149
|
+
citations: [],
|
|
2150
|
+
references: [],
|
|
2151
|
+
images: [],
|
|
2152
|
+
sceneDirection: "",
|
|
2153
|
+
scenePin: normalizeRoleplayScenePin(tab.composer?.scenePin)
|
|
2154
|
+
});
|
|
2133
2155
|
}
|
|
2134
2156
|
|
|
2135
2157
|
function applyAiChatTabState(tab) {
|
|
@@ -2149,6 +2171,7 @@ function applyAiChatTabState(tab) {
|
|
|
2149
2171
|
if (selectedModelId && state.models.some((model) => model.id === selectedModelId)) $("#ai-model").value = selectedModelId;
|
|
2150
2172
|
syncAiModelPicker();
|
|
2151
2173
|
setAiPromptText(tab.composer.text);
|
|
2174
|
+
restoreAiSceneComposer(tab.composer);
|
|
2152
2175
|
renderAiCitations();
|
|
2153
2176
|
renderAiReferences();
|
|
2154
2177
|
renderAiImageAttachments();
|
|
@@ -2170,7 +2193,9 @@ function aiChatTabIsReplaceable(tab = activeAiChatTab()) {
|
|
|
2170
2193
|
&& !(tab.composer?.text || "").trim()
|
|
2171
2194
|
&& !(tab.composer?.citations?.length)
|
|
2172
2195
|
&& !(tab.composer?.references?.length)
|
|
2173
|
-
&& !(tab.composer?.images?.length)
|
|
2196
|
+
&& !(tab.composer?.images?.length)
|
|
2197
|
+
&& !(tab.composer?.sceneDirection || "").trim()
|
|
2198
|
+
&& !roleplayScenePinHasContent(tab.composer?.scenePin));
|
|
2174
2199
|
}
|
|
2175
2200
|
|
|
2176
2201
|
function aiChatTabOpeningSlot() {
|
|
@@ -2443,7 +2468,7 @@ function resetAiFeed(
|
|
|
2443
2468
|
const roleplayName = roleplayCharacter?.name;
|
|
2444
2469
|
const roleplayUserName = roleplayUserCharacter?.name;
|
|
2445
2470
|
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)} 的身份与我互动。` : "
|
|
2471
|
+
? `<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
2472
|
: '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
2448
2473
|
}
|
|
2449
2474
|
|
|
@@ -2521,8 +2546,9 @@ async function retryAiMessage(message) {
|
|
|
2521
2546
|
const sourceTab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
|
|
2522
2547
|
const userMessage = findAiRetryUserMessage(message);
|
|
2523
2548
|
const prompt = userMessage?.dataset.copyText ?? "";
|
|
2549
|
+
const sceneDirection = userMessage?.dataset.sceneDirection ?? "";
|
|
2524
2550
|
const userMessageId = userMessage?.dataset.messageId ?? "";
|
|
2525
|
-
if (!sourceTab?.conversationId || !userMessageId || !prompt.trim()) {
|
|
2551
|
+
if (!sourceTab?.conversationId || !userMessageId || !(prompt.trim() || sceneDirection.trim())) {
|
|
2526
2552
|
toast("找不到需要重试的用户指令", "error");
|
|
2527
2553
|
return;
|
|
2528
2554
|
}
|
|
@@ -2535,6 +2561,7 @@ async function retryAiMessage(message) {
|
|
|
2535
2561
|
retry: {
|
|
2536
2562
|
message,
|
|
2537
2563
|
prompt,
|
|
2564
|
+
sceneDirection,
|
|
2538
2565
|
citations: aiMessageCitations(userMessage),
|
|
2539
2566
|
images: aiMessageImageAttachments(userMessage),
|
|
2540
2567
|
userMessageId
|
|
@@ -2670,6 +2697,8 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
2670
2697
|
recall_self: "回忆自身",
|
|
2671
2698
|
image: "读取设定图片",
|
|
2672
2699
|
recall_relationship: "回忆人物关系",
|
|
2700
|
+
recall_other: "回忆相识角色",
|
|
2701
|
+
recall_known: "回忆知情设定",
|
|
2673
2702
|
recall_story: "回忆故事",
|
|
2674
2703
|
calculate_time: "计算日期"
|
|
2675
2704
|
};
|
|
@@ -2683,9 +2712,11 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
2683
2712
|
search_drafts: "搜索可能采用、也可能永远不会进入正文或正式设定的未确认临时想法。",
|
|
2684
2713
|
recall_self: "读取当前扮演角色自己的角色卡、档案,以及自己参与的关系、时间线和正文记忆。",
|
|
2685
2714
|
image: "读取设定正文引用的图片附件,并返回多模态模型的理解内容。",
|
|
2686
|
-
recall_relationship: "
|
|
2687
|
-
|
|
2688
|
-
|
|
2715
|
+
recall_relationship: "不传角色列表时读取有关系的角色公开摘要;传入一个或多个角色后读取当前角色与这些角色之间的关系详情。",
|
|
2716
|
+
recall_other: "读取自己通过关系、同一组织或共同参与时间线而认识的其他角色公开摘要。",
|
|
2717
|
+
recall_known: "读取自己所属种族、组织,以及与自己身份相关的世界设定。",
|
|
2718
|
+
recall_story: "查询自己姓名或别名出现过的正文段落,避免全知回忆。",
|
|
2719
|
+
calculate_time: "计算两个 YYYY-MM-DD 日期之间的天数差。"
|
|
2689
2720
|
};
|
|
2690
2721
|
|
|
2691
2722
|
const aiFeedScrollFrames = new WeakMap();
|
|
@@ -3183,7 +3214,7 @@ function renderAiConversationHistory() {
|
|
|
3183
3214
|
}
|
|
3184
3215
|
|
|
3185
3216
|
function defaultAiConversationTitle(prompt) {
|
|
3186
|
-
const normalized = String(prompt ?? "").replace(/\s+/gu, " ").trim();
|
|
3217
|
+
const normalized = roleplayUserTurnTitleSource(String(prompt ?? "")).replace(/\s+/gu, " ").trim();
|
|
3187
3218
|
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
3188
3219
|
}
|
|
3189
3220
|
|
|
@@ -3191,7 +3222,7 @@ function upsertAiConversationSummary(conversation) {
|
|
|
3191
3222
|
if (!conversation?.id) return;
|
|
3192
3223
|
const current = state.aiConversations.find((item) => item.id === conversation.id);
|
|
3193
3224
|
const lastMessage = Array.isArray(conversation.messages) ? conversation.messages.at(-1) : null;
|
|
3194
|
-
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: lastMessage.content } : {}) };
|
|
3225
|
+
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: roleplayUserTurnDisplayText(lastMessage.content) } : {}) };
|
|
3195
3226
|
delete summary.messages;
|
|
3196
3227
|
delete summary.messagesPage;
|
|
3197
3228
|
if (!current && loadedAiConversationsWorkId === state.work?.id) {
|
|
@@ -3214,7 +3245,7 @@ function updateAiConversationSummaryFromMessage(message) {
|
|
|
3214
3245
|
...current,
|
|
3215
3246
|
title: current.title === "新对话" && message.role === "user" ? defaultAiConversationTitle(message.content) : current.title,
|
|
3216
3247
|
messageCount: Number(current.messageCount ?? 0) + 1,
|
|
3217
|
-
preview: message.content,
|
|
3248
|
+
preview: roleplayUserTurnDisplayText(message.content),
|
|
3218
3249
|
...(aiConversationMessageHasImages(message) ? { hasImageAttachments: true, modelLockedByImage: true } : {}),
|
|
3219
3250
|
updatedAt: message.createdAt ?? current.updatedAt
|
|
3220
3251
|
});
|
|
@@ -3349,7 +3380,14 @@ function applyConversationToAiChatTab(tab, conversation) {
|
|
|
3349
3380
|
tab.roleplayUserCharacter = conversation.roleplayUserCharacter ?? null;
|
|
3350
3381
|
tab.citations = [];
|
|
3351
3382
|
tab.references = [];
|
|
3352
|
-
tab.composer = {
|
|
3383
|
+
tab.composer = {
|
|
3384
|
+
text: "",
|
|
3385
|
+
citations: [],
|
|
3386
|
+
references: [],
|
|
3387
|
+
images: [],
|
|
3388
|
+
sceneDirection: "",
|
|
3389
|
+
scenePin: normalizeRoleplayScenePin(conversation.scenePin)
|
|
3390
|
+
};
|
|
3353
3391
|
tab.contextUsage = null;
|
|
3354
3392
|
tab.contextWarning = conversation.contextWarningPending === true;
|
|
3355
3393
|
resetAiFeed(tab.feed, tab.roleplayCharacter, tab.roleplayUserCharacter);
|
|
@@ -3400,16 +3438,30 @@ async function createNewAiConversation(taskType = "chat") {
|
|
|
3400
3438
|
}
|
|
3401
3439
|
}
|
|
3402
3440
|
|
|
3441
|
+
function favoriteCharactersFirst(characters) {
|
|
3442
|
+
const items = Array.isArray(characters) ? characters : [];
|
|
3443
|
+
return [
|
|
3444
|
+
...items.filter((character) => character.isFavorite === true),
|
|
3445
|
+
...items.filter((character) => character.isFavorite !== true)
|
|
3446
|
+
];
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
function roleplayCharacterOptionLabel(character) {
|
|
3450
|
+
const favoriteLabel = character?.isFavorite === true ? "[已收藏] " : "";
|
|
3451
|
+
const deathLabel = character?.isDead ? "(已死亡)" : "";
|
|
3452
|
+
return `${favoriteLabel}${String(character?.name ?? "")}${deathLabel}`;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3403
3455
|
function renderAiRoleplayCharacterSelect() {
|
|
3404
3456
|
const select = $("#ai-roleplay-character");
|
|
3405
3457
|
const selectedId = String(state.aiRoleplayCharacter?.id ?? "");
|
|
3406
|
-
const availableCharacters = state.characters.filter((character) => !character.mergedIntoCharacterId);
|
|
3458
|
+
const availableCharacters = favoriteCharactersFirst(state.characters.filter((character) => !character.mergedIntoCharacterId));
|
|
3407
3459
|
const options = [{ id: "", name: "选择角色卡" }, ...availableCharacters.map((character) => ({
|
|
3408
3460
|
id: String(character.id),
|
|
3409
|
-
name:
|
|
3461
|
+
name: roleplayCharacterOptionLabel(character)
|
|
3410
3462
|
}))];
|
|
3411
3463
|
if (selectedId && !options.some((option) => option.id === selectedId)) {
|
|
3412
|
-
options.push({ id: selectedId, name:
|
|
3464
|
+
options.push({ id: selectedId, name: roleplayCharacterOptionLabel(state.aiRoleplayCharacter) });
|
|
3413
3465
|
}
|
|
3414
3466
|
select.replaceChildren(...options.map((option) => {
|
|
3415
3467
|
const element = document.createElement("option");
|
|
@@ -3425,7 +3477,7 @@ function renderAiRoleplayCharacterSelect() {
|
|
|
3425
3477
|
select.title = canSelectCharacter
|
|
3426
3478
|
? state.aiPromptSent
|
|
3427
3479
|
? aiConversationOptionLockedMessage
|
|
3428
|
-
: "为当前对话选择角色卡;角色扮演时 Agent
|
|
3480
|
+
: "为当前对话选择角色卡;角色扮演时 Agent 可以查询角色记忆、相识角色、知情设定、故事正文和设定图片"
|
|
3429
3481
|
: "当前账户没有角色模块读取权限";
|
|
3430
3482
|
renderAiRoleplayUserCharacterSelect();
|
|
3431
3483
|
}
|
|
@@ -3434,15 +3486,15 @@ function renderAiRoleplayUserCharacterSelect() {
|
|
|
3434
3486
|
const select = $("#ai-roleplay-user-character");
|
|
3435
3487
|
const selectedId = String(state.aiRoleplayUserCharacter?.id ?? "");
|
|
3436
3488
|
const aiCharacterId = String(state.aiRoleplayCharacter?.id ?? "");
|
|
3437
|
-
const availableCharacters = state.characters.filter((character) => (
|
|
3489
|
+
const availableCharacters = favoriteCharactersFirst(state.characters.filter((character) => (
|
|
3438
3490
|
!character.mergedIntoCharacterId && String(character.id) !== aiCharacterId
|
|
3439
|
-
));
|
|
3491
|
+
)));
|
|
3440
3492
|
const options = [{ id: "", name: "选择我的角色(可选)" }, ...availableCharacters.map((character) => ({
|
|
3441
3493
|
id: String(character.id),
|
|
3442
|
-
name:
|
|
3494
|
+
name: roleplayCharacterOptionLabel(character)
|
|
3443
3495
|
}))];
|
|
3444
3496
|
if (selectedId && !options.some((option) => option.id === selectedId)) {
|
|
3445
|
-
options.push({ id: selectedId, name:
|
|
3497
|
+
options.push({ id: selectedId, name: roleplayCharacterOptionLabel(state.aiRoleplayUserCharacter) });
|
|
3446
3498
|
}
|
|
3447
3499
|
select.replaceChildren(...options.map((option) => {
|
|
3448
3500
|
const element = document.createElement("option");
|
|
@@ -3461,7 +3513,7 @@ function renderAiRoleplayUserCharacterSelect() {
|
|
|
3461
3513
|
? "请先选择 AI 扮演的角色"
|
|
3462
3514
|
: state.aiPromptSent
|
|
3463
3515
|
? aiConversationOptionLockedMessage
|
|
3464
|
-
: "
|
|
3516
|
+
: "选择后,主输入是该角色的台词或行动;旁白请写在场景框";
|
|
3465
3517
|
}
|
|
3466
3518
|
|
|
3467
3519
|
const aiConversationOptionLockedMessage = "会话选项在会话开始后不支持修改,若需要修改,请新建会话";
|
|
@@ -3600,8 +3652,9 @@ function syncAiTaskOptions() {
|
|
|
3600
3652
|
$("#ai-scope").disabled = interactionBusy || roleplaySelected;
|
|
3601
3653
|
$("#ai-scope").title = state.aiPromptSent
|
|
3602
3654
|
? aiConversationOptionLockedMessage
|
|
3603
|
-
: roleplaySelected ? "
|
|
3655
|
+
: roleplaySelected ? "角色扮演模式可以查询角色记忆、相识角色、知情设定、故事正文和设定图片" : "";
|
|
3604
3656
|
syncAiModelPicker();
|
|
3657
|
+
syncAiSceneComposer();
|
|
3605
3658
|
}
|
|
3606
3659
|
|
|
3607
3660
|
function applyAiConversationTaskType(taskType) {
|
|
@@ -3638,8 +3691,8 @@ function applyAiRoleplayCharacter(character, userCharacter = null) {
|
|
|
3638
3691
|
$(".ai-panel").classList.toggle("is-roleplaying", active);
|
|
3639
3692
|
$("#ai-prompt").dataset.placeholder = active
|
|
3640
3693
|
? state.aiRoleplayUserCharacter
|
|
3641
|
-
? `以 ${String(state.aiRoleplayUserCharacter.name)}
|
|
3642
|
-
:
|
|
3694
|
+
? `以 ${String(state.aiRoleplayUserCharacter.name)} 的身份输入台词或行动……`
|
|
3695
|
+
: `输入对 ${String(state.aiRoleplayCharacter.name)} 说的台词或行动……`
|
|
3643
3696
|
: "告诉 AI 你想讨论或修改什么……";
|
|
3644
3697
|
renderAiRoleplayCharacterSelect();
|
|
3645
3698
|
syncAiTaskOptions();
|
|
@@ -3946,9 +3999,11 @@ function clearAiPromptComposer() {
|
|
|
3946
3999
|
state.aiReferences = [];
|
|
3947
4000
|
state.aiImageAttachments = [];
|
|
3948
4001
|
setAiPromptText("");
|
|
4002
|
+
setAiSceneDirection("");
|
|
3949
4003
|
renderAiCitations();
|
|
3950
4004
|
renderAiImageAttachments();
|
|
3951
4005
|
hideAiMentionMenu();
|
|
4006
|
+
syncAiSceneComposer();
|
|
3952
4007
|
}
|
|
3953
4008
|
|
|
3954
4009
|
function captureAiPromptComposer() {
|
|
@@ -3956,7 +4011,9 @@ function captureAiPromptComposer() {
|
|
|
3956
4011
|
text: aiPromptText(),
|
|
3957
4012
|
citations: state.aiCitations.map((citation) => ({ ...citation })),
|
|
3958
4013
|
references: state.aiReferences.map((reference) => ({ ...reference })),
|
|
3959
|
-
images: normalizeAiChatImageAttachments(state.aiImageAttachments)
|
|
4014
|
+
images: normalizeAiChatImageAttachments(state.aiImageAttachments),
|
|
4015
|
+
sceneDirection: aiSceneDirectionText(),
|
|
4016
|
+
scenePin: captureAiScenePin()
|
|
3960
4017
|
};
|
|
3961
4018
|
}
|
|
3962
4019
|
|
|
@@ -3965,11 +4022,77 @@ function restoreAiPromptComposer(snapshot) {
|
|
|
3965
4022
|
state.aiReferences = snapshot.references.map((reference) => ({ ...reference }));
|
|
3966
4023
|
state.aiImageAttachments = normalizeAiChatImageAttachments(snapshot.images);
|
|
3967
4024
|
setAiPromptText(snapshot.text);
|
|
4025
|
+
restoreAiSceneComposer(snapshot);
|
|
3968
4026
|
renderAiCitations();
|
|
3969
4027
|
renderAiImageAttachments();
|
|
3970
4028
|
hideAiMentionMenu();
|
|
3971
4029
|
}
|
|
3972
4030
|
|
|
4031
|
+
function aiSceneDirectionText() {
|
|
4032
|
+
return String($("#ai-scene-direction")?.value ?? "");
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function setAiSceneDirection(value) {
|
|
4036
|
+
const input = $("#ai-scene-direction");
|
|
4037
|
+
if (input) input.value = String(value ?? "");
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
function captureAiScenePin() {
|
|
4041
|
+
return normalizeRoleplayScenePin({
|
|
4042
|
+
location: $("#ai-scene-location")?.value ?? "",
|
|
4043
|
+
present: $("#ai-scene-present")?.value ?? "",
|
|
4044
|
+
timeLabel: $("#ai-scene-time")?.value ?? ""
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
function setAiScenePin(pin) {
|
|
4049
|
+
const normalized = normalizeRoleplayScenePin(pin);
|
|
4050
|
+
const location = $("#ai-scene-location");
|
|
4051
|
+
const present = $("#ai-scene-present");
|
|
4052
|
+
const timeLabel = $("#ai-scene-time");
|
|
4053
|
+
if (location) location.value = normalized.location;
|
|
4054
|
+
if (present) present.value = normalized.present;
|
|
4055
|
+
if (timeLabel) timeLabel.value = normalized.timeLabel;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
function restoreAiSceneComposer(snapshot = {}) {
|
|
4059
|
+
setAiSceneDirection(snapshot.sceneDirection ?? "");
|
|
4060
|
+
setAiScenePin(snapshot.scenePin);
|
|
4061
|
+
syncAiSceneComposer();
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
function roleplaySceneComposerVisible() {
|
|
4065
|
+
return $("#ai-task").value === "roleplay" && Boolean(state.aiRoleplayCharacter);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
function syncAiSceneComposer() {
|
|
4069
|
+
const button = $("#ai-scene-button");
|
|
4070
|
+
const panel = $("#ai-scene-panel");
|
|
4071
|
+
if (!button || !panel) return;
|
|
4072
|
+
const visible = roleplaySceneComposerVisible();
|
|
4073
|
+
const busy = aiInteractionBusy();
|
|
4074
|
+
const readOnly = Boolean(state.work) && !canWritePermissionModule(state.work, "ai-chat");
|
|
4075
|
+
button.classList.toggle("hidden", !visible);
|
|
4076
|
+
button.disabled = !visible || busy || readOnly;
|
|
4077
|
+
button.setAttribute("aria-hidden", String(!visible));
|
|
4078
|
+
if (!visible) {
|
|
4079
|
+
panel.classList.add("hidden");
|
|
4080
|
+
button.setAttribute("aria-expanded", "false");
|
|
4081
|
+
}
|
|
4082
|
+
const hasContent = Boolean(aiSceneDirectionText().trim()) || roleplayScenePinHasContent(captureAiScenePin());
|
|
4083
|
+
button.classList.toggle("is-active", hasContent);
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
function toggleAiScenePanel() {
|
|
4087
|
+
const button = $("#ai-scene-button");
|
|
4088
|
+
const panel = $("#ai-scene-panel");
|
|
4089
|
+
if (!button || !panel || button.classList.contains("hidden")) return;
|
|
4090
|
+
const willOpen = panel.classList.contains("hidden");
|
|
4091
|
+
panel.classList.toggle("hidden", !willOpen);
|
|
4092
|
+
button.setAttribute("aria-expanded", String(willOpen));
|
|
4093
|
+
if (willOpen) $("#ai-scene-direction")?.focus();
|
|
4094
|
+
}
|
|
4095
|
+
|
|
3973
4096
|
function aiPromptTextBeforeCursor() {
|
|
3974
4097
|
const prompt = $("#ai-prompt");
|
|
3975
4098
|
const selection = window.getSelection();
|
|
@@ -5109,8 +5232,18 @@ async function initializeAuthentication() {
|
|
|
5109
5232
|
return true;
|
|
5110
5233
|
}
|
|
5111
5234
|
|
|
5235
|
+
function syncToastRegionHost() {
|
|
5236
|
+
const region = $("#toast-region");
|
|
5237
|
+
const host = resolveToastRegionHost([...document.querySelectorAll("dialog[open]")], document.body);
|
|
5238
|
+
if (!host || region.parentElement === host) return false;
|
|
5239
|
+
if (typeof region.hidePopover === "function" && region.matches(":popover-open")) region.hidePopover();
|
|
5240
|
+
host.append(region);
|
|
5241
|
+
return true;
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5112
5244
|
function raiseToastRegion() {
|
|
5113
5245
|
const region = $("#toast-region");
|
|
5246
|
+
syncToastRegionHost();
|
|
5114
5247
|
if (typeof region.showPopover !== "function") return;
|
|
5115
5248
|
if (region.matches(":popover-open")) region.hidePopover();
|
|
5116
5249
|
region.showPopover();
|
|
@@ -5263,9 +5396,6 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
5263
5396
|
confirm.textContent = confirmLabel;
|
|
5264
5397
|
actions.append(cancel, confirm);
|
|
5265
5398
|
element.append(heading, description, actions);
|
|
5266
|
-
region.append(element);
|
|
5267
|
-
raiseToastRegion();
|
|
5268
|
-
cancel.focus();
|
|
5269
5399
|
return new Promise((resolve) => {
|
|
5270
5400
|
const finish = (confirmed) => {
|
|
5271
5401
|
element.remove();
|
|
@@ -5280,6 +5410,9 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
5280
5410
|
event.preventDefault();
|
|
5281
5411
|
finish(false);
|
|
5282
5412
|
});
|
|
5413
|
+
region.append(element);
|
|
5414
|
+
raiseToastRegion();
|
|
5415
|
+
cancel.focus();
|
|
5283
5416
|
});
|
|
5284
5417
|
}
|
|
5285
5418
|
|
|
@@ -5340,8 +5473,15 @@ function inputToast(message, { title = "请输入", inputLabel = title, value =
|
|
|
5340
5473
|
|
|
5341
5474
|
document.addEventListener("toggle", (event) => {
|
|
5342
5475
|
const target = event.target;
|
|
5343
|
-
if (target instanceof HTMLDialogElement
|
|
5476
|
+
if (!(target instanceof HTMLDialogElement)) return;
|
|
5477
|
+
const region = $("#toast-region");
|
|
5478
|
+
if (target.open && region.childElementCount) {
|
|
5344
5479
|
raiseToastRegion();
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
if (!target.open && (region.parentElement !== document.body || region.childElementCount)) {
|
|
5483
|
+
const moved = syncToastRegionHost();
|
|
5484
|
+
if (moved && region.childElementCount && typeof region.showPopover === "function") region.showPopover();
|
|
5345
5485
|
}
|
|
5346
5486
|
}, true);
|
|
5347
5487
|
|
|
@@ -6986,6 +7126,7 @@ async function showSettingsHub() {
|
|
|
6986
7126
|
state.dirty = false;
|
|
6987
7127
|
}
|
|
6988
7128
|
dismissChapterInsightToast();
|
|
7129
|
+
dismissTokenUsageDetails({ restoreFocus: false });
|
|
6989
7130
|
dismissDeleteToasts();
|
|
6990
7131
|
updateDocumentTitle(state.work);
|
|
6991
7132
|
$("#app").classList.add("shelf-mode");
|
|
@@ -7034,6 +7175,7 @@ async function showPlatformAi() {
|
|
|
7034
7175
|
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
|
|
7035
7176
|
state.dirty = false;
|
|
7036
7177
|
dismissChapterInsightToast();
|
|
7178
|
+
dismissTokenUsageDetails({ restoreFocus: false });
|
|
7037
7179
|
dismissDeleteToasts();
|
|
7038
7180
|
updateDocumentTitle();
|
|
7039
7181
|
$("#app").classList.add("shelf-mode");
|
|
@@ -7057,6 +7199,7 @@ async function showPlatformUsage() {
|
|
|
7057
7199
|
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入 Token 用量面板将放弃本地修改。是否继续?"))) return false;
|
|
7058
7200
|
state.dirty = false;
|
|
7059
7201
|
dismissChapterInsightToast();
|
|
7202
|
+
dismissTokenUsageDetails({ restoreFocus: false });
|
|
7060
7203
|
dismissDeleteToasts();
|
|
7061
7204
|
updateDocumentTitle();
|
|
7062
7205
|
$("#app").classList.add("shelf-mode");
|
|
@@ -8530,6 +8673,116 @@ function pencilIconMarkup() {
|
|
|
8530
8673
|
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>';
|
|
8531
8674
|
}
|
|
8532
8675
|
|
|
8676
|
+
function characterFavoriteIconMarkup() {
|
|
8677
|
+
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>';
|
|
8678
|
+
}
|
|
8679
|
+
|
|
8680
|
+
function characterFavoriteButton(item) {
|
|
8681
|
+
const isFavorite = item.isFavorite === true;
|
|
8682
|
+
const canFavorite = canEditModule("characters");
|
|
8683
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8684
|
+
const title = canFavorite ? action : "当前账户没有角色模块写入权限";
|
|
8685
|
+
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>`;
|
|
8686
|
+
}
|
|
8687
|
+
|
|
8688
|
+
const recordFavoriteConfigs = Object.freeze({
|
|
8689
|
+
character: { module: "characters", resource: "characters", label: "角色", nameField: "name" },
|
|
8690
|
+
draft: { module: "drafts", resource: "drafts", label: "想法", nameField: "title" },
|
|
8691
|
+
setting: { module: "settings", resource: "settings", label: "设定", nameField: "title" },
|
|
8692
|
+
organization: { module: "organizations", resource: "organizations", label: "组织", nameField: "name" }
|
|
8693
|
+
});
|
|
8694
|
+
|
|
8695
|
+
function recordFavoriteButton(type, item, { cardControl = false } = {}) {
|
|
8696
|
+
const config = recordFavoriteConfigs[type];
|
|
8697
|
+
if (!config) return "";
|
|
8698
|
+
const isFavorite = item.isFavorite === true;
|
|
8699
|
+
const canFavorite = canEditModule(config.module);
|
|
8700
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8701
|
+
const title = canFavorite ? action : `当前账户没有${config.label}模块写入权限`;
|
|
8702
|
+
const name = String(item[config.nameField] ?? "");
|
|
8703
|
+
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>`;
|
|
8704
|
+
}
|
|
8705
|
+
|
|
8706
|
+
async function toggleRecordFavorite(type, item, render) {
|
|
8707
|
+
const config = recordFavoriteConfigs[type];
|
|
8708
|
+
if (!config) return;
|
|
8709
|
+
const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/favorite`, {
|
|
8710
|
+
method: "PATCH",
|
|
8711
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
8712
|
+
});
|
|
8713
|
+
await render();
|
|
8714
|
+
const favoriteButton = [...$("#module-content").querySelectorAll("[data-record-favorite]")].find((button) => (
|
|
8715
|
+
button.dataset.recordFavoriteType === type && button.dataset.recordFavorite === String(updated.id)
|
|
8716
|
+
));
|
|
8717
|
+
favoriteButton?.focus({ preventScroll: true });
|
|
8718
|
+
const name = String(updated[config.nameField] ?? "");
|
|
8719
|
+
toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
|
|
8720
|
+
}
|
|
8721
|
+
|
|
8722
|
+
function bindRecordFavoriteButtons(type, items, render) {
|
|
8723
|
+
const config = recordFavoriteConfigs[type];
|
|
8724
|
+
if (!config) return;
|
|
8725
|
+
$("#module-content").querySelectorAll(`[data-record-favorite-type="${type}"]`).forEach((button) => button.addEventListener("click", async () => {
|
|
8726
|
+
const item = items.find((candidate) => candidate.id === button.dataset.recordFavorite);
|
|
8727
|
+
if (!item || !canEditModule(config.module)) return;
|
|
8728
|
+
button.disabled = true;
|
|
8729
|
+
try {
|
|
8730
|
+
await toggleRecordFavorite(type, item, render);
|
|
8731
|
+
} catch (error) {
|
|
8732
|
+
button.disabled = false;
|
|
8733
|
+
toast(`${config.label}收藏状态更新失败:${error.message}`, "error");
|
|
8734
|
+
}
|
|
8735
|
+
}));
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8738
|
+
function syncEntityDetailFavoriteButton(button, type, item) {
|
|
8739
|
+
const config = recordFavoriteConfigs[type];
|
|
8740
|
+
const visible = Boolean(config && item);
|
|
8741
|
+
button.classList.toggle("hidden", !visible);
|
|
8742
|
+
button.innerHTML = characterFavoriteIconMarkup();
|
|
8743
|
+
if (!visible) {
|
|
8744
|
+
button.disabled = true;
|
|
8745
|
+
button.classList.remove("is-favorite");
|
|
8746
|
+
button.setAttribute("aria-pressed", "false");
|
|
8747
|
+
button.removeAttribute("aria-label");
|
|
8748
|
+
button.removeAttribute("title");
|
|
8749
|
+
return;
|
|
8750
|
+
}
|
|
8751
|
+
const isFavorite = item.isFavorite === true;
|
|
8752
|
+
const canFavorite = canEditModule(config.module);
|
|
8753
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8754
|
+
const name = String(item[config.nameField] ?? "");
|
|
8755
|
+
button.classList.toggle("is-favorite", isFavorite);
|
|
8756
|
+
button.setAttribute("aria-pressed", String(isFavorite));
|
|
8757
|
+
button.setAttribute("aria-label", `${action}${config.label}“${name}”`);
|
|
8758
|
+
button.title = canFavorite ? action : `当前账户没有${config.label}模块写入权限`;
|
|
8759
|
+
button.disabled = !canFavorite;
|
|
8760
|
+
}
|
|
8761
|
+
|
|
8762
|
+
function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
|
|
8763
|
+
syncEntityDetailFavoriteButton(button, type, getItem());
|
|
8764
|
+
button.onclick = async () => {
|
|
8765
|
+
const config = recordFavoriteConfigs[type];
|
|
8766
|
+
const item = getItem();
|
|
8767
|
+
if (!config || !item || !canEditModule(config.module)) return;
|
|
8768
|
+
button.disabled = true;
|
|
8769
|
+
try {
|
|
8770
|
+
const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/favorite`, {
|
|
8771
|
+
method: "PATCH",
|
|
8772
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
8773
|
+
});
|
|
8774
|
+
onUpdated(updated);
|
|
8775
|
+
syncEntityDetailFavoriteButton(button, type, getItem() ?? updated);
|
|
8776
|
+
button.focus({ preventScroll: true });
|
|
8777
|
+
const name = String(updated[config.nameField] ?? "");
|
|
8778
|
+
toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
|
|
8779
|
+
} catch (error) {
|
|
8780
|
+
syncEntityDetailFavoriteButton(button, type, getItem());
|
|
8781
|
+
toast(`${config.label}收藏状态更新失败:${error instanceof Error ? error.message : "未知错误"}`, "error");
|
|
8782
|
+
}
|
|
8783
|
+
};
|
|
8784
|
+
}
|
|
8785
|
+
|
|
8533
8786
|
function recordCardEditButton(attribute, id, label) {
|
|
8534
8787
|
return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
|
|
8535
8788
|
}
|
|
@@ -8865,9 +9118,10 @@ function openReviewDetailDialog(item) {
|
|
|
8865
9118
|
}
|
|
8866
9119
|
|
|
8867
9120
|
function settingRecordActions(item) {
|
|
8868
|
-
|
|
9121
|
+
const recordAction = canEditModule("settings")
|
|
8869
9122
|
? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
|
|
8870
9123
|
: recordHistoryButton("setting", item.id, item.title);
|
|
9124
|
+
return `${recordFavoriteButton("setting", item)}${recordAction}`;
|
|
8871
9125
|
}
|
|
8872
9126
|
|
|
8873
9127
|
function draftTypeLabel(draftType) {
|
|
@@ -8921,6 +9175,7 @@ async function deleteDraft(item) {
|
|
|
8921
9175
|
}
|
|
8922
9176
|
|
|
8923
9177
|
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
9178
|
+
let draftDialogItem = item;
|
|
8924
9179
|
const viewOnly = readOnly || !canEditModule("drafts");
|
|
8925
9180
|
const volumeOptions = [["", "全局(不绑定分卷)"], ...(state.work?.volumes ?? []).map((volume) => [volume.id, volume.title])];
|
|
8926
9181
|
const settingModuleOptions = [["", "全局(不绑定设定模块)"], ...draftSettingModules];
|
|
@@ -8978,6 +9233,18 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
8978
9233
|
else control.readOnly = true;
|
|
8979
9234
|
});
|
|
8980
9235
|
}
|
|
9236
|
+
if (item && viewOnly) {
|
|
9237
|
+
const headerActions = $("#dialog-context-actions");
|
|
9238
|
+
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>' : ""}`;
|
|
9239
|
+
bindEntityDetailFavoriteButton($("#draft-dialog-favorite"), "draft", () => draftDialogItem, (updated) => {
|
|
9240
|
+
draftDialogItem = updated;
|
|
9241
|
+
void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
|
|
9242
|
+
});
|
|
9243
|
+
$("#draft-dialog-edit")?.addEventListener("click", () => {
|
|
9244
|
+
$("#form-dialog").close();
|
|
9245
|
+
openDraftDialog(draftDialogItem);
|
|
9246
|
+
});
|
|
9247
|
+
}
|
|
8981
9248
|
}
|
|
8982
9249
|
|
|
8983
9250
|
async function renderDrafts(page = moduleListPages.drafts) {
|
|
@@ -9011,9 +9278,9 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
9011
9278
|
<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>
|
|
9012
9279
|
<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>
|
|
9013
9280
|
</section>`;
|
|
9014
|
-
const actions = (item) => canEditModule("drafts")
|
|
9281
|
+
const actions = (item) => `${recordFavoriteButton("draft", item)}${canEditModule("drafts")
|
|
9015
9282
|
? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
|
|
9016
|
-
: recordHistoryButton("draft", item.id, item.title)
|
|
9283
|
+
: recordHistoryButton("draft", item.id, item.title)}`;
|
|
9017
9284
|
const cards = `<div class="card-grid">${pageResult.items.map((item) => `
|
|
9018
9285
|
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看想法 ${esc(item.title)}">
|
|
9019
9286
|
<small>${esc(draftTypeLabel(item.draftType))} · ${esc(draftBindingLabel(item))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
@@ -9069,6 +9336,10 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
9069
9336
|
$("#module-content").querySelectorAll("[data-edit-draft]").forEach((button) => button.addEventListener("click", async () => {
|
|
9070
9337
|
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(button.dataset.editDraft)}`));
|
|
9071
9338
|
}));
|
|
9339
|
+
bindRecordFavoriteButtons("draft", pageResult.items, async () => {
|
|
9340
|
+
moduleListPages.drafts = 1;
|
|
9341
|
+
await renderDrafts(1);
|
|
9342
|
+
});
|
|
9072
9343
|
bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
|
|
9073
9344
|
}
|
|
9074
9345
|
|
|
@@ -9132,6 +9403,10 @@ async function renderSettings(page = moduleListPages.settings) {
|
|
|
9132
9403
|
bindModulePagination("settings", renderSettingResults);
|
|
9133
9404
|
const openSetting = async (id, readOnly) => openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly });
|
|
9134
9405
|
$("#setting-filter-results").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => { void openSetting(button.dataset.editSetting, false); }));
|
|
9406
|
+
bindRecordFavoriteButtons("setting", pageResult.items, async () => {
|
|
9407
|
+
moduleListPages.settings = 1;
|
|
9408
|
+
await renderSettings(1);
|
|
9409
|
+
});
|
|
9135
9410
|
bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
|
|
9136
9411
|
};
|
|
9137
9412
|
|
|
@@ -9168,6 +9443,21 @@ async function renderSettings(page = moduleListPages.settings) {
|
|
|
9168
9443
|
renderSettingResults(page);
|
|
9169
9444
|
}
|
|
9170
9445
|
|
|
9446
|
+
async function toggleCharacterFavorite(item) {
|
|
9447
|
+
const updated = await api(`/api/characters/${encodeURIComponent(item.id)}/favorite`, {
|
|
9448
|
+
method: "PATCH",
|
|
9449
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
9450
|
+
});
|
|
9451
|
+
if (loadedAiReferencesWorkId === state.work?.id) {
|
|
9452
|
+
state.characters = upsertEntityCollection(state.characters, updated);
|
|
9453
|
+
renderAiRoleplayCharacterSelect();
|
|
9454
|
+
}
|
|
9455
|
+
characterListPage = 1;
|
|
9456
|
+
await renderCharacters(1);
|
|
9457
|
+
$("#module-content").querySelector(`[data-character-favorite="${CSS.escape(String(updated.id))}"]`)?.focus({ preventScroll: true });
|
|
9458
|
+
toast(updated.isFavorite ? `已收藏角色“${updated.name}”` : `已取消收藏角色“${updated.name}”`);
|
|
9459
|
+
}
|
|
9460
|
+
|
|
9171
9461
|
async function renderCharacters(page = characterListPage) {
|
|
9172
9462
|
const hasCharacterFilters = characterFilters.raceIds.length > 0
|
|
9173
9463
|
|| characterFilters.organizationIds.length > 0
|
|
@@ -9190,14 +9480,14 @@ async function renderCharacters(page = characterListPage) {
|
|
|
9190
9480
|
[state.races, state.organizations] = [races, organizations];
|
|
9191
9481
|
mountModuleCount(characterPage.total);
|
|
9192
9482
|
const layout = readModuleLayout();
|
|
9193
|
-
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)
|
|
9483
|
+
const characterActions = (item) => `${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
|
|
9194
9484
|
const characterLockBadge = (item) => item.lockedFields.length
|
|
9195
9485
|
? `<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>`
|
|
9196
9486
|
: "";
|
|
9197
9487
|
const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
|
|
9198
9488
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
9199
9489
|
return `
|
|
9200
|
-
<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}”`)}
|
|
9490
|
+
<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}”`)}
|
|
9201
9491
|
<div class="character-card-heading">${characterAvatarHtml(item)}<h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
|
|
9202
9492
|
${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
|
|
9203
9493
|
<div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
|
|
@@ -9263,6 +9553,17 @@ async function renderCharacters(page = characterListPage) {
|
|
|
9263
9553
|
? `${layout === "rows" ? characterRows() : characterCards()}${pagination}`
|
|
9264
9554
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
9265
9555
|
bindModuleLayoutToggle(renderCharacters);
|
|
9556
|
+
$("#module-content").querySelectorAll("[data-character-favorite]").forEach((button) => button.addEventListener("click", async () => {
|
|
9557
|
+
const item = pageCharacters.find((candidate) => candidate.id === button.dataset.characterFavorite);
|
|
9558
|
+
if (!item || !canEditModule("characters")) return;
|
|
9559
|
+
button.disabled = true;
|
|
9560
|
+
try {
|
|
9561
|
+
await toggleCharacterFavorite(item);
|
|
9562
|
+
} catch (error) {
|
|
9563
|
+
button.disabled = false;
|
|
9564
|
+
toast(`角色收藏状态更新失败:${error.message}`, "error");
|
|
9565
|
+
}
|
|
9566
|
+
}));
|
|
9266
9567
|
const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
9267
9568
|
$("#character-race-filter").addEventListener("change", async () => {
|
|
9268
9569
|
characterFiltersPanelOpen = true;
|
|
@@ -9417,14 +9718,14 @@ async function renderOrganizations(page = moduleListPages.organizations) {
|
|
|
9417
9718
|
moduleListPages.organizations = pageResult.page;
|
|
9418
9719
|
const layout = readModuleLayout();
|
|
9419
9720
|
const canEditOrganizations = canEditModule("organizations");
|
|
9420
|
-
const organizationActions = (item) => canEditOrganizations
|
|
9721
|
+
const organizationActions = (item, { cardControl = false } = {}) => `${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
|
|
9421
9722
|
? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
|
|
9422
|
-
: recordHistoryButton("organization", item.id, item.name)
|
|
9723
|
+
: recordHistoryButton("organization", item.id, item.name)}`;
|
|
9423
9724
|
const organizationCardActions = (item) => canEditOrganizations
|
|
9424
|
-
? organizationActions(item)
|
|
9725
|
+
? organizationActions(item, { cardControl: true })
|
|
9425
9726
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
9426
9727
|
const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
|
|
9427
|
-
<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>
|
|
9728
|
+
<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>
|
|
9428
9729
|
<h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
9429
9730
|
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
9430
9731
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
@@ -9449,6 +9750,10 @@ async function renderOrganizations(page = moduleListPages.organizations) {
|
|
|
9449
9750
|
bindModulePagination("organizations", renderOrganizations);
|
|
9450
9751
|
const openOrganization = async (id, readOnly) => openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly });
|
|
9451
9752
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => { void openOrganization(button.dataset.editOrganization, false); }));
|
|
9753
|
+
bindRecordFavoriteButtons("organization", pageResult.items, async () => {
|
|
9754
|
+
moduleListPages.organizations = 1;
|
|
9755
|
+
await renderOrganizations(1);
|
|
9756
|
+
});
|
|
9452
9757
|
bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
|
|
9453
9758
|
}
|
|
9454
9759
|
|
|
@@ -11705,6 +12010,138 @@ function scrollUsageCalendarsToLatest(root) {
|
|
|
11705
12010
|
});
|
|
11706
12011
|
}
|
|
11707
12012
|
|
|
12013
|
+
let tokenUsageDetailsTrigger = null;
|
|
12014
|
+
|
|
12015
|
+
function tokenUsageDetailCount(value) {
|
|
12016
|
+
return Math.max(0, Math.round(Number(value) || 0)).toLocaleString("zh-CN");
|
|
12017
|
+
}
|
|
12018
|
+
|
|
12019
|
+
function dismissTokenUsageDetails({ restoreFocus = true } = {}) {
|
|
12020
|
+
const region = $("#toast-region");
|
|
12021
|
+
const element = region.querySelector("#token-usage-details-toast");
|
|
12022
|
+
if (!element) return;
|
|
12023
|
+
element.remove();
|
|
12024
|
+
const trigger = tokenUsageDetailsTrigger;
|
|
12025
|
+
tokenUsageDetailsTrigger = null;
|
|
12026
|
+
region.classList.remove("token-usage-details-region");
|
|
12027
|
+
if (trigger) {
|
|
12028
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
12029
|
+
if (restoreFocus && trigger.isConnected) trigger.focus({ preventScroll: true });
|
|
12030
|
+
}
|
|
12031
|
+
if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
|
|
12032
|
+
region.hidePopover();
|
|
12033
|
+
}
|
|
12034
|
+
}
|
|
12035
|
+
|
|
12036
|
+
function appendTokenUsageDetailRow(parent, label, usage, isSummary = false) {
|
|
12037
|
+
const row = document.createElement("tr");
|
|
12038
|
+
if (isSummary) row.className = "is-summary";
|
|
12039
|
+
const model = document.createElement("th");
|
|
12040
|
+
model.scope = "row";
|
|
12041
|
+
model.textContent = label;
|
|
12042
|
+
row.append(model);
|
|
12043
|
+
["directInputTokens", "cacheWriteInputTokens", "cacheReadInputTokens", "outputTokens", "totalTokens", "requestCount", "estimatedRequestCount"].forEach((key) => {
|
|
12044
|
+
const cell = document.createElement("td");
|
|
12045
|
+
cell.textContent = tokenUsageDetailCount(usage?.[key]);
|
|
12046
|
+
row.append(cell);
|
|
12047
|
+
});
|
|
12048
|
+
const price = document.createElement("td");
|
|
12049
|
+
price.textContent = formatEstimatedCost(usage?.estimatedCost);
|
|
12050
|
+
row.append(price);
|
|
12051
|
+
parent.append(row);
|
|
12052
|
+
}
|
|
12053
|
+
|
|
12054
|
+
function showTokenUsageDetails(usage, title, trigger) {
|
|
12055
|
+
dismissTokenUsageDetails({ restoreFocus: false });
|
|
12056
|
+
const region = $("#toast-region");
|
|
12057
|
+
const element = document.createElement("section");
|
|
12058
|
+
element.id = "token-usage-details-toast";
|
|
12059
|
+
element.className = "toast token-usage-details-toast";
|
|
12060
|
+
element.setAttribute("role", "dialog");
|
|
12061
|
+
element.setAttribute("aria-labelledby", "token-usage-details-title");
|
|
12062
|
+
element.setAttribute("aria-describedby", "token-usage-details-description");
|
|
12063
|
+
|
|
12064
|
+
const header = document.createElement("header");
|
|
12065
|
+
header.className = "token-usage-details-header";
|
|
12066
|
+
const heading = document.createElement("div");
|
|
12067
|
+
heading.className = "token-usage-details-heading";
|
|
12068
|
+
const headingTitle = document.createElement("strong");
|
|
12069
|
+
headingTitle.id = "token-usage-details-title";
|
|
12070
|
+
headingTitle.textContent = "Token 用量详细数据";
|
|
12071
|
+
const headingScope = document.createElement("span");
|
|
12072
|
+
headingScope.textContent = title;
|
|
12073
|
+
heading.append(headingTitle, headingScope);
|
|
12074
|
+
const close = document.createElement("button");
|
|
12075
|
+
close.className = "ghost-button token-usage-details-close";
|
|
12076
|
+
close.type = "button";
|
|
12077
|
+
close.textContent = "关闭";
|
|
12078
|
+
close.setAttribute("aria-label", "关闭 Token 用量详细数据");
|
|
12079
|
+
header.append(heading, close);
|
|
12080
|
+
|
|
12081
|
+
const body = document.createElement("div");
|
|
12082
|
+
body.className = "token-usage-details-body";
|
|
12083
|
+
const description = document.createElement("p");
|
|
12084
|
+
description.id = "token-usage-details-description";
|
|
12085
|
+
description.className = "token-usage-details-note";
|
|
12086
|
+
description.textContent = "Raw Input 为不含缓存的输入 Token;Cache Write 与 Cache Read 也是输入组成部分,并已包含在总计中。估算价格按美元显示,未匹配价格的模型显示为暂无价格。汇总行包含当前范围内全部模型。";
|
|
12087
|
+
body.append(description);
|
|
12088
|
+
|
|
12089
|
+
const tableScroll = document.createElement("div");
|
|
12090
|
+
tableScroll.className = "token-usage-details-table-scroll";
|
|
12091
|
+
const table = document.createElement("table");
|
|
12092
|
+
table.className = "token-usage-details-table";
|
|
12093
|
+
const caption = document.createElement("caption");
|
|
12094
|
+
caption.textContent = `${title}的模型 Token 用量`;
|
|
12095
|
+
table.append(caption);
|
|
12096
|
+
const head = document.createElement("thead");
|
|
12097
|
+
const headRow = document.createElement("tr");
|
|
12098
|
+
["模型", "Raw Input", "Cache Write", "Cache Read", "Output", "总计", "调用", "估算调用", "估算价格"].forEach((label) => {
|
|
12099
|
+
const cell = document.createElement("th");
|
|
12100
|
+
cell.scope = "col";
|
|
12101
|
+
cell.textContent = label;
|
|
12102
|
+
headRow.append(cell);
|
|
12103
|
+
});
|
|
12104
|
+
head.append(headRow);
|
|
12105
|
+
table.append(head);
|
|
12106
|
+
const bodyRows = document.createElement("tbody");
|
|
12107
|
+
const models = Array.isArray(usage?.models) ? usage.models : [];
|
|
12108
|
+
models.forEach((model) => appendTokenUsageDetailRow(bodyRows, model.modelId || "未指定模型", model));
|
|
12109
|
+
if (!models.length) {
|
|
12110
|
+
const emptyRow = document.createElement("tr");
|
|
12111
|
+
const emptyCell = document.createElement("td");
|
|
12112
|
+
emptyCell.colSpan = 9;
|
|
12113
|
+
emptyCell.textContent = "还没有模型用量记录。";
|
|
12114
|
+
emptyRow.append(emptyCell);
|
|
12115
|
+
bodyRows.append(emptyRow);
|
|
12116
|
+
}
|
|
12117
|
+
table.append(bodyRows);
|
|
12118
|
+
const foot = document.createElement("tfoot");
|
|
12119
|
+
appendTokenUsageDetailRow(foot, "汇总", usage?.summary ?? {}, true);
|
|
12120
|
+
table.append(foot);
|
|
12121
|
+
tableScroll.append(table);
|
|
12122
|
+
body.append(tableScroll);
|
|
12123
|
+
element.append(header, body);
|
|
12124
|
+
|
|
12125
|
+
close.addEventListener("click", () => dismissTokenUsageDetails());
|
|
12126
|
+
element.addEventListener("keydown", (event) => {
|
|
12127
|
+
if (event.key !== "Escape") return;
|
|
12128
|
+
event.preventDefault();
|
|
12129
|
+
dismissTokenUsageDetails();
|
|
12130
|
+
});
|
|
12131
|
+
region.append(element);
|
|
12132
|
+
region.classList.add("token-usage-details-region");
|
|
12133
|
+
tokenUsageDetailsTrigger = trigger;
|
|
12134
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
12135
|
+
raiseToastRegion();
|
|
12136
|
+
close.focus({ preventScroll: true });
|
|
12137
|
+
}
|
|
12138
|
+
|
|
12139
|
+
function bindTokenUsageDetails(host, usage, title) {
|
|
12140
|
+
const button = host.querySelector("[data-token-usage-details]");
|
|
12141
|
+
if (!button) return;
|
|
12142
|
+
button.addEventListener("click", () => showTokenUsageDetails(usage, title, button));
|
|
12143
|
+
}
|
|
12144
|
+
|
|
11708
12145
|
function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false } = {}) {
|
|
11709
12146
|
const summary = usage?.summary ?? {};
|
|
11710
12147
|
const totalTokens = Number(summary.totalTokens) || 0;
|
|
@@ -11721,7 +12158,7 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
|
|
|
11721
12158
|
? "供应商尚未返回可计算的缓存明细"
|
|
11722
12159
|
: `${cachedInputTokens.toLocaleString("zh-CN")} / ${cacheEligibleInputTokens.toLocaleString("zh-CN")} 个可统计输入 Token 命中缓存`;
|
|
11723
12160
|
const estimatedCost = formatEstimatedCost(summary.estimatedCost);
|
|
11724
|
-
const pricingDescription = "
|
|
12161
|
+
const pricingDescription = "根据多来源模型价格表估算,价格单位为美元;未匹配模型不计入。";
|
|
11725
12162
|
const pricingBadge = summary.pricingAvailable === true && summary.estimatedCost !== null && summary.estimatedCost !== undefined
|
|
11726
12163
|
? `<span class="usage-cost-bubble" title="${esc(pricingDescription)}">估价 ${esc(estimatedCost)}</span>`
|
|
11727
12164
|
: "";
|
|
@@ -11739,7 +12176,7 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
|
|
|
11739
12176
|
<td>${Number(work.requestCount || 0).toLocaleString("zh-CN")}</td>
|
|
11740
12177
|
</tr>`).join("");
|
|
11741
12178
|
return `<section class="usage-overview" aria-labelledby="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">
|
|
11742
|
-
<div class="config-section-header"><div><h2 id="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">${esc(title || "Token 用量")}</h2><p>${esc(description || "统计该范围内的全部 AI 调用。")}</p></div></div>
|
|
12179
|
+
<div class="config-section-header usage-overview-header"><div><h2 id="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">${esc(title || "Token 用量")}</h2><p>${esc(description || "统计该范围内的全部 AI 调用。")}</p></div><button class="ghost-button usage-details-button" type="button" data-token-usage-details aria-haspopup="dialog" aria-expanded="false" aria-controls="token-usage-details-toast">详细数据</button></div>
|
|
11743
12180
|
<div class="usage-stat-grid">
|
|
11744
12181
|
<article class="usage-stat is-primary"><div class="usage-stat-label"><span>总消耗</span>${pricingBadge}</div><strong title="${esc(exactTotal)} Token">${esc(formatTokenCount(totalTokens))}</strong><small>${esc(exactTotal)} Token</small></article>
|
|
11745
12182
|
<article class="usage-stat"><span>输入 Token</span><strong>${esc(formatTokenCount(summary.inputTokens))}</strong><small title="${esc(inputDescription)}">${esc(inputDescription)}</small></article>
|
|
@@ -11765,6 +12202,7 @@ async function renderPlatformTokenUsage() {
|
|
|
11765
12202
|
description: "汇总所有作品迄今产生的输入与输出 Token;缓存命中率仅基于供应商返回了缓存明细的调用。",
|
|
11766
12203
|
showWorks: true
|
|
11767
12204
|
});
|
|
12205
|
+
bindTokenUsageDetails(host, usage, "项目累计用量");
|
|
11768
12206
|
bindUsageCalendarInteractions(host);
|
|
11769
12207
|
scrollUsageCalendarsToLatest(host);
|
|
11770
12208
|
}
|
|
@@ -11809,6 +12247,7 @@ async function renderBookAiSettings() {
|
|
|
11809
12247
|
title: "本书 Token 用量",
|
|
11810
12248
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
11811
12249
|
})}</section><section class="config-section"><div class="config-section-header"><div><h2>每日 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然日可使用的输入与输出 Token 总量。额度必须设置为大于 0 的整数;低于 10,000 时仅提示风险;达到额度后,新的 AI 请求会等到后端时区的次日零点重置后再执行。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="daily-token-quota-enabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}>启用每日额度</label><label class="daily-token-quota-field">每日额度<input id="daily-token-quota" type="number" min="1" max="2000000000" step="1" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="本书每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label><button id="save-daily-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="daily-token-quota-status" class="usage-measurement-note" role="status">${esc(quotaStatusText)}</p></section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>该阈值按对话历史的独立预算计算,用于显示可选择压缩或忽略的提醒;整次请求达到模型上下文窗口 95% 时仍会强制压缩较早消息,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>设定上下文注入</h2><p>开启后,本书的普通 AI 请求会自动注入锁定设定、组织、种族与相关约束;即使本轮同时使用“@注入上下文设定”,也只会注入一次。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="always-include-setting-info" type="checkbox" ${settings.alwaysIncludeSettingInfo ? "checked" : ""}>是否注入设定</label><button id="save-always-include-setting-info" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>Agent 工具调用上限</h2><p>限制单次回答里 Agent 可调用工具的次数,并用「全局倍数」给整次回答加一道不会因 Compact 重置的熔断阀,防止工具死循环空耗 Token。调用上限 5–48(默认 12);全局倍数 1–6(默认 3,全局上限 = 调用上限 × 倍数)。<a class="config-doc-link" href="https://scriverse.top/docs/global-tool-call-limit.html" target="_blank" rel="noopener noreferrer">了解原理与推荐设置</a></p></div></div><div class="config-inline-save"><label class="agent-tool-call-limit-field">调用上限<input id="agent-tool-call-limit" type="number" min="5" max="48" value="${esc(String(settings.agentToolCallLimit ?? 12))}" aria-label="Agent 工具调用上限"></label><div class="agent-tool-call-global-multiplier-field"><span id="agent-tool-call-global-multiplier-label">全局倍数</span><div class="settings-layout-toggle agent-tool-call-global-multiplier-toggle" role="group" aria-labelledby="agent-tool-call-global-multiplier-label">${[1, 2, 3, 4, 5, 6].map((value) => `<button type="button" data-global-multiplier="${value}" aria-pressed="${Number(settings.agentToolCallGlobalMultiplier ?? 3) === value}">${value}</button>`).join("")}</div><input id="agent-tool-call-global-multiplier" type="hidden" value="${esc(String(Math.min(6, Math.max(1, Number(settings.agentToolCallGlobalMultiplier ?? 3) || 3))))}" aria-label="Agent 工具调用全局倍数"></div><button id="save-agent-tool-call-limit" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section ai-agent-tools-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。已开始的对话会锁定创建时的工具集,修改后仅对新对话生效,避免打断 prompt cache。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名、拼音或短关键词混合检索设定、人物、组织、时间线、关系、大纲和伏笔;非语义问答。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults, settings)}`;
|
|
12250
|
+
bindTokenUsageDetails(host, usage, "本书 Token 用量");
|
|
11812
12251
|
const dailyQuotaSection = host.querySelector("#daily-token-quota-enabled")?.closest(".config-section");
|
|
11813
12252
|
dailyQuotaSection?.insertAdjacentHTML("afterend", `<section class="config-section"><div class="config-section-header"><div><h2>每月 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然月(当月 1 日至月末)可使用的输入与输出 Token 总量。额度必须设置为大于 0 的整数;低于 1,000,000 时仅提示风险;达到额度后,新的 AI 请求会等到下月 1 日零点重置后再执行。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="monthly-token-quota-enabled" type="checkbox" ${monthlyTokenQuota === null ? "" : "checked"}>启用每月额度</label><label class="monthly-token-quota-field">每月额度<input id="monthly-token-quota" type="number" min="1" max="2000000000" step="1" value="${esc(String(monthlyTokenQuota ?? 10000))}" aria-label="本书每月 Token 额度" ${monthlyTokenQuota === null ? "disabled" : ""}></label><button id="save-monthly-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="monthly-token-quota-status" class="usage-measurement-note" role="status">${esc(monthlyQuotaStatusText)}</p></section>`);
|
|
11814
12253
|
const configureTokenQuotaInput = (input, warningId, threshold) => {
|
|
@@ -11843,7 +12282,7 @@ async function renderBookAiSettings() {
|
|
|
11843
12282
|
);
|
|
11844
12283
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
11845
12284
|
"beforeend",
|
|
11846
|
-
`<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
|
|
12285
|
+
`<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>`
|
|
11847
12286
|
);
|
|
11848
12287
|
if (!canEditModule("ai-settings")) {
|
|
11849
12288
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -12557,6 +12996,7 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
12557
12996
|
const submitLabel = options.submitLabel ?? "保存";
|
|
12558
12997
|
let submitting = false;
|
|
12559
12998
|
let disabledStates = [];
|
|
12999
|
+
$("#dialog-context-actions").replaceChildren();
|
|
12560
13000
|
$("#dialog-title").textContent = title;
|
|
12561
13001
|
$("#dialog-title").classList.toggle("hidden", Boolean(options.titleInput));
|
|
12562
13002
|
titleInput.classList.toggle("hidden", !options.titleInput);
|
|
@@ -13026,6 +13466,10 @@ function openSettingEditor(item = null, { readOnly = false } = {}) {
|
|
|
13026
13466
|
const editButton = $("#setting-editor-edit");
|
|
13027
13467
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule("settings"));
|
|
13028
13468
|
editButton.onclick = () => openSettingEditor(settingEditorItem);
|
|
13469
|
+
bindEntityDetailFavoriteButton($("#setting-editor-favorite"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
|
|
13470
|
+
settingEditorItem = updated;
|
|
13471
|
+
state.settings = upsertEntityCollection(state.settings, updated);
|
|
13472
|
+
});
|
|
13029
13473
|
const statusButtons = [$("#setting-editor-confirm"), $("#setting-editor-deprecate")];
|
|
13030
13474
|
syncSettingEditorChrome(viewOnly);
|
|
13031
13475
|
$("#setting-editor-history").onclick = async () => {
|
|
@@ -14052,11 +14496,12 @@ function renderCharacterEditorFields(item) {
|
|
|
14052
14496
|
(canReadModule("editor")
|
|
14053
14497
|
? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
|
|
14054
14498
|
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
14055
|
-
characterEditorSection("profile", "人物档案", "
|
|
14499
|
+
characterEditorSection("profile", "人物档案", "记录人物定位、行为动力、公开人设和便于创作时快速理解的简介。",
|
|
14056
14500
|
field("code", "编号", "text", item?.code) +
|
|
14057
14501
|
field("identity", "身份与定位", "text", item?.attributes?.identity) +
|
|
14058
14502
|
field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
|
|
14059
|
-
field("summary", "人物简介", "textarea", item?.profile?.summary)
|
|
14503
|
+
field("summary", "人物简介", "textarea", item?.profile?.summary) +
|
|
14504
|
+
'<div class="form-field"><span>人设摘要</span><small>关系扮演时作为公开人设注入对方可见的角色卡,不会包含私密档案或 Markdown 章节。</small><textarea name="personaSummary" maxlength="20000" aria-label="人设摘要">' + esc(item?.profile?.personaSummary ?? "") + "</textarea></div>"),
|
|
14060
14505
|
characterEditorSection("settings", "扩展设定", "可用短属性和 Markdown 长章节承载形态、能力、生态、经历与研究记录。",
|
|
14061
14506
|
field("details", "扩展属性", "key-value-list", item?.attributes?.details) +
|
|
14062
14507
|
'<div id="character-markdown-sections" class="character-markdown-sections"></div>'),
|
|
@@ -14104,7 +14549,8 @@ function collectCharacterBody(form) {
|
|
|
14104
14549
|
profile: {
|
|
14105
14550
|
...profile,
|
|
14106
14551
|
motivation: String(form.get("motivation") ?? "").trim(),
|
|
14107
|
-
summary: String(form.get("summary") ?? "").trim()
|
|
14552
|
+
summary: String(form.get("summary") ?? "").trim(),
|
|
14553
|
+
personaSummary: String(form.get("personaSummary") ?? "").trim()
|
|
14108
14554
|
},
|
|
14109
14555
|
currentState: buildCharacterState(form.getAll("stateKey"), form.getAll("stateValue"), item?.currentState ?? {}),
|
|
14110
14556
|
lockedFields: form.getAll("lockedFields").map((value) => String(value).trim()).filter(Boolean),
|
|
@@ -14256,6 +14702,13 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
|
14256
14702
|
const editButton = $("#character-editor-edit");
|
|
14257
14703
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule("characters"));
|
|
14258
14704
|
editButton.onclick = () => void openCharacterEditor(characterEditorItem);
|
|
14705
|
+
bindEntityDetailFavoriteButton($("#character-editor-favorite"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
|
|
14706
|
+
characterEditorItem = updated;
|
|
14707
|
+
if (loadedAiReferencesWorkId === state.work?.id) {
|
|
14708
|
+
state.characters = upsertEntityCollection(state.characters, updated);
|
|
14709
|
+
renderAiRoleplayCharacterSelect();
|
|
14710
|
+
}
|
|
14711
|
+
});
|
|
14259
14712
|
document.querySelectorAll("[data-character-editor-tab]").forEach((button) => {
|
|
14260
14713
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
14261
14714
|
});
|
|
@@ -14442,6 +14895,10 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
|
|
|
14442
14895
|
editButton.textContent = `编辑${label}`;
|
|
14443
14896
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule(module));
|
|
14444
14897
|
editButton.onclick = () => void openKnowledgeEditor(kind, knowledgeEditorItem);
|
|
14898
|
+
bindEntityDetailFavoriteButton($("#knowledge-editor-favorite"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
|
|
14899
|
+
knowledgeEditorItem = updated;
|
|
14900
|
+
state.organizations = upsertEntityCollection(state.organizations, updated);
|
|
14901
|
+
});
|
|
14445
14902
|
const form = $("#knowledge-editor-form");
|
|
14446
14903
|
form.onsubmit = async (event) => {
|
|
14447
14904
|
event.preventDefault();
|
|
@@ -15545,10 +16002,25 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
15545
16002
|
if (aiRequestManager.hasActive(tab.id)) return;
|
|
15546
16003
|
const composerSnapshot = captureAiPromptComposer();
|
|
15547
16004
|
const requestComposerSnapshot = retry
|
|
15548
|
-
? {
|
|
16005
|
+
? {
|
|
16006
|
+
text: retry.prompt,
|
|
16007
|
+
citations: retry.citations ?? [],
|
|
16008
|
+
references: [],
|
|
16009
|
+
images: retry.images ?? [],
|
|
16010
|
+
sceneDirection: retry.sceneDirection ?? "",
|
|
16011
|
+
scenePin: captureAiScenePin()
|
|
16012
|
+
}
|
|
15549
16013
|
: composerSnapshot;
|
|
15550
16014
|
const instruction = requestComposerSnapshot.text.trim();
|
|
15551
|
-
|
|
16015
|
+
const sceneDirection = $("#ai-task").value === "roleplay"
|
|
16016
|
+
? String(requestComposerSnapshot.sceneDirection ?? "").trim()
|
|
16017
|
+
: "";
|
|
16018
|
+
const scenePin = $("#ai-task").value === "roleplay"
|
|
16019
|
+
? normalizeRoleplayScenePin(requestComposerSnapshot.scenePin)
|
|
16020
|
+
: emptyRoleplayScenePin();
|
|
16021
|
+
if (!instruction && !sceneDirection) {
|
|
16022
|
+
return toast($("#ai-task").value === "roleplay" ? "请输入台词或场景旁白" : "请输入指令", "error");
|
|
16023
|
+
}
|
|
15552
16024
|
if ($("#ai-task").value === "roleplay" && !state.aiRoleplayCharacter) return toast("请先选择角色卡", "error");
|
|
15553
16025
|
const requestScope = currentAiRequestScope();
|
|
15554
16026
|
if (!requestScope) return toast("请先选择章节", "error");
|
|
@@ -15645,6 +16117,8 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
15645
16117
|
if (taskType === "chat") {
|
|
15646
16118
|
const streamed = await streamChat(requestHolder, aiRetryStreamRequestBody({
|
|
15647
16119
|
instruction,
|
|
16120
|
+
...(sceneDirection ? { sceneDirection } : {}),
|
|
16121
|
+
...($("#ai-task").value === "roleplay" ? { scenePin } : {}),
|
|
15648
16122
|
scope,
|
|
15649
16123
|
modelId,
|
|
15650
16124
|
citations,
|
|
@@ -15824,9 +16298,23 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
15824
16298
|
const message = document.createElement("div");
|
|
15825
16299
|
message.className = "assistant-message is-streaming";
|
|
15826
16300
|
message.dataset.testid = "ai-stream-message";
|
|
15827
|
-
|
|
16301
|
+
const streamConnectionStartedAt = Date.now();
|
|
16302
|
+
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流…… <span class="ai-stream-connection-seconds" data-testid="ai-stream-connection-seconds"></span> 秒</div>';
|
|
15828
16303
|
const content = message.querySelector(".message-body");
|
|
15829
16304
|
const meta = message.querySelector(".message-meta");
|
|
16305
|
+
const connectionSeconds = message.querySelector(".ai-stream-connection-seconds");
|
|
16306
|
+
const renderStreamConnectionElapsed = () => {
|
|
16307
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - streamConnectionStartedAt) / 1000));
|
|
16308
|
+
connectionSeconds.textContent = String(elapsedSeconds);
|
|
16309
|
+
};
|
|
16310
|
+
renderStreamConnectionElapsed();
|
|
16311
|
+
let streamConnectionTimer = window.setInterval(renderStreamConnectionElapsed, 1000);
|
|
16312
|
+
const stopStreamConnectionTimer = () => {
|
|
16313
|
+
if (streamConnectionTimer === null) return;
|
|
16314
|
+
window.clearInterval(streamConnectionTimer);
|
|
16315
|
+
streamConnectionTimer = null;
|
|
16316
|
+
};
|
|
16317
|
+
const streamConnectionEstablishedEvents = new Set(["delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
|
|
15830
16318
|
const streamSpeedController = createStreamTypewriterSpeedController();
|
|
15831
16319
|
let messageMounted = false;
|
|
15832
16320
|
const mountAssistantMessage = () => {
|
|
@@ -15903,6 +16391,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
15903
16391
|
let streamError = null;
|
|
15904
16392
|
const consume = async (eventName, payload) => {
|
|
15905
16393
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
16394
|
+
if (streamConnectionEstablishedEvents.has(eventName)) stopStreamConnectionTimer();
|
|
15906
16395
|
if (eventName === "context") {
|
|
15907
16396
|
contextAction = typeof payload.action === "string" ? payload.action : "ready";
|
|
15908
16397
|
if (!tab.promptSent) setAiChatTabContextUsage(tab, payload.usage);
|
|
@@ -16014,7 +16503,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
16014
16503
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
16015
16504
|
message.classList.remove("is-streaming");
|
|
16016
16505
|
content.setAttribute("aria-busy", "false");
|
|
16017
|
-
message.querySelector(".message-heading > span").textContent = "
|
|
16506
|
+
message.querySelector(".message-heading > span").textContent = aiAssistantLabel("", tab.roleplayCharacter);
|
|
16018
16507
|
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : toolCalls;
|
|
16019
16508
|
processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : processSteps;
|
|
16020
16509
|
const processDurationMs = Number.isFinite(payload.processDurationMs) && payload.processDurationMs >= 0
|
|
@@ -16082,6 +16571,8 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
16082
16571
|
if (streamedText) attachAssistantCopyAction(message, streamedText);
|
|
16083
16572
|
scrollAiFeedToBottom(feed);
|
|
16084
16573
|
throw streamFailure;
|
|
16574
|
+
} finally {
|
|
16575
|
+
stopStreamConnectionTimer();
|
|
16085
16576
|
}
|
|
16086
16577
|
}
|
|
16087
16578
|
|
|
@@ -16118,9 +16609,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16118
16609
|
const isInterrupted = role === "assistant" && metadata?.interrupted === true;
|
|
16119
16610
|
const interruptionCode = typeof metadata?.interruptionCode === "string" ? metadata.interruptionCode : "AI_STREAM_FAILED";
|
|
16120
16611
|
message.className = `${role === "user" ? "user-message" : "assistant-message"}${isFailure || isInterrupted ? " is-error" : ""}`;
|
|
16612
|
+
const parsedUserTurn = role === "user" ? parseRoleplayUserTurn(text) : null;
|
|
16121
16613
|
const messageBody = isFailure
|
|
16122
16614
|
? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
|
|
16123
|
-
: renderMarkdown(text);
|
|
16615
|
+
: renderMarkdown(parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
|
|
16124
16616
|
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
16125
16617
|
message.querySelector("[data-ai-tool-call-settings-link]")?.addEventListener("click", (event) => {
|
|
16126
16618
|
event.preventDefault();
|
|
@@ -16142,6 +16634,18 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16142
16634
|
failureBadge.setAttribute("aria-label", `消息状态:${isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : "失败"}`);
|
|
16143
16635
|
heading.firstElementChild?.append(failureBadge);
|
|
16144
16636
|
}
|
|
16637
|
+
if (parsedUserTurn?.hasMarkup && parsedUserTurn.sceneDirection) {
|
|
16638
|
+
const scene = document.createElement("div");
|
|
16639
|
+
scene.className = "user-message-scene";
|
|
16640
|
+
const sceneLabel = document.createElement("span");
|
|
16641
|
+
sceneLabel.className = "user-message-scene-label";
|
|
16642
|
+
sceneLabel.textContent = "旁白 / 场景";
|
|
16643
|
+
const sceneBody = document.createElement("div");
|
|
16644
|
+
sceneBody.className = "user-message-scene-body";
|
|
16645
|
+
sceneBody.textContent = parsedUserTurn.sceneDirection;
|
|
16646
|
+
scene.append(sceneLabel, sceneBody);
|
|
16647
|
+
message.querySelector(".message-body")?.before(scene);
|
|
16648
|
+
}
|
|
16145
16649
|
const mentionGroups = role === "user"
|
|
16146
16650
|
? [
|
|
16147
16651
|
["角色", metadata?.mentionCharacterIds, state.characters],
|
|
@@ -16197,7 +16701,8 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16197
16701
|
);
|
|
16198
16702
|
message.dataset.aiImageAttachmentIds = JSON.stringify(imageAttachments.map((attachment) => attachment.id));
|
|
16199
16703
|
appendAiMessageImageAttachments(message, imageAttachments);
|
|
16200
|
-
attachUserCopyAction(message, text);
|
|
16704
|
+
attachUserCopyAction(message, parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
|
|
16705
|
+
if (parsedUserTurn?.hasMarkup) message.dataset.sceneDirection = parsedUserTurn.sceneDirection;
|
|
16201
16706
|
}
|
|
16202
16707
|
if (role === "assistant" && !text.startsWith("调用失败:")) {
|
|
16203
16708
|
const selectedModelId = tab?.modelId ?? tab?.selectedModelId ?? $("#ai-model").value;
|
|
@@ -16861,16 +17366,12 @@ $("#account-settings-button").addEventListener("click", () => {
|
|
|
16861
17366
|
$("#profile-display-name").value = state.user?.displayName ?? "";
|
|
16862
17367
|
renderProfileAvatar();
|
|
16863
17368
|
$("#password-form").reset();
|
|
16864
|
-
$("#api-key-result").classList.add("hidden");
|
|
16865
|
-
$("#api-key-value").value = "";
|
|
16866
17369
|
$("#account-dialog").showModal();
|
|
16867
17370
|
api("/api/auth/api-key").then((status) => {
|
|
16868
|
-
|
|
16869
|
-
? `已配置 ${status.prefix}…${status.lastUsedAt ? `,最近使用:${formatDateTime(status.lastUsedAt)}` : ",尚未使用"}`
|
|
16870
|
-
: "尚未生成 API Key。";
|
|
16871
|
-
$("#api-key-reset-button").textContent = status.configured ? "重置 API Key" : "生成 API Key";
|
|
17371
|
+
paintApiKeyStatus(status);
|
|
16872
17372
|
}).catch((error) => {
|
|
16873
17373
|
$("#api-key-status").textContent = error.message;
|
|
17374
|
+
$("#api-key-copy-button").hidden = true;
|
|
16874
17375
|
});
|
|
16875
17376
|
});
|
|
16876
17377
|
$("#account-dialog-close").addEventListener("click", () => $("#account-dialog").close());
|
|
@@ -17307,6 +17808,17 @@ function validatePasswordChangeConfirmation() {
|
|
|
17307
17808
|
}
|
|
17308
17809
|
$("#password-form input[name='newPassword']").addEventListener("input", validatePasswordChangeConfirmation);
|
|
17309
17810
|
$("#password-form input[name='passwordConfirmation']").addEventListener("input", validatePasswordChangeConfirmation);
|
|
17811
|
+
function paintApiKeyStatus(status) {
|
|
17812
|
+
const copyButton = $("#api-key-copy-button");
|
|
17813
|
+
copyButton.hidden = !status.configured;
|
|
17814
|
+
if (status.configured) {
|
|
17815
|
+
$("#api-key-status").textContent = `已配置 ${status.prefix}…${status.lastUsedAt ? `,最近使用:${formatDateTime(status.lastUsedAt)}` : ",尚未使用"}`;
|
|
17816
|
+
$("#api-key-reset-button").textContent = "重置 API Key";
|
|
17817
|
+
return;
|
|
17818
|
+
}
|
|
17819
|
+
$("#api-key-status").textContent = "尚未生成 API Key。";
|
|
17820
|
+
$("#api-key-reset-button").textContent = "生成 API Key";
|
|
17821
|
+
}
|
|
17310
17822
|
$("#api-key-reset-button").addEventListener("click", async () => {
|
|
17311
17823
|
if ($("#api-key-reset-button").textContent.includes("重置") && !(await confirmToast(
|
|
17312
17824
|
"重置后,所有使用旧 API Key 的 CLI 会立刻退出登录。确定继续吗?",
|
|
@@ -17314,25 +17826,31 @@ $("#api-key-reset-button").addEventListener("click", async () => {
|
|
|
17314
17826
|
))) return;
|
|
17315
17827
|
try {
|
|
17316
17828
|
const result = await api("/api/auth/api-key/reset", { method: "POST", body: {} });
|
|
17317
|
-
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
toast("新的 API Key
|
|
17829
|
+
paintApiKeyStatus({
|
|
17830
|
+
configured: true,
|
|
17831
|
+
prefix: result.prefix,
|
|
17832
|
+
lastUsedAt: null,
|
|
17833
|
+
copyable: result.copyable !== false
|
|
17834
|
+
});
|
|
17835
|
+
toast("新的 API Key 已生成,可点击复制");
|
|
17324
17836
|
} catch (error) { toast(error.message, "error"); }
|
|
17325
17837
|
});
|
|
17326
17838
|
$("#api-key-copy-button").addEventListener("click", async () => {
|
|
17327
|
-
const
|
|
17328
|
-
if (
|
|
17839
|
+
const button = $("#api-key-copy-button");
|
|
17840
|
+
if (button.hidden || button.disabled) return;
|
|
17841
|
+
button.disabled = true;
|
|
17329
17842
|
try {
|
|
17330
|
-
await
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
|
|
17334
|
-
|
|
17335
|
-
|
|
17843
|
+
const result = await api("/api/auth/api-key/reveal", { method: "POST", body: {} });
|
|
17844
|
+
try {
|
|
17845
|
+
await navigator.clipboard.writeText(result.apiKey);
|
|
17846
|
+
toast("API Key 已复制");
|
|
17847
|
+
} catch {
|
|
17848
|
+
toast("无法自动复制,请检查浏览器剪贴板权限", "error");
|
|
17849
|
+
}
|
|
17850
|
+
} catch (error) {
|
|
17851
|
+
toast(error.message, "error");
|
|
17852
|
+
} finally {
|
|
17853
|
+
button.disabled = false;
|
|
17336
17854
|
}
|
|
17337
17855
|
});
|
|
17338
17856
|
$("#logout-button").addEventListener("click", async () => {
|
|
@@ -17936,6 +18454,15 @@ $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftD
|
|
|
17936
18454
|
}
|
|
17937
18455
|
$("#ai-attachment-input").click();
|
|
17938
18456
|
});
|
|
18457
|
+
$("#ai-scene-button").addEventListener("click", () => {
|
|
18458
|
+
toggleAiScenePanel();
|
|
18459
|
+
});
|
|
18460
|
+
for (const field of ["#ai-scene-direction", "#ai-scene-location", "#ai-scene-present", "#ai-scene-time"]) {
|
|
18461
|
+
$(field)?.addEventListener("input", () => {
|
|
18462
|
+
syncAiSceneComposer();
|
|
18463
|
+
persistActiveAiChatTab();
|
|
18464
|
+
});
|
|
18465
|
+
}
|
|
17939
18466
|
$("#ai-attachment-input").addEventListener("change", (event) => {
|
|
17940
18467
|
const input = event.currentTarget;
|
|
17941
18468
|
void addAiImageFiles(input.files).finally(() => { input.value = ""; });
|