@musnows/scriverse 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +157 -21
- package/dist/ai.js.map +1 -1
- package/dist/app.js +70 -6
- package/dist/app.js.map +1 -1
- package/dist/database.js +24 -2
- package/dist/database.js.map +1 -1
- package/dist/im-orchestrator.js +73 -0
- package/dist/im-orchestrator.js.map +1 -1
- package/dist/public/ai-request-manager.js +10 -2
- package/dist/public/app.js +136 -23
- package/dist/public/index.html +2 -2
- package/dist/public/stream-typewriter.d.ts +1 -0
- package/dist/public/stream-typewriter.js +22 -3
- package/dist/public/styles.css +33 -0
- package/dist/public/text-count.d.ts +1 -0
- package/dist/public/text-count.js +7 -0
- package/dist/store.js +12 -5
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1";
|
|
2
2
|
import { formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
|
+
import { countProseWords } from "/text-count.js?v=20260906-chapter-word-count-consistency-v1";
|
|
3
4
|
import { renderMarkdown } from "/markdown.js?v=20260830-adjacent-blockquotes-v1";
|
|
4
5
|
import { createImWorkspace } from "/im.js?v=20260904-im-judge-outcomes-v106";
|
|
5
6
|
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
} from "/roleplay-turn.js?v=20260823-ai-roleplay-scene-turn-v2";
|
|
15
16
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
16
17
|
import { createAiChatTabManager, normalizeAiChatTabLimit } from "/ai-chat-tabs.js?v=20260816-ai-chat-switcher-v2";
|
|
17
|
-
import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManager, isAiRequestCancellation } from "/ai-request-manager.js?v=
|
|
18
|
+
import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManager, isAiRequestCancellation } from "/ai-request-manager.js?v=20260905-question-stream-v2";
|
|
18
19
|
import { calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
19
20
|
import { buildChapterLineMirror, findChapterLineWindow } from "/chapter-editor-virtualization.js?v=20260810-visible-lines-v1";
|
|
20
21
|
import { CHAPTER_PARAGRAPH_INDENT, calculateChapterCaretScroll, chapterLineIndexAtOffset, insertIndentedParagraph } from "/chapter-editor-behavior.js?v=20260828-centered-scroll-v1";
|
|
@@ -31,7 +32,7 @@ import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_
|
|
|
31
32
|
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260822-private-ai-endpoint-hint-v1";
|
|
32
33
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
33
34
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
34
|
-
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=
|
|
35
|
+
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260906-background-stream-v2";
|
|
35
36
|
import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
|
|
36
37
|
import { buildUsageCalendar, formatCacheHitRate, formatEstimatedCost, formatTokenCount, usageCalendarYears } from "/ai-usage.js?v=20260830-ai-usage-year-v1";
|
|
37
38
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
|
|
@@ -665,8 +666,8 @@ function aiSendButtonIconMarkup(stateName) {
|
|
|
665
666
|
|
|
666
667
|
function syncAiRequestControls() {
|
|
667
668
|
const activeTabId = activeAiChatTab()?.id;
|
|
668
|
-
const sending = aiRequestManager.hasActive(activeTabId);
|
|
669
669
|
const continuingQuestion = activeTabId ? aiQuestionContinuationTabIds.has(activeTabId) : false;
|
|
670
|
+
const sending = aiRequestManager.hasActive(activeTabId) && !continuingQuestion;
|
|
670
671
|
const switching = aiConversationNavigationPending !== null;
|
|
671
672
|
const button = $("#ai-send");
|
|
672
673
|
const stateName = sending ? "stop" : (switching || continuingQuestion) ? "switching" : "send";
|
|
@@ -2952,6 +2953,7 @@ function renderMessageCardActions(message) {
|
|
|
2952
2953
|
return;
|
|
2953
2954
|
}
|
|
2954
2955
|
fork.disabled = true;
|
|
2956
|
+
const dismissForkingToast = persistentToast("正在创建分支对话…");
|
|
2955
2957
|
try {
|
|
2956
2958
|
const sourceTab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
|
|
2957
2959
|
if (!sourceTab?.conversationId) throw new Error("无法确定消息所属对话");
|
|
@@ -2965,6 +2967,8 @@ function renderMessageCardActions(message) {
|
|
|
2965
2967
|
} catch (error) {
|
|
2966
2968
|
fork.disabled = false;
|
|
2967
2969
|
toast(error.message, "error");
|
|
2970
|
+
} finally {
|
|
2971
|
+
dismissForkingToast();
|
|
2968
2972
|
}
|
|
2969
2973
|
});
|
|
2970
2974
|
actions.append(fork);
|
|
@@ -3532,18 +3536,23 @@ async function openAiUserQuestionDialog(questionId) {
|
|
|
3532
3536
|
}
|
|
3533
3537
|
}
|
|
3534
3538
|
|
|
3535
|
-
function beginAiQuestionContinuationUi(conversationId) {
|
|
3539
|
+
async function beginAiQuestionContinuationUi(conversationId) {
|
|
3536
3540
|
const tab = conversationId ? aiChatTabManager.findByConversation(conversationId) : null;
|
|
3537
3541
|
if (!tab) return null;
|
|
3542
|
+
const workId = state.work.id;
|
|
3543
|
+
await aiRequestManager.whenIdle(tab.id);
|
|
3544
|
+
if (state.work?.id !== workId || !aiChatTabManager.get(tab.id)) throw createAiRequestAbortError("AI 请求目标已切换");
|
|
3545
|
+
const requestHolder = { snapshot: aiRequestManager.begin({ tabId: tab.id, workId, conversationId }) };
|
|
3538
3546
|
aiQuestionContinuationTabIds.add(tab.id);
|
|
3539
3547
|
setAiChatTabStatus(tab, "streaming");
|
|
3540
3548
|
if (isActiveAiChatTab(tab)) syncAiRequestControls();
|
|
3541
3549
|
scrollAiFeedToBottom(tab.feed);
|
|
3542
|
-
return { tab };
|
|
3550
|
+
return { tab, requestHolder };
|
|
3543
3551
|
}
|
|
3544
3552
|
|
|
3545
3553
|
function finishAiQuestionContinuationUi(continuationUi, failed = false) {
|
|
3546
3554
|
if (!continuationUi) return;
|
|
3555
|
+
aiRequestManager.finish(continuationUi.requestHolder.snapshot);
|
|
3547
3556
|
aiQuestionContinuationTabIds.delete(continuationUi.tab.id);
|
|
3548
3557
|
if (aiChatTabManager.get(continuationUi.tab.id)) setAiChatTabStatus(continuationUi.tab, failed ? "error" : "ready");
|
|
3549
3558
|
if (isActiveAiChatTab(continuationUi.tab)) syncAiRequestControls();
|
|
@@ -3557,7 +3566,7 @@ async function reloadAiQuestionConversation(conversationId) {
|
|
|
3557
3566
|
if (String(conversation.workId ?? "") !== String(state.work?.id ?? "")) throw new Error("AI 对话不属于当前作品");
|
|
3558
3567
|
upsertAiConversationSummary(conversation);
|
|
3559
3568
|
applyConversationToAiChatTab(tab, conversation);
|
|
3560
|
-
activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
3569
|
+
if (isActiveAiChatTab(tab)) activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
3561
3570
|
return conversation;
|
|
3562
3571
|
}
|
|
3563
3572
|
|
|
@@ -3577,9 +3586,13 @@ async function respondAiUserQuestion(questionId, payload) {
|
|
|
3577
3586
|
const conversationId = typeof knownQuestion?.conversationId === "string" ? knownQuestion.conversationId : null;
|
|
3578
3587
|
if (questionDialog.open) questionDialog.close();
|
|
3579
3588
|
if (approvalCenterDialog.open) approvalCenterDialog.close();
|
|
3580
|
-
continuationUi = beginAiQuestionContinuationUi(conversationId);
|
|
3589
|
+
continuationUi = await beginAiQuestionContinuationUi(conversationId);
|
|
3581
3590
|
let question;
|
|
3582
|
-
if (
|
|
3591
|
+
if (continuationUi) {
|
|
3592
|
+
const endpoint = questionsEndpoint(`/${encodeURIComponent(String(questionId))}/${payload.action === "reject" ? "reject" : "answer"}`);
|
|
3593
|
+
const streamed = await streamChat(continuationUi.requestHolder, payload.action === "reject" ? {} : { answers: payload.answers }, createAiIdempotencyKey(), { endpoint });
|
|
3594
|
+
question = streamed.question;
|
|
3595
|
+
} else if (payload.action === "reject") {
|
|
3583
3596
|
question = await api(questionsEndpoint(`/${encodeURIComponent(String(questionId))}/reject`), { method: "POST" });
|
|
3584
3597
|
} else {
|
|
3585
3598
|
question = await api(questionsEndpoint(`/${encodeURIComponent(String(questionId))}/answer`), {
|
|
@@ -3588,8 +3601,7 @@ async function respondAiUserQuestion(questionId, payload) {
|
|
|
3588
3601
|
});
|
|
3589
3602
|
}
|
|
3590
3603
|
cacheAiQuestionView(question);
|
|
3591
|
-
currentAiQuestionDialogView = question;
|
|
3592
|
-
aiQuestionDialogQuestionId = String(question.id);
|
|
3604
|
+
if (aiQuestionDialogQuestionId === String(question.id)) currentAiQuestionDialogView = question;
|
|
3593
3605
|
await reloadAiQuestionConversation(question.conversationId ?? conversationId);
|
|
3594
3606
|
toast(payload.action === "reject" ? "已跳过该提问批次,AI 已继续处理" : "全部回答已提交,AI 已继续处理");
|
|
3595
3607
|
return question;
|
|
@@ -3609,6 +3621,7 @@ async function respondAiUserQuestion(questionId, payload) {
|
|
|
3609
3621
|
} finally {
|
|
3610
3622
|
finishAiQuestionContinuationUi(continuationUi, continuationFailed);
|
|
3611
3623
|
aiQuestionDialogBusy = false;
|
|
3624
|
+
if (questionDialog.open && currentAiQuestionDialogView?.status === "pending") renderAiUserQuestionOptions(currentAiQuestionDialogView);
|
|
3612
3625
|
}
|
|
3613
3626
|
}
|
|
3614
3627
|
|
|
@@ -9293,8 +9306,8 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
9293
9306
|
function updateChapterStats() {
|
|
9294
9307
|
if (!state.chapter) return;
|
|
9295
9308
|
const text = $("#chapter-content").value;
|
|
9296
|
-
const count =
|
|
9297
|
-
$("#chapter-stats").textContent = `${count} 字 · v${state.chapter.versionNo}`;
|
|
9309
|
+
const count = countProseWords(text);
|
|
9310
|
+
$("#chapter-stats").textContent = `${count.toLocaleString("zh-CN")} 字 · v${state.chapter.versionNo}`;
|
|
9298
9311
|
}
|
|
9299
9312
|
|
|
9300
9313
|
function readReadingStorage(key) {
|
|
@@ -9825,7 +9838,7 @@ const moduleMeta = {
|
|
|
9825
9838
|
comments: ["正文协作", "正文评论与待办", "集中查看并处理当前作品所有章节的评论与待办。", ""],
|
|
9826
9839
|
reviews: ["作者决策", "审核队列", "集中处理冲突、候选设定、低置信度关系和时间问题。", "新增审核项"],
|
|
9827
9840
|
tasks: ["AI 深度分析", "AI 分析中心", "对全书或指定章节运行人物关系、世界观、设定、事件与一致性分析。", "开始 AI 分析"],
|
|
9828
|
-
"ai-settings": ["书籍提示词", "本书 AI 设置", "
|
|
9841
|
+
"ai-settings": ["书籍提示词", "本书 AI 设置", "本书系统提示词默认追加在内置提示词和平台全局提示词之后;可在折叠的高级设置中改为彻底覆写。任务默认模型只作用于当前作品。", "保存设置"]
|
|
9829
9842
|
};
|
|
9830
9843
|
|
|
9831
9844
|
async function showModule(module) {
|
|
@@ -13423,6 +13436,53 @@ function startBackgroundTaskCenter(workId) {
|
|
|
13423
13436
|
void refreshBackgroundTaskCenter();
|
|
13424
13437
|
}
|
|
13425
13438
|
|
|
13439
|
+
function systemPromptOverrideDetails({ inputId, checked, scope }) {
|
|
13440
|
+
const platformScope = scope === "platform";
|
|
13441
|
+
const consequence = platformScope
|
|
13442
|
+
? "保存后,普通 AI 请求只会把上方平台提示词作为 system 消息;Scriverse 内置规则、本书提示词、工具指引和时间信息都不会发送。"
|
|
13443
|
+
: "保存后,本书的普通 AI 请求只会把上方本书提示词作为 system 消息;Scriverse 内置规则、平台提示词、工具指引和时间信息都不会发送。";
|
|
13444
|
+
return `<details class="system-prompt-override-settings${checked ? " is-enabled" : ""}" ${checked ? "open" : ""}><summary><span>高级设置</span><strong data-system-prompt-override-state>${checked ? "已选择覆写" : "默认追加"}</strong></summary><div class="system-prompt-override-body"><label class="checkbox-field config-checkbox-field"><input id="${inputId}" type="checkbox" ${checked ? "checked" : ""} aria-describedby="${inputId}-warning">彻底覆写系统提示词</label><div id="${inputId}-warning" class="system-prompt-override-warning" role="alert"><strong>高风险设置</strong><p>${consequence} 空提示词会让请求不携带 system 消息,可能导致上下文、工具和安全约束失效。</p></div></div></details>`;
|
|
13445
|
+
}
|
|
13446
|
+
|
|
13447
|
+
function syncSystemPromptOverridePresentation(input) {
|
|
13448
|
+
const details = input.closest(".system-prompt-override-settings");
|
|
13449
|
+
details?.classList.toggle("is-enabled", input.checked);
|
|
13450
|
+
const stateLabel = details?.querySelector("[data-system-prompt-override-state]");
|
|
13451
|
+
if (stateLabel) stateLabel.textContent = input.checked ? "已选择覆写" : "默认追加";
|
|
13452
|
+
}
|
|
13453
|
+
|
|
13454
|
+
const systemPromptOverrideConfirmationByInput = new WeakMap();
|
|
13455
|
+
|
|
13456
|
+
async function confirmSystemPromptOverride(input, scopeLabel) {
|
|
13457
|
+
if (!input.checked) {
|
|
13458
|
+
syncSystemPromptOverridePresentation(input);
|
|
13459
|
+
return;
|
|
13460
|
+
}
|
|
13461
|
+
const pendingConfirmation = systemPromptOverrideConfirmationByInput.get(input);
|
|
13462
|
+
if (pendingConfirmation) return pendingConfirmation;
|
|
13463
|
+
input.disabled = true;
|
|
13464
|
+
const confirmation = (async () => {
|
|
13465
|
+
const confirmed = await confirmToast(
|
|
13466
|
+
`开启后,${scopeLabel}提示词将彻底替换 Scriverse 内置系统提示词,模型不会收到内置规则、工具指引和其他追加提示。错误配置可能导致功能异常或安全约束失效,确认继续吗?`,
|
|
13467
|
+
{ title: "确认开启系统提示词覆写", confirmLabel: "确认开启" }
|
|
13468
|
+
);
|
|
13469
|
+
if (!confirmed) input.checked = false;
|
|
13470
|
+
syncSystemPromptOverridePresentation(input);
|
|
13471
|
+
})().finally(() => {
|
|
13472
|
+
input.disabled = false;
|
|
13473
|
+
if (systemPromptOverrideConfirmationByInput.get(input) === confirmation) {
|
|
13474
|
+
systemPromptOverrideConfirmationByInput.delete(input);
|
|
13475
|
+
}
|
|
13476
|
+
});
|
|
13477
|
+
systemPromptOverrideConfirmationByInput.set(input, confirmation);
|
|
13478
|
+
return confirmation;
|
|
13479
|
+
}
|
|
13480
|
+
|
|
13481
|
+
async function waitForSystemPromptOverrideConfirmation(input) {
|
|
13482
|
+
const confirmation = systemPromptOverrideConfirmationByInput.get(input);
|
|
13483
|
+
if (confirmation) await confirmation;
|
|
13484
|
+
}
|
|
13485
|
+
|
|
13426
13486
|
async function renderPlatformAiConfig() {
|
|
13427
13487
|
const [providers, models, settings, protocolOptions] = await Promise.all([
|
|
13428
13488
|
api("/api/platform/ai/providers"),
|
|
@@ -13440,12 +13500,23 @@ async function renderPlatformAiConfig() {
|
|
|
13440
13500
|
const available = isSelectableModel({ ...model, providerStatus: provider?.status, providerConnectionStatus: provider?.connectionStatus });
|
|
13441
13501
|
return `<option value="${esc(model.id)}" ${model.id === settings.imageToolModelId ? "selected" : ""} ${available || model.id === settings.imageToolModelId ? "" : "disabled"}>${esc(`${available ? "" : "不可用 · "}${modelOptionLabel({ ...model, providerName: model.providerName || provider?.name })}`)}</option>`;
|
|
13442
13502
|
}).join("");
|
|
13443
|
-
host.innerHTML = `<section class="config-section platform-system-prompt-section"><div class="config-section-header"><div><h2>平台全局系统提示词</h2><p
|
|
13503
|
+
host.innerHTML = `<section class="config-section platform-system-prompt-section"><div class="config-section-header"><div><h2>平台全局系统提示词</h2><p>默认追加在内置系统提示词之后,并在所有作品的专属提示词之前发送给模型。</p></div></div><div class="field-label"><textarea id="platform-system-prompt" rows="7" aria-label="全局系统提示词" placeholder="例如:默认使用简体中文,避免代替作者做最终决定。">${esc(settings.systemPrompt)}</textarea></div>${systemPromptOverrideDetails({ inputId: "platform-system-prompt-override", checked: settings.systemPromptOverride === true, scope: "platform" })}<div class="card-actions"><button id="save-platform-system-prompt" class="primary-button">保存全局提示词</button></div></section><section class="config-section platform-image-tool-section"><div class="config-section-header"><div><h2>多模态读图默认模型</h2><p>Agent 的 image 工具使用这里配置的模型读取设定库图片;作品可以在自己的 AI 设置中覆盖此选择。</p></div></div><div class="platform-image-tool-panel"><label class="platform-image-tool-field"><span>当前平台默认模型</span><select id="platform-image-tool-model" aria-label="平台多模态读图默认模型"><option value="">未配置</option>${imageModelOptions}</select></label><button id="save-platform-image-tool-model" class="ghost-button config-save-button" type="button">保存默认模型</button></div></section><section class="config-section platform-stream-timeout-section"><div class="config-section-header"><div><h2>AI 流事件空闲超时</h2><p>首个流事件或相邻流事件在此时间内没有新数据时,请求会被关闭。默认 90 秒,最低 30 秒,最高 600 秒。</p></div></div><div class="platform-stream-timeout-panel"><label class="platform-stream-timeout-field"><span>超时时间(秒)</span><input id="platform-ai-stream-idle-timeout" type="number" min="30" max="600" step="1" value="${esc(String(settings.streamIdleTimeoutSeconds ?? 90))}" aria-label="AI 流事件空闲超时时间(秒)"></label><button id="save-platform-ai-stream-idle-timeout" class="ghost-button config-save-button" type="button">保存流超时设置</button></div></section><section class="config-section platform-providers-section"><div class="config-section-header"><div><h2>模型供应商配置</h2><p>管理供应商连接、模型列表和连接状态;模型的多模态能力在对应模型配置中设置。</p></div></div>${renderProviderCards(providers, models, protocolOptions)}</section>`;
|
|
13504
|
+
$("#platform-system-prompt-override").addEventListener("change", (event) => {
|
|
13505
|
+
void confirmSystemPromptOverride(event.currentTarget, "平台全局");
|
|
13506
|
+
});
|
|
13444
13507
|
$("#save-platform-system-prompt").addEventListener("click", async () => {
|
|
13445
13508
|
const button = $("#save-platform-system-prompt");
|
|
13446
13509
|
button.disabled = true;
|
|
13447
13510
|
try {
|
|
13448
|
-
|
|
13511
|
+
const overrideInput = $("#platform-system-prompt-override");
|
|
13512
|
+
await waitForSystemPromptOverrideConfirmation(overrideInput);
|
|
13513
|
+
await api("/api/platform/ai/settings", {
|
|
13514
|
+
method: "PATCH",
|
|
13515
|
+
body: {
|
|
13516
|
+
systemPrompt: $("#platform-system-prompt").value,
|
|
13517
|
+
systemPromptOverride: overrideInput.checked
|
|
13518
|
+
}
|
|
13519
|
+
});
|
|
13449
13520
|
toast("平台全局系统提示词已保存");
|
|
13450
13521
|
} catch (error) {
|
|
13451
13522
|
toast(error.message, "error");
|
|
@@ -13861,6 +13932,18 @@ async function renderBookAiSettings() {
|
|
|
13861
13932
|
const available = isAvailableConfiguredModel(model);
|
|
13862
13933
|
return `<option value="${esc(model.id)}" ${model.id === selectedId ? "selected" : ""} ${available || model.id === selectedId ? "" : "disabled"}>${esc(`${available ? "" : "不可用 · "}${modelOptionLabel(model)}`)}</option>`;
|
|
13863
13934
|
}).join("");
|
|
13935
|
+
const workSystemPrompt = host.querySelector("#work-system-prompt");
|
|
13936
|
+
const workSystemPromptDescription = workSystemPromptSection?.querySelector(".config-section-header p");
|
|
13937
|
+
if (workSystemPromptDescription) {
|
|
13938
|
+
workSystemPromptDescription.textContent = `默认追加在内置系统提示词和平台全局系统提示词之后,只影响《${state.work.title}》的 AI 请求。`;
|
|
13939
|
+
}
|
|
13940
|
+
workSystemPrompt?.closest(".field-label")?.insertAdjacentHTML(
|
|
13941
|
+
"afterend",
|
|
13942
|
+
systemPromptOverrideDetails({ inputId: "work-system-prompt-override", checked: settings.systemPromptOverride === true, scope: "work" })
|
|
13943
|
+
);
|
|
13944
|
+
$("#work-system-prompt-override").addEventListener("change", (event) => {
|
|
13945
|
+
void confirmSystemPromptOverride(event.currentTarget, "本书");
|
|
13946
|
+
});
|
|
13864
13947
|
const semanticSection = `<section id="semantic-search-settings" class="config-section semantic-search-settings"><div class="config-section-header"><div><h2>主动语义检索(RAG)</h2><p>与拼音索引并列维护。Embedding 和 rerank 复用平台供应商的端点与凭证保险库;普通聊天、续写、润色和分析不会自动调用此通道。</p></div></div><div class="semantic-settings-grid"><label class="checkbox-field config-checkbox-field semantic-enabled-field"><input id="semantic-search-enabled" type="checkbox" ${settings.semanticSearchEnabled ? "checked" : ""}>启用主动语义检索</label><label>Embedding 模型<select id="semantic-embedding-model" aria-label="RAG Embedding 模型"><option value="">请选择 embedding 模型</option>${semanticModelOptions("embedding", settings.semanticEmbeddingModelId)}</select></label><label>Rerank 模型(可选)<select id="semantic-rerank-model" aria-label="RAG Rerank 模型"><option value="">不使用 rerank</option>${semanticModelOptions("rerank", settings.semanticRerankModelId)}</select></label><label>向量维度<input id="semantic-vector-dimension" type="number" min="1" max="65536" value="${esc(String(settings.semanticVectorDimension ?? 1024))}"></label><label>语义召回数量<input id="semantic-recall-limit" type="number" min="1" max="200" value="${esc(String(settings.semanticRecallLimit ?? 20))}"></label><label>展示结果数量<input id="semantic-result-limit" type="number" min="1" max="100" value="${esc(String(settings.semanticResultLimit ?? 12))}"></label><label>上下文预算(Token)<input id="semantic-budget-tokens" type="number" min="256" max="100000" value="${esc(String(settings.semanticBudgetTokens ?? 4000))}"></label><label>RRF 语义通道权重<input id="semantic-channel-weight" type="number" min="0.1" max="5" step="0.1" value="${esc(String(settings.semanticChannelWeight ?? 1))}"></label></div><p class="usage-measurement-note">API Key 由所选模型所属供应商的凭证保险库加密保存;此页面不会读取或返回明文密钥。更换端点、模型、维度或分片规则后必须完整重建,旧向量不会继续参与检索。</p><div id="semantic-search-index-status" role="status" aria-live="polite">${semanticIndexStatusMarkup(semanticIndex)}</div><div class="relationship-index-actions"><button id="save-semantic-search-settings" class="primary-button config-save-button" type="button">保存 RAG 配置</button><button id="sync-semantic-search-index" class="ghost-button config-save-button" type="button">同步增量</button><button id="refresh-semantic-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-semantic-search-index" class="ghost-button config-save-button" type="button">完整重建 RAG</button></div></section>`;
|
|
13865
13948
|
const relationshipSection = [...host.querySelectorAll(".config-section")].find((section) => section.querySelector("h2")?.textContent === "人物关系拼音索引");
|
|
13866
13949
|
relationshipSection?.insertAdjacentHTML("afterend", semanticSection);
|
|
@@ -14123,7 +14206,15 @@ async function renderBookAiSettings() {
|
|
|
14123
14206
|
const button = $("#save-work-system-prompt");
|
|
14124
14207
|
button.disabled = true;
|
|
14125
14208
|
try {
|
|
14126
|
-
|
|
14209
|
+
const overrideInput = $("#work-system-prompt-override");
|
|
14210
|
+
await waitForSystemPromptOverrideConfirmation(overrideInput);
|
|
14211
|
+
await api(`/api/works/${state.work.id}/ai-settings`, {
|
|
14212
|
+
method: "PATCH",
|
|
14213
|
+
body: {
|
|
14214
|
+
systemPrompt: $("#work-system-prompt").value,
|
|
14215
|
+
systemPromptOverride: overrideInput.checked
|
|
14216
|
+
}
|
|
14217
|
+
});
|
|
14127
14218
|
toast("本书系统提示词已保存");
|
|
14128
14219
|
setAiContextMeter(null);
|
|
14129
14220
|
} catch (error) {
|
|
@@ -18235,7 +18326,7 @@ function renderAiStreamingCharacterProgress(meta, visibleCharacters) {
|
|
|
18235
18326
|
meta.replaceChildren("正在生成 · ", createAiStreamCharacterCount(visible), " 字");
|
|
18236
18327
|
}
|
|
18237
18328
|
|
|
18238
|
-
async function streamChat(requestHolder, body, idempotencyKey) {
|
|
18329
|
+
async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null } = {}) {
|
|
18239
18330
|
const tab = aiChatTabForRequest(requestHolder.snapshot);
|
|
18240
18331
|
if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
|
|
18241
18332
|
const feed = tab.feed;
|
|
@@ -18258,7 +18349,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18258
18349
|
window.clearInterval(streamConnectionTimer);
|
|
18259
18350
|
streamConnectionTimer = null;
|
|
18260
18351
|
};
|
|
18261
|
-
const streamConnectionEstablishedEvents = new Set(["delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
|
|
18352
|
+
const streamConnectionEstablishedEvents = new Set(["continuation", "delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
|
|
18262
18353
|
const streamSpeedController = createStreamTypewriterSpeedController();
|
|
18263
18354
|
let messageMounted = false;
|
|
18264
18355
|
const mountAssistantMessage = () => {
|
|
@@ -18288,12 +18379,14 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18288
18379
|
let persistedMessageCreatedAt = null;
|
|
18289
18380
|
let conversationTitle = null;
|
|
18290
18381
|
let writingSuggestion = null;
|
|
18382
|
+
let question = null;
|
|
18291
18383
|
let persistedUserMessage = null;
|
|
18292
18384
|
let contextAction = "ready";
|
|
18293
18385
|
let warningOnly = false;
|
|
18294
18386
|
let streamContextCompacted = false;
|
|
18295
18387
|
const processStartedAt = Date.now();
|
|
18296
|
-
|
|
18388
|
+
let previousProcessDurationMs = 0;
|
|
18389
|
+
const elapsedProcessTime = () => previousProcessDurationMs + Math.max(0, Date.now() - processStartedAt);
|
|
18297
18390
|
const processStepTypewriters = new Map();
|
|
18298
18391
|
const processStepVisibleContents = new Map();
|
|
18299
18392
|
const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
|
|
@@ -18322,7 +18415,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18322
18415
|
};
|
|
18323
18416
|
try {
|
|
18324
18417
|
const request = assertAiRequestCurrent(requestHolder.snapshot);
|
|
18325
|
-
const response = await fetch(`/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
|
|
18418
|
+
const response = await fetch(endpoint ?? `/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
|
|
18326
18419
|
method: "POST",
|
|
18327
18420
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream", "X-CSRF-Token": state.csrfToken, "Idempotency-Key": idempotencyKey },
|
|
18328
18421
|
body: JSON.stringify(body),
|
|
@@ -18337,7 +18430,23 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18337
18430
|
const consume = async (eventName, payload) => {
|
|
18338
18431
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18339
18432
|
if (streamConnectionEstablishedEvents.has(eventName)) stopStreamConnectionTimer();
|
|
18340
|
-
if (eventName === "
|
|
18433
|
+
if (eventName === "continuation") {
|
|
18434
|
+
if (String(payload.conversationId ?? "") !== requestHolder.snapshot.conversationId) throw new Error("流式续接返回了其他对话");
|
|
18435
|
+
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : [];
|
|
18436
|
+
processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : [];
|
|
18437
|
+
previousProcessDurationMs = Math.max(0, Number(payload.processDurationMs) || 0);
|
|
18438
|
+
const previousMessage = [...feed.querySelectorAll(".assistant-message[data-message-id]")]
|
|
18439
|
+
.find((candidate) => candidate.dataset.messageId === String(payload.messageId));
|
|
18440
|
+
if (previousMessage) {
|
|
18441
|
+
attachMessageHeading(message, aiAssistantLabel("正在生成", tab.roleplayCharacter), undefined, tab);
|
|
18442
|
+
previousMessage.replaceWith(message);
|
|
18443
|
+
messageMounted = true;
|
|
18444
|
+
} else mountAssistantMessage();
|
|
18445
|
+
attachMessageIdentity(message, payload.messageId);
|
|
18446
|
+
renderStreamingProcessSteps(false);
|
|
18447
|
+
meta.textContent = "已收到回答,正在继续思考与执行……";
|
|
18448
|
+
scrollAiFeedToBottom(feed);
|
|
18449
|
+
} else if (eventName === "context") {
|
|
18341
18450
|
contextAction = typeof payload.action === "string" ? payload.action : "ready";
|
|
18342
18451
|
if (!tab.promptSent) setAiChatTabContextUsage(tab, payload.usage);
|
|
18343
18452
|
if (payload.conversation?.id) {
|
|
@@ -18434,6 +18543,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18434
18543
|
setAiChatTabContextUsage(tab, payload.contextUsage);
|
|
18435
18544
|
if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
|
|
18436
18545
|
} else if (eventName === "complete") {
|
|
18546
|
+
question = payload.question ?? null;
|
|
18437
18547
|
if (payload.warningOnly === true) {
|
|
18438
18548
|
warningOnly = true;
|
|
18439
18549
|
setAiChatTabContextUsage(tab, payload.contextUsage);
|
|
@@ -18490,7 +18600,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18490
18600
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18491
18601
|
if (streamError) throw streamError;
|
|
18492
18602
|
assertAiStreamCompleted(streamCompleted);
|
|
18493
|
-
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, writingSuggestion, userMessage: persistedUserMessage };
|
|
18603
|
+
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, writingSuggestion, userMessage: persistedUserMessage, question };
|
|
18494
18604
|
} catch (error) {
|
|
18495
18605
|
const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
|
|
18496
18606
|
const interruptionCode = typeof streamFailure.code === "string" ? streamFailure.code.slice(0, 100) : "AI_STREAM_FAILED";
|
|
@@ -18715,7 +18825,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
18715
18825
|
function continuationGuardMarkup(guard) {
|
|
18716
18826
|
if (!guard) return "";
|
|
18717
18827
|
const issues = Array.isArray(guard.issues) ? guard.issues : [];
|
|
18718
|
-
|
|
18828
|
+
const failure = typeof guard.failure === "string" && guard.failure.trim()
|
|
18829
|
+
? guard.failure.trim()
|
|
18830
|
+
: "无法完成检查,请谨慎采纳";
|
|
18831
|
+
return `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<details class="guard-failure-details"><summary>查看失败原因</summary><p>${esc(failure)}</p></details>` : issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>`;
|
|
18719
18832
|
}
|
|
18720
18833
|
|
|
18721
18834
|
async function applyAcceptedWritingSuggestion(message, suggestion) {
|
package/dist/public/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<script src="/theme-init.js?v=20260827-reader-prefetch-v1"></script>
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
|
-
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-
|
|
12
|
+
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=continuation-guard-failure-details-v1">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="auth-pending">
|
|
15
15
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -1425,6 +1425,6 @@
|
|
|
1425
1425
|
</dialog>
|
|
1426
1426
|
|
|
1427
1427
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1428
|
-
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2"></script>
|
|
1428
|
+
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=continuation-guard-failure-details-v1"></script>
|
|
1429
1429
|
</body>
|
|
1430
1430
|
</html>
|
|
@@ -28,4 +28,5 @@ export function createStreamTypewriter<FrameHandle = number>(options: {
|
|
|
28
28
|
cancelFrame?: (handle: FrameHandle) => void;
|
|
29
29
|
reducedMotion?: boolean;
|
|
30
30
|
speedController?: StreamTypewriterSpeedController | null;
|
|
31
|
+
visibilitySource?: Pick<Document, "visibilityState" | "addEventListener" | "removeEventListener"> | null;
|
|
31
32
|
}): StreamTypewriter;
|
|
@@ -74,7 +74,8 @@ export function createStreamTypewriter({
|
|
|
74
74
|
scheduleFrame = (callback) => window.requestAnimationFrame(callback),
|
|
75
75
|
cancelFrame = (handle) => window.cancelAnimationFrame(handle),
|
|
76
76
|
reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true,
|
|
77
|
-
speedController = null
|
|
77
|
+
speedController = null,
|
|
78
|
+
visibilitySource = typeof document === "undefined" ? null : document
|
|
78
79
|
}) {
|
|
79
80
|
if (typeof onRender !== "function") throw new TypeError("onRender must be a function");
|
|
80
81
|
|
|
@@ -83,10 +84,18 @@ export function createStreamTypewriter({
|
|
|
83
84
|
const idleResolvers = [];
|
|
84
85
|
let scheduledFrame = null;
|
|
85
86
|
let finishing = false;
|
|
87
|
+
let listeningForVisibility = false;
|
|
86
88
|
|
|
87
89
|
const snapshot = () => visibleCharacters.join("");
|
|
90
|
+
const visibilityChanged = () => {
|
|
91
|
+
if (pendingCharacters.length) typewriter.reveal();
|
|
92
|
+
};
|
|
88
93
|
const resolveIdle = () => {
|
|
89
94
|
if (pendingCharacters.length || scheduledFrame !== null) return;
|
|
95
|
+
if (listeningForVisibility) {
|
|
96
|
+
visibilitySource.removeEventListener("visibilitychange", visibilityChanged);
|
|
97
|
+
listeningForVisibility = false;
|
|
98
|
+
}
|
|
90
99
|
const value = snapshot();
|
|
91
100
|
for (const resolve of idleResolvers.splice(0)) resolve(value);
|
|
92
101
|
};
|
|
@@ -97,7 +106,15 @@ export function createStreamTypewriter({
|
|
|
97
106
|
});
|
|
98
107
|
};
|
|
99
108
|
const schedule = () => {
|
|
109
|
+
if (visibilitySource?.visibilityState === "hidden") {
|
|
110
|
+
typewriter.reveal();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
100
113
|
if (scheduledFrame !== null || pendingCharacters.length === 0) return;
|
|
114
|
+
if (visibilitySource && !listeningForVisibility) {
|
|
115
|
+
visibilitySource.addEventListener("visibilitychange", visibilityChanged);
|
|
116
|
+
listeningForVisibility = true;
|
|
117
|
+
}
|
|
101
118
|
scheduledFrame = scheduleFrame(() => {
|
|
102
119
|
scheduledFrame = null;
|
|
103
120
|
const batchSize = reducedMotion
|
|
@@ -114,7 +131,7 @@ export function createStreamTypewriter({
|
|
|
114
131
|
});
|
|
115
132
|
};
|
|
116
133
|
|
|
117
|
-
|
|
134
|
+
const typewriter = {
|
|
118
135
|
append(value) {
|
|
119
136
|
const characters = Array.from(String(value ?? ""));
|
|
120
137
|
if (!characters.length) return;
|
|
@@ -137,8 +154,9 @@ export function createStreamTypewriter({
|
|
|
137
154
|
finish() {
|
|
138
155
|
if (!pendingCharacters.length && scheduledFrame === null) return Promise.resolve(snapshot());
|
|
139
156
|
finishing = true;
|
|
157
|
+
const completed = new Promise((resolve) => idleResolvers.push(resolve));
|
|
140
158
|
schedule();
|
|
141
|
-
return
|
|
159
|
+
return completed;
|
|
142
160
|
},
|
|
143
161
|
reveal() {
|
|
144
162
|
if (scheduledFrame !== null) {
|
|
@@ -152,4 +170,5 @@ export function createStreamTypewriter({
|
|
|
152
170
|
return snapshot();
|
|
153
171
|
}
|
|
154
172
|
};
|
|
173
|
+
return typewriter;
|
|
155
174
|
}
|
package/dist/public/styles.css
CHANGED
|
@@ -1440,6 +1440,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1440
1440
|
display: grid;
|
|
1441
1441
|
grid-template-columns: auto minmax(0, 2em);
|
|
1442
1442
|
align-items: start;
|
|
1443
|
+
justify-content: start;
|
|
1443
1444
|
word-break: break-all;
|
|
1444
1445
|
}
|
|
1445
1446
|
.module-nav > button[data-module="outlines"] {
|
|
@@ -1778,6 +1779,21 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1778
1779
|
.config-section { margin-top: 28px; padding-top: 24px; border-top: 1px solid var(--line); }
|
|
1779
1780
|
.config-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
1780
1781
|
.platform-system-prompt-section { margin-bottom: 36px; }
|
|
1782
|
+
.system-prompt-override-settings { margin-top: 12px; overflow: hidden; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
|
|
1783
|
+
.system-prompt-override-settings.is-enabled { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }
|
|
1784
|
+
.system-prompt-override-settings summary { display: grid; min-height: 40px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 9px 12px; cursor: pointer; color: var(--muted); font-size: 11px; list-style: none; }
|
|
1785
|
+
.system-prompt-override-settings summary::-webkit-details-marker { display: none; }
|
|
1786
|
+
.system-prompt-override-settings summary::before { content: "›"; color: var(--muted); font-size: 16px; line-height: 1; transform: rotate(0deg); transition: transform .16s ease; }
|
|
1787
|
+
.system-prompt-override-settings[open] summary::before { transform: rotate(90deg); }
|
|
1788
|
+
.system-prompt-override-settings summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
1789
|
+
.system-prompt-override-settings summary strong { color: var(--ink); font-size: 10px; font-weight: 600; }
|
|
1790
|
+
.system-prompt-override-settings.is-enabled summary strong { color: var(--accent-dark); }
|
|
1791
|
+
.system-prompt-override-body { display: grid; gap: 10px; padding: 12px; border-top: 1px solid var(--line); }
|
|
1792
|
+
.system-prompt-override-body > .config-checkbox-field { display: inline-flex; width: fit-content; align-items: center; gap: 8px; margin: 0; color: var(--ink); font-size: 11px; }
|
|
1793
|
+
.system-prompt-override-body > .config-checkbox-field input { width: 18px; min-width: 18px; height: 18px; }
|
|
1794
|
+
.system-prompt-override-warning { padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--accent) 48%, var(--line)); border-radius: 5px; background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }
|
|
1795
|
+
.system-prompt-override-warning strong { color: var(--ink); font-size: 11px; }
|
|
1796
|
+
.system-prompt-override-warning p { margin: 3px 0 0; color: var(--muted); font-size: 10px; line-height: 1.55; }
|
|
1781
1797
|
.platform-image-tool-section { margin-bottom: 0; }
|
|
1782
1798
|
.platform-image-tool-panel { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; padding: 16px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
|
|
1783
1799
|
.platform-image-tool-field { display: grid; flex: 1 1 360px; gap: 7px; min-width: 0; color: var(--muted); font-size: 10px; }
|
|
@@ -3076,6 +3092,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
3076
3092
|
.writing-suggestion-ui { display: contents; }
|
|
3077
3093
|
.guard-card.warning { border-left-color: #c07b31; }.guard-card.failed { border-left-color: var(--accent); }
|
|
3078
3094
|
.guard-card strong { display: block; margin-bottom: 5px; font-size: 10px; }.guard-card p { margin: 4px 0; color: var(--muted); font-size: 9px; line-height: 1.45; }.guard-card p b { color: var(--ink); }
|
|
3095
|
+
.guard-failure-details { margin-top: 6px; }
|
|
3096
|
+
.guard-failure-details summary { width: max-content; max-width: 100%; color: var(--accent-dark); font-size: 9px; line-height: 1.45; cursor: pointer; }
|
|
3097
|
+
.guard-failure-details p { margin-top: 6px; padding: 7px 8px; border: 1px solid var(--line); border-radius: 3px; background: var(--surface); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
3079
3098
|
.message-actions { display: flex; gap: 7px; margin-top: 10px; }
|
|
3080
3099
|
.message-actions button { border: 1px solid rgba(139,61,44,.25); background: var(--surface); color: var(--accent-dark); font-size: 10px; padding: 5px 8px; border-radius: 3px; }
|
|
3081
3100
|
.prompt-box { border-top: 1px solid var(--line); padding-top: 12px; }
|
|
@@ -3520,6 +3539,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
3520
3539
|
.dialog-fields .form-field-note { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.65; }
|
|
3521
3540
|
.dialog-fields label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
|
|
3522
3541
|
.dialog-fields input, .dialog-fields textarea, .dialog-fields select { width: 100%; padding: 9px 10px; font-size: 13px; }
|
|
3542
|
+
.dialog-fields input[type="checkbox"] { flex: 0 0 16px; width: 16px; min-width: 16px; height: 16px; min-height: 16px; margin: 0; padding: 0; }
|
|
3523
3543
|
.dialog-fields select { padding-inline-end: 28px; }
|
|
3524
3544
|
.dialog-fields .character-extraction-candidate-toggle { display: inline-flex; width: auto; align-items: center; gap: 7px; margin: 0; color: var(--ink); font-size: 9px; white-space: nowrap; }
|
|
3525
3545
|
.dialog-fields .character-extraction-candidate-toggle input[type="checkbox"] { flex: 0 0 18px; width: 18px; min-width: 18px; height: 18px; padding: 0; }
|
|
@@ -3570,6 +3590,14 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
3570
3590
|
.model-capability-option small, .model-multimodal-note { color: var(--muted); font-size: 9px; line-height: 1.5; }
|
|
3571
3591
|
.model-capability-option:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
|
|
3572
3592
|
.model-multimodal-note { margin: 0 2px; }
|
|
3593
|
+
#form-dialog:has(.model-kind-fields) { width: min(720px, 92vw); }
|
|
3594
|
+
#dialog-fields:has(.model-kind-fields) { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 14px; }
|
|
3595
|
+
#dialog-fields:has(.model-kind-fields) [data-chat-model-fields] { display: contents; }
|
|
3596
|
+
#dialog-fields:has(.model-kind-fields) > .model-kind-fields,
|
|
3597
|
+
#dialog-fields:has(.model-kind-fields) > .checkbox-field,
|
|
3598
|
+
#dialog-fields:has(.model-kind-fields) .chip-field,
|
|
3599
|
+
#dialog-fields:has(.model-kind-fields) .model-multimodal-fields,
|
|
3600
|
+
#dialog-fields:has(.model-kind-fields) .model-connection-test { grid-column: 1 / -1; }
|
|
3573
3601
|
.analysis-type-description { margin: 0; color: var(--muted); font-size: 10px; line-height: 1.55; }
|
|
3574
3602
|
.task-chapter-field { transition: opacity .15s ease; }
|
|
3575
3603
|
.task-chapter-field.is-disabled { opacity: .48; }
|
|
@@ -4467,6 +4495,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
4467
4495
|
margin: auto;
|
|
4468
4496
|
border-radius: 10px;
|
|
4469
4497
|
}
|
|
4498
|
+
#form-dialog:has(.model-kind-fields) { width: calc(100vw - 20px); max-width: calc(100vw - 20px); }
|
|
4470
4499
|
.dialog-header { padding: 14px 16px 12px; }
|
|
4471
4500
|
.dialog-header h2 { font-size: 20px; }
|
|
4472
4501
|
.dialog-title-input { width: min(100%, calc(100vw - 100px)); font-size: 20px; }
|
|
@@ -4573,6 +4602,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
4573
4602
|
.onboarding-popover { width: calc(100vw - 20px); }
|
|
4574
4603
|
}
|
|
4575
4604
|
|
|
4605
|
+
@media (max-width: 480px) {
|
|
4606
|
+
#dialog-fields:has(.model-kind-fields) { grid-template-columns: minmax(0, 1fr); }
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4576
4609
|
@media (max-width: 850px) {
|
|
4577
4610
|
#onboarding-menu-button { display: none; }
|
|
4578
4611
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function countProseWords(text: unknown): number;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function countProseWords(text) {
|
|
2
|
+
const value = String(text ?? "");
|
|
3
|
+
const chinese = value.match(/[\p{Script=Han}]/gu)?.length ?? 0;
|
|
4
|
+
const withoutChinese = value.replace(/[\p{Script=Han}]/gu, " ");
|
|
5
|
+
const latin = withoutChinese.match(/[\p{L}\p{N}]+/gu)?.length ?? 0;
|
|
6
|
+
return chinese + latin;
|
|
7
|
+
}
|