@musnows/scriverse 1.0.6 → 1.0.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/dist/ai-tool-results.js +14 -8
- package/dist/ai-tool-results.js.map +1 -1
- package/dist/ai.js +37 -20
- package/dist/ai.js.map +1 -1
- package/dist/app.js +5 -2
- package/dist/app.js.map +1 -1
- package/dist/public/ai-context-meter.js +45 -4
- package/dist/public/ai-render-scheduler.js +30 -0
- package/dist/public/app.js +174 -76
- package/dist/public/icon-dev.svg +10 -0
- package/dist/public/index.html +6 -3
- package/dist/public/markdown.js +22 -10
- package/dist/public/stream-markdown.js +107 -0
- package/dist/public/stream-typewriter.d.ts +1 -0
- package/dist/public/stream-typewriter.js +33 -19
- package/dist/public/styles.css +14 -15
- package/dist/store.js +7 -7
- 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,7 +1,9 @@
|
|
|
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
3
|
import { countProseWords } from "/text-count.js?v=20260906-chapter-word-count-consistency-v1";
|
|
4
|
-
import { renderMarkdown } from "/markdown.js?v=
|
|
4
|
+
import { renderMarkdown } from "/markdown.js?v=20260912-stream-render-v2";
|
|
5
|
+
import { createStreamingMarkdownRenderer } from "/stream-markdown.js?v=20260912-stream-render-v2";
|
|
6
|
+
import { createAiRenderScheduler } from "/ai-render-scheduler.js?v=20260912-stream-render-v2";
|
|
5
7
|
import { createImWorkspace } from "/im.js?v=20260904-im-judge-outcomes-v106";
|
|
6
8
|
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
|
|
7
9
|
import { applyAiSkillCommand, findAiSkillCommand, listAiSkillOptions } from "/ai-skill-menu.js?v=20260830-ai-skill-slash-menu-v1";
|
|
@@ -32,11 +34,11 @@ import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_
|
|
|
32
34
|
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260822-private-ai-endpoint-hint-v1";
|
|
33
35
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
34
36
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
35
|
-
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=
|
|
37
|
+
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260912-stream-render-v2";
|
|
36
38
|
import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
|
|
37
39
|
import { buildUsageCalendar, formatCacheHitRate, formatEstimatedCost, formatTokenCount, usageCalendarYears } from "/ai-usage.js?v=20260830-ai-usage-year-v1";
|
|
38
40
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
|
|
39
|
-
import {
|
|
41
|
+
import { attachAiContextCacheHitPercent, formatAiContextPopoverDescription, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260911-context-cache-hit-v1";
|
|
40
42
|
import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
|
|
41
43
|
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
42
44
|
import {
|
|
@@ -2688,6 +2690,7 @@ function activateAiChatTab(tabId, { persistCurrent = true, force = false } = {})
|
|
|
2688
2690
|
feed.id = feed === tab.feed ? "ai-feed" : `ai-chat-panel-${feed.dataset.aiTabId}`;
|
|
2689
2691
|
}
|
|
2690
2692
|
tab.feed.classList.remove("hidden");
|
|
2693
|
+
aiStreamRenders.refresh();
|
|
2691
2694
|
applyAiChatTabState(tab);
|
|
2692
2695
|
renderAiChatTabs();
|
|
2693
2696
|
return tab;
|
|
@@ -2703,6 +2706,7 @@ function closeAiChatTab(tabId) {
|
|
|
2703
2706
|
}
|
|
2704
2707
|
const { active } = aiChatTabManager.close(tab.id);
|
|
2705
2708
|
tab.feed.remove();
|
|
2709
|
+
aiStreamRenders.refresh();
|
|
2706
2710
|
if (!wasActive) {
|
|
2707
2711
|
renderAiChatTabs();
|
|
2708
2712
|
return;
|
|
@@ -3087,6 +3091,42 @@ const aiFeedScrollFrames = new WeakMap();
|
|
|
3087
3091
|
const aiFeedAutoScrollStates = new WeakMap();
|
|
3088
3092
|
const aiFeedScrollBindings = new WeakSet();
|
|
3089
3093
|
const aiProcessScrollFrames = new WeakMap();
|
|
3094
|
+
const aiProcessRenderStates = new WeakMap();
|
|
3095
|
+
const aiMarkdownRenderers = new WeakMap();
|
|
3096
|
+
function aiStreamTargetVisible(target) {
|
|
3097
|
+
target = target.element ?? target;
|
|
3098
|
+
const feed = target.closest(".ai-feed");
|
|
3099
|
+
const app = $("#app");
|
|
3100
|
+
return document.visibilityState !== "hidden" && Boolean(feed?.isConnected)
|
|
3101
|
+
&& !feed.classList.contains("hidden") && !app.classList.contains("shelf-mode")
|
|
3102
|
+
&& (!app.classList.contains("ai-panel-collapsed") || app.classList.contains("ai-workspace-mode"));
|
|
3103
|
+
}
|
|
3104
|
+
const aiStreamRenders = createAiRenderScheduler({
|
|
3105
|
+
isVisible: aiStreamTargetVisible,
|
|
3106
|
+
isConnected: (target) => (target.element ?? target).isConnected
|
|
3107
|
+
});
|
|
3108
|
+
document.addEventListener("visibilitychange", () => aiStreamRenders.refresh());
|
|
3109
|
+
// 只监听面板显隐,不观察正文或消息内容,避免渲染自身触发新的刷新。
|
|
3110
|
+
const aiPanelVisibilityObserver = new MutationObserver(() => aiStreamRenders.refresh());
|
|
3111
|
+
aiPanelVisibilityObserver.observe($("#app"), { attributes: true, attributeFilter: ["class"] });
|
|
3112
|
+
|
|
3113
|
+
function updateAiMarkdown(body, content) {
|
|
3114
|
+
let render = aiMarkdownRenderers.get(body);
|
|
3115
|
+
if (!render) {
|
|
3116
|
+
const target = { element: body };
|
|
3117
|
+
render = createStreamingMarkdownRenderer(body, {
|
|
3118
|
+
enqueue: (callback) => queueMicrotask(() => aiStreamRenders.enqueue(target, callback)),
|
|
3119
|
+
onRender: () => {
|
|
3120
|
+
const message = body.closest(".assistant-message");
|
|
3121
|
+
if (message?.classList.contains("is-streaming")) scrollAiProcessStepsToBottom(message);
|
|
3122
|
+
const feed = body.closest(".ai-feed");
|
|
3123
|
+
if (feed) scrollAiFeedToBottom(feed);
|
|
3124
|
+
}
|
|
3125
|
+
});
|
|
3126
|
+
aiMarkdownRenderers.set(body, render);
|
|
3127
|
+
}
|
|
3128
|
+
return render(content);
|
|
3129
|
+
}
|
|
3090
3130
|
const AI_FEED_BOTTOM_THRESHOLD_PX = 24;
|
|
3091
3131
|
let markdownTableMenuTarget = null;
|
|
3092
3132
|
let markdownTableMenuTrigger = null;
|
|
@@ -3153,9 +3193,8 @@ function scrollAiFeedToBottom(feed = $("#ai-feed"), { force = false } = {}) {
|
|
|
3153
3193
|
bindAiFeedAutoScroll(feed);
|
|
3154
3194
|
if (force) aiFeedAutoScrollStates.set(feed, true);
|
|
3155
3195
|
if (!aiFeedAutoScrollStates.get(feed)) return;
|
|
3156
|
-
feed.scrollTop = feed.scrollHeight;
|
|
3157
3196
|
const currentFrame = aiFeedScrollFrames.get(feed);
|
|
3158
|
-
if (currentFrame !== undefined
|
|
3197
|
+
if (currentFrame !== undefined || !aiStreamTargetVisible(feed)) return;
|
|
3159
3198
|
const nextFrame = window.requestAnimationFrame(() => {
|
|
3160
3199
|
feed.scrollTop = feed.scrollHeight;
|
|
3161
3200
|
aiFeedScrollFrames.delete(feed);
|
|
@@ -3169,9 +3208,8 @@ function scrollAiProcessStepsToBottom(message) {
|
|
|
3169
3208
|
body.scrollTop = body.scrollHeight;
|
|
3170
3209
|
});
|
|
3171
3210
|
};
|
|
3172
|
-
scroll();
|
|
3173
3211
|
const currentFrame = aiProcessScrollFrames.get(message);
|
|
3174
|
-
if (currentFrame !== undefined
|
|
3212
|
+
if (currentFrame !== undefined || !aiStreamTargetVisible(message)) return;
|
|
3175
3213
|
const nextFrame = window.requestAnimationFrame(() => {
|
|
3176
3214
|
scroll();
|
|
3177
3215
|
aiProcessScrollFrames.delete(message);
|
|
@@ -3724,74 +3762,80 @@ function shouldRenderAiProcessStep(step) {
|
|
|
3724
3762
|
}
|
|
3725
3763
|
|
|
3726
3764
|
function renderAiProcessSteps(message, steps, completed, durationMs = null, visibleContents = null) {
|
|
3727
|
-
const
|
|
3728
|
-
const previousScrollStates = new Map();
|
|
3729
|
-
previousDetails?.querySelectorAll(".ai-process-step[data-ai-process-step-id]").forEach((section) => {
|
|
3730
|
-
const body = section.querySelector(".ai-process-step-body");
|
|
3731
|
-
if (!body) return;
|
|
3732
|
-
previousScrollStates.set(section.dataset.aiProcessStepId, {
|
|
3733
|
-
scrollTop: body.scrollTop,
|
|
3734
|
-
nearBottom: body.scrollHeight - body.scrollTop - body.clientHeight < 24
|
|
3735
|
-
});
|
|
3736
|
-
});
|
|
3737
|
-
previousDetails?.remove();
|
|
3765
|
+
const cached = aiProcessRenderStates.get(message);
|
|
3738
3766
|
const renderableSteps = (Array.isArray(steps) ? steps : []).filter(shouldRenderAiProcessStep);
|
|
3739
|
-
if (!renderableSteps.length)
|
|
3740
|
-
|
|
3767
|
+
if (!renderableSteps.length) {
|
|
3768
|
+
cached?.details.remove();
|
|
3769
|
+
aiProcessRenderStates.delete(message);
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
const details = cached?.details ?? document.createElement("details");
|
|
3741
3773
|
details.className = "ai-process-details";
|
|
3742
3774
|
// 存在待确认/待回答的交互卡片时保持展开,避免审批入口在历史消息中被折叠。
|
|
3743
3775
|
details.open = !completed || steps.some((step) => step?.type === "tool" && isInteractiveToolPending(step.toolCall));
|
|
3744
|
-
const summary = document.createElement("summary");
|
|
3745
|
-
const title = document.createElement("span");
|
|
3776
|
+
const summary = cached?.summary ?? document.createElement("summary");
|
|
3777
|
+
const title = cached?.title ?? document.createElement("span");
|
|
3746
3778
|
title.textContent = completed ? "思考与执行过程" : "正在思考与执行";
|
|
3747
|
-
const status = document.createElement("small");
|
|
3779
|
+
const status = cached?.status ?? document.createElement("small");
|
|
3748
3780
|
const duration = durationMs === null || durationMs === undefined ? "" : formatAiProcessDuration(durationMs);
|
|
3749
3781
|
status.textContent = `${renderableSteps.length} 个步骤${duration ? ` · 耗时 ${duration}` : ""}`;
|
|
3750
|
-
summary.append(title, status);
|
|
3751
|
-
const list = document.createElement("div");
|
|
3782
|
+
if (!cached) summary.append(title, status);
|
|
3783
|
+
const list = cached?.list ?? document.createElement("div");
|
|
3752
3784
|
list.className = "ai-process-list";
|
|
3753
|
-
|
|
3785
|
+
const entries = new Map();
|
|
3786
|
+
const nextNodes = [];
|
|
3787
|
+
for (const [index, step] of renderableSteps.entries()) {
|
|
3788
|
+
const key = `${step.type}:${String(step.id ?? index)}`;
|
|
3789
|
+
const previous = cached?.entries.get(key);
|
|
3754
3790
|
if (step?.type === "context_compaction") {
|
|
3755
|
-
|
|
3791
|
+
const divider = previous?.node ?? createAiContextCompactionDivider({
|
|
3756
3792
|
kind: "tool",
|
|
3757
3793
|
ariaLabel: `第 ${Number(step.round) || 1} 轮已压缩上下文`,
|
|
3758
3794
|
title: `已将 ${Number(step.sourceMessageCount) || 0} 条工具上下文压缩为摘要`
|
|
3759
|
-
})
|
|
3795
|
+
});
|
|
3796
|
+
entries.set(key, { node: divider });
|
|
3797
|
+
nextNodes.push(divider);
|
|
3760
3798
|
continue;
|
|
3761
3799
|
}
|
|
3762
3800
|
if (step?.type === "tool" && step.toolCall) {
|
|
3801
|
+
if (previous?.value === step.toolCall) {
|
|
3802
|
+
entries.set(key, previous);
|
|
3803
|
+
nextNodes.push(previous.node);
|
|
3804
|
+
continue;
|
|
3805
|
+
}
|
|
3763
3806
|
const tool = document.createElement("section");
|
|
3764
3807
|
tool.className = "ai-process-step ai-process-tool-step";
|
|
3765
3808
|
const label = document.createElement("small");
|
|
3766
3809
|
label.textContent = `第 ${Number(step.round) || 1} 轮 · 工具调用`;
|
|
3767
3810
|
tool.append(label, createAiToolCallButton(step.toolCall));
|
|
3768
|
-
|
|
3811
|
+
entries.set(key, { node: tool, value: step.toolCall });
|
|
3812
|
+
nextNodes.push(tool);
|
|
3769
3813
|
continue;
|
|
3770
3814
|
}
|
|
3771
|
-
const section = document.createElement("section");
|
|
3815
|
+
const section = previous?.node ?? document.createElement("section");
|
|
3772
3816
|
section.className = `ai-process-step ai-process-${step.type}-step`;
|
|
3773
3817
|
section.dataset.aiProcessStepId = `${step.type}:${String(step.id ?? step.round ?? "")}`;
|
|
3774
|
-
const label = document.createElement("small");
|
|
3818
|
+
const label = previous?.label ?? document.createElement("small");
|
|
3775
3819
|
label.textContent = `第 ${Number(step.round) || 1} 轮 · ${step.type === "thinking" ? "Thinking" : "中间输出"}`;
|
|
3776
|
-
const body = document.createElement("div");
|
|
3820
|
+
const body = previous?.body ?? document.createElement("div");
|
|
3777
3821
|
body.className = "message-body ai-process-step-body";
|
|
3778
3822
|
const content = visibleContents?.has(step) ? visibleContents.get(step) : step.content;
|
|
3779
|
-
body
|
|
3780
|
-
section.append(label, body);
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3823
|
+
updateAiMarkdown(body, content);
|
|
3824
|
+
if (!previous) section.append(label, body);
|
|
3825
|
+
entries.set(key, { node: section, label, body });
|
|
3826
|
+
nextNodes.push(section);
|
|
3827
|
+
}
|
|
3828
|
+
if (!cached) details.append(summary, list);
|
|
3829
|
+
nextNodes.forEach((node, index) => {
|
|
3830
|
+
if (list.children[index] !== node) list.insertBefore(node, list.children[index] ?? null);
|
|
3831
|
+
});
|
|
3832
|
+
while (list.children.length > nextNodes.length) list.lastElementChild.remove();
|
|
3833
|
+
if (!cached) {
|
|
3834
|
+
const body = message.querySelector(".message-body");
|
|
3835
|
+
if (body) body.before(details);
|
|
3836
|
+
else message.append(details);
|
|
3837
|
+
}
|
|
3838
|
+
aiProcessRenderStates.set(message, { details, summary, title, status, list, entries, completed });
|
|
3795
3839
|
if (!completed) scrollAiProcessStepsToBottom(message);
|
|
3796
3840
|
}
|
|
3797
3841
|
|
|
@@ -5393,6 +5437,26 @@ function addSelectedLinesAsCitation() {
|
|
|
5393
5437
|
toast(`已引用《${citation.chapterTitle}》第 ${citation.startLine}${citation.startLine === citation.endLine ? "" : `-${citation.endLine}`} 行`);
|
|
5394
5438
|
}
|
|
5395
5439
|
|
|
5440
|
+
function addChapterAsAiReference(chapterId) {
|
|
5441
|
+
if (!state.work || !canWritePermissionModule(state.work, "ai-chat")) return toast("当前账户没有创作助手编辑权限", "error");
|
|
5442
|
+
const volume = state.work.volumes.find((item) => item.chapters.some((chapter) => chapter.id === chapterId));
|
|
5443
|
+
const chapter = volume?.chapters.find((item) => item.id === chapterId);
|
|
5444
|
+
if (!volume || !chapter) return toast("章节不存在或已被删除", "error");
|
|
5445
|
+
const reference = { kind: "chapter", id: String(chapter.id), name: `${volume.title} / ${chapter.title}` };
|
|
5446
|
+
if (state.aiReferences.some((item) => aiReferenceKey(item) === aiReferenceKey(reference))) {
|
|
5447
|
+
ensureAiPanelExpanded();
|
|
5448
|
+
renderAiReferences();
|
|
5449
|
+
return toast(`《${chapter.title}》已在助手引用中`);
|
|
5450
|
+
}
|
|
5451
|
+
const chapterReferenceCount = state.aiReferences.filter((item) => item.kind === "chapter").length;
|
|
5452
|
+
if (chapterReferenceCount >= 20) return toast("一次最多添加 20 个章节助手引用", "error");
|
|
5453
|
+
state.aiReferences.push(reference);
|
|
5454
|
+
ensureAiPanelExpanded();
|
|
5455
|
+
renderAiReferences();
|
|
5456
|
+
persistActiveAiChatTab();
|
|
5457
|
+
toast(`已将《${chapter.title}》添加到助手引用`);
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5396
5460
|
async function createSelectedLineAnnotation(kind) {
|
|
5397
5461
|
const permissionModule = kind === "todo" ? "todos" : "comments";
|
|
5398
5462
|
if (!state.chapter || !chapterLineSelection || !canWritePermissionModule(state.work, permissionModule)) return;
|
|
@@ -5909,6 +5973,7 @@ function createClientError(payload, fallbackMessage, fallbackStatus = null) {
|
|
|
5909
5973
|
const error = new Error(typeof source.message === "string" ? source.message : fallbackMessage);
|
|
5910
5974
|
error.code = typeof source.code === "string" ? source.code : undefined;
|
|
5911
5975
|
error.status = Number.isInteger(source.status) ? source.status : fallbackStatus;
|
|
5976
|
+
error.failureOrigin = source.failureOrigin === "platform" || source.failureOrigin === "provider" ? source.failureOrigin : undefined;
|
|
5912
5977
|
error.details = source.details;
|
|
5913
5978
|
error.failure = typeof source.failure === "string" ? source.failure : undefined;
|
|
5914
5979
|
error.callId = typeof source.callId === "string" ? source.callId : undefined;
|
|
@@ -5946,21 +6011,31 @@ function formatAiFailureMessage(error) {
|
|
|
5946
6011
|
const providerName = typeof error?.providerName === "string" ? error.providerName : typeof details.providerName === "string" ? details.providerName : "";
|
|
5947
6012
|
const providerId = typeof error?.providerId === "string" ? error.providerId : typeof details.providerId === "string" ? details.providerId : "";
|
|
5948
6013
|
const modelId = typeof error?.modelId === "string" ? error.modelId : typeof details.modelId === "string" ? details.modelId : "";
|
|
6014
|
+
const failureOrigin = aiFailureOrigin(error, details);
|
|
6015
|
+
const providerLabel = providerName || providerId;
|
|
5949
6016
|
if (code) lines.push(`错误码:${code}`);
|
|
5950
|
-
|
|
6017
|
+
lines.push(`错误来源:${failureOrigin === "provider" ? `LLM 供应商${providerLabel ? `(${providerLabel})` : ""}` : "叙界平台"}`);
|
|
6018
|
+
if (status) lines.push(`叙界响应状态:HTTP ${status}`);
|
|
5951
6019
|
if (details.platformLimited === true) {
|
|
5952
6020
|
const limitSource = details.limitScope === "provider"
|
|
5953
6021
|
? `配置的供应商额度${providerName ? `(${providerName})` : ""}`
|
|
5954
6022
|
: "单个小说额度";
|
|
5955
6023
|
lines.push(`叙界平台限制来源:${limitSource}`);
|
|
5956
6024
|
}
|
|
5957
|
-
if (
|
|
5958
|
-
if (modelId) lines.push(
|
|
6025
|
+
if (failureOrigin === "platform" && providerLabel) lines.push(`请求模型供应商:${providerLabel}`);
|
|
6026
|
+
if (modelId) lines.push(`请求模型 ID:${modelId}`);
|
|
5959
6027
|
if (callId) lines.push(`调用 ID:${callId}`);
|
|
5960
|
-
if (failure && failure !== message) lines.push(
|
|
6028
|
+
if (failure && failure !== message) lines.push(`${failureOrigin === "provider" ? "LLM 供应商详情" : "平台详情"}:${failure}`);
|
|
5961
6029
|
return lines.join("\n");
|
|
5962
6030
|
}
|
|
5963
6031
|
|
|
6032
|
+
function aiFailureOrigin(error, details) {
|
|
6033
|
+
if (error?.failureOrigin === "platform" || error?.failureOrigin === "provider") return error.failureOrigin;
|
|
6034
|
+
if (details.failureOrigin === "platform" || details.failureOrigin === "provider") return details.failureOrigin;
|
|
6035
|
+
if (details.platformLimited === true || error?.code !== "AI_CALL_FAILED") return "platform";
|
|
6036
|
+
return "provider";
|
|
6037
|
+
}
|
|
6038
|
+
|
|
5964
6039
|
function aiFailureMessageMetadata(error) {
|
|
5965
6040
|
const details = error?.details && typeof error.details === "object" && !Array.isArray(error.details) ? error.details : {};
|
|
5966
6041
|
return {
|
|
@@ -6074,6 +6149,12 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
6074
6149
|
function applyProductHealthMetadata(health) {
|
|
6075
6150
|
const version = String(health?.version ?? "").trim();
|
|
6076
6151
|
const versionLabel = String(health?.versionLabel ?? "").trim();
|
|
6152
|
+
const iconPath = health?.development === true ? "/icon-dev.svg?v=20260910" : "/icon.svg?v=20260712";
|
|
6153
|
+
document.querySelectorAll(".brand-mark").forEach((element) => {
|
|
6154
|
+
element.src = iconPath;
|
|
6155
|
+
});
|
|
6156
|
+
const favicon = document.querySelector('link[rel="icon"]');
|
|
6157
|
+
if (favicon) favicon.href = iconPath;
|
|
6077
6158
|
document.querySelectorAll("[data-product-footer-version]").forEach((element) => {
|
|
6078
6159
|
element.textContent = versionLabel || (version ? `v${version}` : "v—");
|
|
6079
6160
|
});
|
|
@@ -8833,10 +8914,17 @@ function renderTree() {
|
|
|
8833
8914
|
}
|
|
8834
8915
|
});
|
|
8835
8916
|
button.addEventListener("contextmenu", (event) => {
|
|
8836
|
-
if (!canEditProse()) return;
|
|
8917
|
+
if (!canEditProse() && !canWritePermissionModule(state.work, "ai-chat")) return;
|
|
8837
8918
|
event.preventDefault();
|
|
8838
8919
|
openChapterTypeMenu(button.dataset.chapterId, event.clientX, event.clientY);
|
|
8839
8920
|
});
|
|
8921
|
+
button.addEventListener("keydown", (event) => {
|
|
8922
|
+
if (!canEditProse() && !canWritePermissionModule(state.work, "ai-chat")) return;
|
|
8923
|
+
if (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10")) return;
|
|
8924
|
+
event.preventDefault();
|
|
8925
|
+
const rect = button.getBoundingClientRect();
|
|
8926
|
+
openChapterTypeMenu(button.dataset.chapterId, rect.left, rect.bottom);
|
|
8927
|
+
});
|
|
8840
8928
|
if (proseEditable) {
|
|
8841
8929
|
button.addEventListener("dragstart", (event) => {
|
|
8842
8930
|
event.dataTransfer?.setData("text/plain", button.dataset.chapterId);
|
|
@@ -9059,7 +9147,12 @@ function openChapterTypeMenu(chapterId, clientX, clientY) {
|
|
|
9059
9147
|
if (!chapter) return;
|
|
9060
9148
|
state.contextChapterId = chapterId;
|
|
9061
9149
|
const menu = $("#chapter-type-menu");
|
|
9062
|
-
|
|
9150
|
+
const canManageChapter = canEditProse();
|
|
9151
|
+
const canAddAiReference = canWritePermissionModule(state.work, "ai-chat");
|
|
9152
|
+
menu.querySelector("strong").textContent = `操作“${chapter.title}”`;
|
|
9153
|
+
menu.querySelectorAll("[data-chapter-type], [data-delete-chapter]").forEach((button) => button.classList.toggle("hidden", !canManageChapter));
|
|
9154
|
+
menu.querySelector("[data-add-chapter-ai-reference]")?.classList.toggle("hidden", !canAddAiReference);
|
|
9155
|
+
menu.querySelector("#chapter-type-ai-reference-separator")?.classList.toggle("hidden", !(canManageChapter && canAddAiReference));
|
|
9063
9156
|
menu.querySelectorAll("[data-chapter-type]").forEach((button) => {
|
|
9064
9157
|
button.classList.toggle("active", button.dataset.chapterType === (chapter.chapterType || "正文"));
|
|
9065
9158
|
button.setAttribute("aria-checked", String(button.classList.contains("active")));
|
|
@@ -13973,7 +14066,7 @@ async function renderBookAiSettings() {
|
|
|
13973
14066
|
const remoteMcpStatusText = remoteMcpServers.length > 0
|
|
13974
14067
|
? `已验证 ${remoteMcpServers.length} 个远程 MCP Server,共发现 ${remoteMcpToolCount} 个工具。`
|
|
13975
14068
|
: "尚未配置远程 MCP Server。";
|
|
13976
|
-
const maximumAgentToolCallLimit = Math.max(
|
|
14069
|
+
const maximumAgentToolCallLimit = Math.max(10, Number(settings.agentToolCallLimitMaximum) || 300);
|
|
13977
14070
|
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image", "calculate_time"]);
|
|
13978
14071
|
const dailyTokenQuota = settings.dailyTokenQuota === null ? null : Number(settings.dailyTokenQuota);
|
|
13979
14072
|
const quotaUsedTokens = Number(usage?.quota?.usedTokens) || 0;
|
|
@@ -13995,7 +14088,7 @@ async function renderBookAiSettings() {
|
|
|
13995
14088
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
13996
14089
|
title: "本书 Token 用量",
|
|
13997
14090
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
13998
|
-
})}</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。调用上限
|
|
14091
|
+
})}</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。调用上限 10–${maximumAgentToolCallLimit}(默认 20);全局倍数 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="10" max="${maximumAgentToolCallLimit}" value="${esc(String(settings.agentToolCallLimit ?? 20))}" 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)}`;
|
|
13999
14092
|
const workSystemPromptSection = host.querySelector("#work-system-prompt")?.closest(".config-section");
|
|
14000
14093
|
workSystemPromptSection?.insertAdjacentHTML("afterend", `<section class="config-section remote-mcp-settings"><div class="config-section-header"><div><h2>远程 MCP 工具</h2><p>填写标准的 <code>mcpServers</code> JSON 配置。保存前会逐个检查 JSON、远程传输、安全地址、MCP 握手与工具列表;任一 Server 失败时都不会覆盖当前配置。</p></div></div><label class="field-label remote-mcp-config-field"><span>mcpServers JSON</span><textarea id="remote-mcp-config" rows="12" spellcheck="false" autocapitalize="off" autocomplete="off" aria-describedby="remote-mcp-config-help remote-mcp-status" placeholder='{"mcpServers":{"example":{"url":"https://example.com/mcp"}}}'>${esc(remoteMcpConfigText)}</textarea></label><small id="remote-mcp-config-help" class="remote-mcp-config-help">仅支持远程 MCP 工具(SSE / Streamable HTTP),不支持会执行本地命令的 stdio 配置。敏感 Header 会加密保存,页面中的 ${esc("********")} 掩码再次保存时会保留原值。</small><p id="remote-mcp-status" class="remote-mcp-status" role="status" aria-live="polite">${esc(remoteMcpStatusText)}</p><div class="card-actions"><button id="save-remote-mcp-config" class="ghost-button config-save-button" type="button">测试并保存 MCP 配置</button></div></section>`);
|
|
14001
14094
|
const semanticModelOptions = (kind, selectedId) => semanticModels
|
|
@@ -14035,10 +14128,6 @@ async function renderBookAiSettings() {
|
|
|
14035
14128
|
};
|
|
14036
14129
|
configureTokenQuotaInput(host.querySelector("#daily-token-quota"), "daily-token-quota-warning", 10_000);
|
|
14037
14130
|
configureTokenQuotaInput(host.querySelector("#monthly-token-quota"), "monthly-token-quota-warning", 1_000_000);
|
|
14038
|
-
const agentToolCallLimitInput = host.querySelector("#agent-tool-call-limit");
|
|
14039
|
-
agentToolCallLimitInput?.setAttribute("max", String(maximumAgentToolCallLimit));
|
|
14040
|
-
const agentToolCallDescription = agentToolCallLimitInput?.closest(".config-section")?.querySelector(".config-section-header p");
|
|
14041
|
-
if (agentToolCallDescription?.firstChild) agentToolCallDescription.firstChild.nodeValue = agentToolCallDescription.firstChild.nodeValue.replace("5–48", `5–${maximumAgentToolCallLimit}`);
|
|
14042
14131
|
host.querySelectorAll(".config-section").forEach((section) => {
|
|
14043
14132
|
if (section.querySelector("h2")?.textContent === "Agent 工具调用上限") section.id = "agent-tool-call-limit-settings";
|
|
14044
14133
|
});
|
|
@@ -14398,9 +14487,9 @@ async function renderBookAiSettings() {
|
|
|
14398
14487
|
const button = $("#save-agent-tool-call-limit");
|
|
14399
14488
|
const input = $("#agent-tool-call-limit");
|
|
14400
14489
|
const value = Number(input.value);
|
|
14401
|
-
const maximum = Number(input.max) ||
|
|
14402
|
-
if (!Number.isInteger(value) || value <
|
|
14403
|
-
toast(`Agent 工具调用上限必须是
|
|
14490
|
+
const maximum = Number(input.max) || 300;
|
|
14491
|
+
if (!Number.isInteger(value) || value < 10) {
|
|
14492
|
+
toast(`Agent 工具调用上限必须是 10 到 ${maximum} 之间的整数`, "error");
|
|
14404
14493
|
input.focus();
|
|
14405
14494
|
return;
|
|
14406
14495
|
}
|
|
@@ -14589,10 +14678,7 @@ function renderAiContextDistribution(usage) {
|
|
|
14589
14678
|
const popover = $("#ai-context-popover");
|
|
14590
14679
|
const host = $("#ai-context-distribution");
|
|
14591
14680
|
const distribution = normalizeAiContextTokenDistribution(usage);
|
|
14592
|
-
|
|
14593
|
-
$("#ai-context-popover-description").textContent = usage
|
|
14594
|
-
? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok · ${formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow)}`
|
|
14595
|
-
: "选择可用模型后显示当前上下文用量";
|
|
14681
|
+
$("#ai-context-popover-description").textContent = formatAiContextPopoverDescription(usage);
|
|
14596
14682
|
host.replaceChildren(...distribution.items.map((item) => {
|
|
14597
14683
|
const row = document.createElement("div");
|
|
14598
14684
|
row.className = "ai-context-distribution-row";
|
|
@@ -14634,13 +14720,11 @@ function setAiContextMeter(usage, allowShrink = true) {
|
|
|
14634
14720
|
: mergeAiContextUsage(latestAiContextUsage, usage, false);
|
|
14635
14721
|
latestAiContextUsage = displayUsage;
|
|
14636
14722
|
const meter = $("#ai-context-meter");
|
|
14637
|
-
const value = meter.querySelector("b");
|
|
14638
14723
|
const distribution = renderAiContextDistribution(displayUsage);
|
|
14639
14724
|
if (!displayUsage) {
|
|
14640
14725
|
meter.classList.add("is-empty");
|
|
14641
14726
|
meter.classList.remove("is-warning", "is-danger");
|
|
14642
14727
|
meter.style.setProperty("--context-usage", "0");
|
|
14643
|
-
value.textContent = "—";
|
|
14644
14728
|
const tooltip = formatAiContextUsageTooltip(null);
|
|
14645
14729
|
meter.dataset.tooltip = tooltip;
|
|
14646
14730
|
meter.setAttribute("aria-label", tooltip);
|
|
@@ -14653,7 +14737,6 @@ function setAiContextMeter(usage, allowShrink = true) {
|
|
|
14653
14737
|
meter.classList.toggle("is-warning", percent >= 70 && percent < 90);
|
|
14654
14738
|
meter.classList.toggle("is-danger", percent >= 90);
|
|
14655
14739
|
meter.style.setProperty("--context-usage", String(percent));
|
|
14656
|
-
value.textContent = formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow);
|
|
14657
14740
|
const tooltip = formatAiContextUsageTooltip(displayUsage);
|
|
14658
14741
|
meter.dataset.tooltip = tooltip;
|
|
14659
14742
|
meter.setAttribute("aria-label", `当前上下文用量:${tooltip}`);
|
|
@@ -18450,11 +18533,15 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18450
18533
|
};
|
|
18451
18534
|
const typewriter = createStreamTypewriter({
|
|
18452
18535
|
speedController: streamSpeedController,
|
|
18536
|
+
shouldAnimate: () => aiStreamTargetVisible(feed),
|
|
18453
18537
|
onRender: (text, progress) => {
|
|
18454
18538
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
|
|
18455
|
-
content
|
|
18456
|
-
|
|
18457
|
-
|
|
18539
|
+
aiStreamRenders.enqueue(content, () => {
|
|
18540
|
+
if (!aiRequestTargetsCurrentState(requestHolder.snapshot) && message.classList.contains("is-streaming")) return;
|
|
18541
|
+
updateAiMarkdown(content, text);
|
|
18542
|
+
if (message.classList.contains("is-streaming")) renderAiStreamingCharacterProgress(meta, progress.visibleCharacters);
|
|
18543
|
+
scrollAiFeedToBottom(feed);
|
|
18544
|
+
});
|
|
18458
18545
|
}
|
|
18459
18546
|
});
|
|
18460
18547
|
let streamedText = "";
|
|
@@ -18478,7 +18565,10 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18478
18565
|
const processStepVisibleContents = new Map();
|
|
18479
18566
|
const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
|
|
18480
18567
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
18481
|
-
|
|
18568
|
+
aiStreamRenders.enqueue(message, () => {
|
|
18569
|
+
renderAiProcessSteps(message, processSteps, completed, durationMs, processStepVisibleContents);
|
|
18570
|
+
scrollAiFeedToBottom(feed);
|
|
18571
|
+
});
|
|
18482
18572
|
};
|
|
18483
18573
|
const processStepTypewriter = (step) => {
|
|
18484
18574
|
const existing = processStepTypewriters.get(step);
|
|
@@ -18486,6 +18576,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18486
18576
|
processStepVisibleContents.set(step, "");
|
|
18487
18577
|
const typewriter = createStreamTypewriter({
|
|
18488
18578
|
speedController: streamSpeedController,
|
|
18579
|
+
shouldAnimate: () => aiStreamTargetVisible(feed),
|
|
18489
18580
|
onRender: (text) => {
|
|
18490
18581
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
18491
18582
|
processStepVisibleContents.set(step, text);
|
|
@@ -18648,7 +18739,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18648
18739
|
|| writingSuggestion?.processSteps?.some((step) => step?.toolCall?.status === "failed");
|
|
18649
18740
|
if (writingSuggestionFailed) setAiChatTabStatus(tab, "error");
|
|
18650
18741
|
const announcedCompaction = contextAction === "compacted" || streamContextCompacted;
|
|
18651
|
-
setAiChatTabContextUsage(tab, payload.contextUsage, announcedCompaction);
|
|
18742
|
+
setAiChatTabContextUsage(tab, attachAiContextCacheHitPercent(payload.contextUsage, payload.cacheHitPercent), announcedCompaction);
|
|
18652
18743
|
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
18653
18744
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18654
18745
|
message.classList.remove("is-streaming");
|
|
@@ -18671,7 +18762,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18671
18762
|
writingSuggestionId: writingSuggestion.id
|
|
18672
18763
|
} : {})
|
|
18673
18764
|
};
|
|
18674
|
-
|
|
18765
|
+
renderStreamingProcessSteps(true, processDurationMs);
|
|
18675
18766
|
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent, "", processDurationMs);
|
|
18676
18767
|
attachAssistantCopyAction(message, streamedText);
|
|
18677
18768
|
scrollAiFeedToBottom(feed);
|
|
@@ -21051,6 +21142,13 @@ $("#cover-file").addEventListener("change", async (event) => {
|
|
|
21051
21142
|
}
|
|
21052
21143
|
});
|
|
21053
21144
|
$("#chapter-type-menu").addEventListener("click", async (event) => {
|
|
21145
|
+
const aiReferenceButton = event.target.closest("[data-add-chapter-ai-reference]");
|
|
21146
|
+
if (aiReferenceButton) {
|
|
21147
|
+
const chapterId = state.contextChapterId;
|
|
21148
|
+
closeChapterTypeMenu();
|
|
21149
|
+
if (chapterId) addChapterAsAiReference(chapterId);
|
|
21150
|
+
return;
|
|
21151
|
+
}
|
|
21054
21152
|
const deleteButton = event.target.closest("[data-delete-chapter]");
|
|
21055
21153
|
if (deleteButton) {
|
|
21056
21154
|
const chapterId = state.contextChapterId;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title description">
|
|
2
|
+
<title id="title">叙界</title>
|
|
3
|
+
<desc id="description">一本展开的书与一颗星,代表小说创作与灵感</desc>
|
|
4
|
+
<rect width="64" height="64" rx="14" fill="#bb911a"/>
|
|
5
|
+
<path d="M10 16.5c8.7-1.6 15.7.4 21 6v29c-5.4-4.8-12.4-6.5-21-5.1V16.5Z" fill="#fffaf0"/>
|
|
6
|
+
<path d="M54 16.5c-8.7-1.6-15.7.4-21 6v29c5.4-4.8 12.4-6.5 21-5.1V16.5Z" fill="#fffaf0"/>
|
|
7
|
+
<path d="M31 22.5h2V52h-2z" fill="#c79b32"/>
|
|
8
|
+
<path d="m48 9.5 1.7 4.1 4.3 1.7-4.3 1.7-1.7 4.1-1.7-4.1-4.3-1.7 4.3-1.7L48 9.5Z" fill="#ffe6a0"/>
|
|
9
|
+
<path d="M15.5 25.5c4.2-.3 7.8.6 10.8 2.7M15.5 32c4.2-.3 7.8.6 10.8 2.7M48.5 25.5c-4.2-.3-7.8.6-10.8 2.7M48.5 32c-4.2-.3-7.8.6-10.8 2.7" fill="none" stroke="#b69048" stroke-linecap="round" stroke-width="2"/>
|
|
10
|
+
</svg>
|