@musnows/scriverse 1.0.2 → 1.0.4
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 +21 -4
- package/dist/ai.js.map +1 -1
- package/dist/app.js +92 -11
- package/dist/app.js.map +1 -1
- package/dist/public/ai-interactive.js +59 -6
- package/dist/public/ai-request-manager.js +10 -2
- package/dist/public/app.js +178 -61
- 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 +3 -0
- package/dist/public/text-count.d.ts +1 -0
- package/dist/public/text-count.js +7 -0
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -51,6 +51,17 @@ export function aiQuestionStatusLabel(status) {
|
|
|
51
51
|
return QUESTION_STATUS_LABELS[String(status)] ?? String(status);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** 待回答或尚未加载完成时不允许静默关闭提问窗口。 */
|
|
55
|
+
export function aiQuestionDialogCanClose(question) {
|
|
56
|
+
return ["answered", "rejected", "expired"].includes(String(question?.status ?? ""));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 优先读取结构化错误码,并兼容修复前只保存了错误正文的历史消息。 */
|
|
60
|
+
export function aiFailureCode(text, metadata = {}) {
|
|
61
|
+
if (typeof metadata?.errorCode === "string" && metadata.errorCode) return metadata.errorCode;
|
|
62
|
+
return String(text ?? "").match(/(?:^|\n)错误码:([^\n]+)(?:\n|$)/u)?.[1]?.trim() ?? "";
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
/** 状态徽章色调:CSS 里按 data-tone 展示统一配色。 */
|
|
55
66
|
export function statusTone(status) {
|
|
56
67
|
switch (String(status)) {
|
|
@@ -114,19 +125,63 @@ export function parseInteractiveToolPayload(toolCall) {
|
|
|
114
125
|
error
|
|
115
126
|
};
|
|
116
127
|
}
|
|
117
|
-
const
|
|
128
|
+
const rawArguments = toolCall?.arguments;
|
|
129
|
+
let suppliedArguments = rawArguments;
|
|
130
|
+
if (typeof rawArguments === "string") {
|
|
131
|
+
try { suppliedArguments = JSON.parse(rawArguments); } catch { suppliedArguments = null; }
|
|
132
|
+
}
|
|
133
|
+
const argumentQuestions = Array.isArray(suppliedArguments?.questions)
|
|
134
|
+
? suppliedArguments.questions
|
|
135
|
+
: suppliedArguments?.question
|
|
136
|
+
? [{ question: suppliedArguments.question, options: suppliedArguments.options }]
|
|
137
|
+
: [];
|
|
138
|
+
const resultQuestion = ok && result?.question && typeof result.question === "object" && !Array.isArray(result.question)
|
|
139
|
+
? result.question
|
|
140
|
+
: null;
|
|
141
|
+
const question = resultQuestion && (!Array.isArray(resultQuestion.questions) || resultQuestion.questions.length === 0) && argumentQuestions.length > 0
|
|
142
|
+
? { ...resultQuestion, questions: argumentQuestions }
|
|
143
|
+
: resultQuestion;
|
|
118
144
|
return {
|
|
119
145
|
kind: "question",
|
|
120
146
|
ok,
|
|
121
147
|
name,
|
|
122
148
|
calledAt: toolCall?.calledAt ?? "",
|
|
123
|
-
question
|
|
124
|
-
|
|
149
|
+
question,
|
|
150
|
+
argumentQuestions,
|
|
125
151
|
message: typeof result?.message === "string" ? result.message : "",
|
|
126
152
|
error
|
|
127
153
|
};
|
|
128
154
|
}
|
|
129
155
|
|
|
156
|
+
/** 兼容历史单题、批量题目、字符串选项与完整 API 选项对象。 */
|
|
157
|
+
export function normalizeAiQuestionItems(question) {
|
|
158
|
+
const items = Array.isArray(question?.questions) && question.questions.length > 0
|
|
159
|
+
? question.questions
|
|
160
|
+
: [{
|
|
161
|
+
question: question?.question ?? "",
|
|
162
|
+
options: question?.options ?? [],
|
|
163
|
+
selectedOption: question?.selectedOption ?? null,
|
|
164
|
+
customAnswer: question?.customAnswer ?? "",
|
|
165
|
+
answerText: question?.answerText ?? "",
|
|
166
|
+
isCustomAnswer: question?.isCustomAnswer === true
|
|
167
|
+
}];
|
|
168
|
+
return items.map((item, index) => ({
|
|
169
|
+
...item,
|
|
170
|
+
index,
|
|
171
|
+
question: String(item?.question ?? ""),
|
|
172
|
+
options: (Array.isArray(item?.options) ? item.options : []).map((option, optionIndex) => (
|
|
173
|
+
option && typeof option === "object" && !Array.isArray(option)
|
|
174
|
+
? {
|
|
175
|
+
...option,
|
|
176
|
+
index: Number.isInteger(option.index) ? Number(option.index) : optionIndex,
|
|
177
|
+
label: String(option.label ?? ""),
|
|
178
|
+
recommended: option.recommended === true || (option.recommended === undefined && optionIndex === 0)
|
|
179
|
+
}
|
|
180
|
+
: { index: optionIndex, label: String(option ?? ""), recommended: optionIndex === 0 }
|
|
181
|
+
))
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
130
185
|
/**
|
|
131
186
|
* 会话内的最新审批详情缓存:确认/撤销之后写回,避免卡片在重渲染时回退到提交时刻的快照。
|
|
132
187
|
*/
|
|
@@ -294,9 +349,7 @@ function buildQuestionCard(model, actions) {
|
|
|
294
349
|
card.append(body);
|
|
295
350
|
}
|
|
296
351
|
|
|
297
|
-
const options = question
|
|
298
|
-
? question.options.map((option) => option.label)
|
|
299
|
-
: model.argumentOptions;
|
|
352
|
+
const options = normalizeAiQuestionItems(question).flatMap((item) => item.options.map((option) => option.label));
|
|
300
353
|
if (options.length > 0) {
|
|
301
354
|
const list = document.createElement("ol");
|
|
302
355
|
list.className = "ai-question-option-preview";
|
|
@@ -50,13 +50,17 @@ export function createAiRequestManager() {
|
|
|
50
50
|
if (!current) return false;
|
|
51
51
|
activeRequests.delete(key);
|
|
52
52
|
current.controller.abort(createAiRequestAbortError(reason));
|
|
53
|
+
current.resolveFinished();
|
|
53
54
|
return true;
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
const cancelAll = (reason = "AI 请求已取消") => {
|
|
57
58
|
const requests = [...activeRequests.values()];
|
|
58
59
|
activeRequests.clear();
|
|
59
|
-
for (const current of requests)
|
|
60
|
+
for (const current of requests) {
|
|
61
|
+
current.controller.abort(createAiRequestAbortError(reason));
|
|
62
|
+
current.resolveFinished();
|
|
63
|
+
}
|
|
60
64
|
return requests.length;
|
|
61
65
|
};
|
|
62
66
|
|
|
@@ -67,7 +71,9 @@ export function createAiRequestManager() {
|
|
|
67
71
|
generation += 1;
|
|
68
72
|
const request = snapshot(input, controller, generation);
|
|
69
73
|
if (!request.workId) throw new Error("AI 请求必须绑定作品");
|
|
70
|
-
|
|
74
|
+
let resolveFinished;
|
|
75
|
+
const finished = new Promise((resolve) => { resolveFinished = resolve; });
|
|
76
|
+
activeRequests.set(key, { controller, snapshot: request, finished, resolveFinished });
|
|
71
77
|
return request;
|
|
72
78
|
};
|
|
73
79
|
|
|
@@ -98,6 +104,7 @@ export function createAiRequestManager() {
|
|
|
98
104
|
|
|
99
105
|
const finish = (request) => {
|
|
100
106
|
if (!isCurrent(request)) return false;
|
|
107
|
+
activeRequests.get(request.tabId).resolveFinished();
|
|
101
108
|
activeRequests.delete(request.tabId);
|
|
102
109
|
return true;
|
|
103
110
|
};
|
|
@@ -108,6 +115,7 @@ export function createAiRequestManager() {
|
|
|
108
115
|
cancel,
|
|
109
116
|
cancelAll,
|
|
110
117
|
finish,
|
|
118
|
+
whenIdle: (tabId) => activeRequests.get(requestKey({ tabId }))?.finished ?? Promise.resolve(),
|
|
111
119
|
hasActive: (tabId = null) => tabId === null
|
|
112
120
|
? activeRequests.size > 0
|
|
113
121
|
: activeRequests.has(requestKey({ tabId })),
|
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";
|
|
@@ -40,15 +41,18 @@ import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
|
|
|
40
41
|
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
41
42
|
import {
|
|
42
43
|
AI_WRITE_TOOLS_META,
|
|
44
|
+
aiFailureCode,
|
|
45
|
+
aiQuestionDialogCanClose,
|
|
43
46
|
cacheAiQuestionView,
|
|
44
47
|
cacheAiWritePlanDetail,
|
|
45
48
|
createInteractiveToolCard,
|
|
49
|
+
normalizeAiQuestionItems,
|
|
46
50
|
parseInteractiveToolPayload,
|
|
47
51
|
renderApprovalCenterRows,
|
|
48
52
|
renderWritePlanDetailMarkup,
|
|
49
53
|
isInteractiveToolPending,
|
|
50
54
|
aiFormatDateTime
|
|
51
|
-
} from "/ai-interactive.js?v=
|
|
55
|
+
} from "/ai-interactive.js?v=20260906-question-recovery-v1";
|
|
52
56
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
53
57
|
import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
|
|
54
58
|
import { clipboardImageFiles } from "/character-markdown.js?v=20260820-ai-chat-image-attachments-v1";
|
|
@@ -665,8 +669,8 @@ function aiSendButtonIconMarkup(stateName) {
|
|
|
665
669
|
|
|
666
670
|
function syncAiRequestControls() {
|
|
667
671
|
const activeTabId = activeAiChatTab()?.id;
|
|
668
|
-
const sending = aiRequestManager.hasActive(activeTabId);
|
|
669
672
|
const continuingQuestion = activeTabId ? aiQuestionContinuationTabIds.has(activeTabId) : false;
|
|
673
|
+
const sending = aiRequestManager.hasActive(activeTabId) && !continuingQuestion;
|
|
670
674
|
const switching = aiConversationNavigationPending !== null;
|
|
671
675
|
const button = $("#ai-send");
|
|
672
676
|
const stateName = sending ? "stop" : (switching || continuingQuestion) ? "switching" : "send";
|
|
@@ -2897,6 +2901,41 @@ function aiRetryStreamRequestBody(body, retry) {
|
|
|
2897
2901
|
: body;
|
|
2898
2902
|
}
|
|
2899
2903
|
|
|
2904
|
+
async function discardPendingAiQuestion(message, button) {
|
|
2905
|
+
const feed = message.closest(".ai-feed");
|
|
2906
|
+
const tab = aiChatTabManager.get(feed?.dataset.aiTabId);
|
|
2907
|
+
if (!tab?.conversationId || !tab.workId || String(tab.workId) !== String(state.work?.id ?? "")) {
|
|
2908
|
+
throw new Error("无法确定待回答问题所属的当前对话");
|
|
2909
|
+
}
|
|
2910
|
+
const label = button.querySelector("span");
|
|
2911
|
+
button.disabled = true;
|
|
2912
|
+
if (label) label.textContent = "正在作废提问";
|
|
2913
|
+
try {
|
|
2914
|
+
const expectedQuestionId = String(message.dataset.pendingQuestionId ?? "");
|
|
2915
|
+
const parameters = new URLSearchParams({ conversationId: tab.conversationId, status: "pending", limit: "1" });
|
|
2916
|
+
const payload = await api(`/api/works/${encodeURIComponent(tab.workId)}/ai/questions?${parameters}`);
|
|
2917
|
+
const questions = Array.isArray(payload) ? payload : (Array.isArray(payload?.questions) ? payload.questions : []);
|
|
2918
|
+
const questionId = String(questions.find((question) => String(question?.id ?? "") === expectedQuestionId)?.id ?? questions[0]?.id ?? "");
|
|
2919
|
+
if (!questionId) {
|
|
2920
|
+
if (label) label.textContent = "提问已处理";
|
|
2921
|
+
button.setAttribute("aria-label", "待回答提问已处理");
|
|
2922
|
+
toast("当前对话已经没有待回答问题,可以重新发送消息");
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
await respondAiUserQuestion(questionId, { action: "reject" });
|
|
2926
|
+
for (const action of tab.feed.querySelectorAll(".ai-question-discard-button")) {
|
|
2927
|
+
action.disabled = true;
|
|
2928
|
+
action.setAttribute("aria-label", "待回答提问已作废");
|
|
2929
|
+
const actionLabel = action.querySelector("span");
|
|
2930
|
+
if (actionLabel) actionLabel.textContent = "提问已作废";
|
|
2931
|
+
}
|
|
2932
|
+
} catch (error) {
|
|
2933
|
+
button.disabled = false;
|
|
2934
|
+
if (label) label.textContent = "作废提问并继续";
|
|
2935
|
+
throw error;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2900
2939
|
function renderMessageCardActions(message) {
|
|
2901
2940
|
let actions = message.querySelector(".message-card-actions");
|
|
2902
2941
|
if (!actions) {
|
|
@@ -2926,18 +2965,30 @@ function renderMessageCardActions(message) {
|
|
|
2926
2965
|
actions.append(copy);
|
|
2927
2966
|
}
|
|
2928
2967
|
if (message.dataset.status === "failed" && message.classList.contains("assistant-message")) {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
if (retry.isConnected) retry.disabled = false;
|
|
2968
|
+
if (message.dataset.errorCode === "AI_QUESTION_PENDING") {
|
|
2969
|
+
const discard = document.createElement("button");
|
|
2970
|
+
discard.type = "button";
|
|
2971
|
+
discard.className = "message-retry-button ai-question-discard-button";
|
|
2972
|
+
discard.setAttribute("aria-label", "作废待回答提问并让 AI 继续");
|
|
2973
|
+
discard.innerHTML = '<svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5l14 14M19 5 5 19"/><path d="M15 12h6M18 9l3 3-3 3"/></svg><span>作废提问并继续</span>';
|
|
2974
|
+
discard.addEventListener("click", () => {
|
|
2975
|
+
void discardPendingAiQuestion(message, discard).catch((error) => toast(`作废提问失败:${error.message}`, "error"));
|
|
2938
2976
|
});
|
|
2939
|
-
|
|
2940
|
-
|
|
2977
|
+
actions.append(discard);
|
|
2978
|
+
} else {
|
|
2979
|
+
const retry = document.createElement("button");
|
|
2980
|
+
retry.type = "button";
|
|
2981
|
+
retry.className = "message-retry-button";
|
|
2982
|
+
retry.setAttribute("aria-label", "重试");
|
|
2983
|
+
retry.innerHTML = '<svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 0 0-14.7-4L4 9"/><path d="M4 4v5h5"/><path d="M4 13a8 8 0 0 0 14.7 4L20 15"/><path d="M20 20v-5h-5"/></svg><span>重试</span>';
|
|
2984
|
+
retry.addEventListener("click", () => {
|
|
2985
|
+
retry.disabled = true;
|
|
2986
|
+
void retryAiMessage(message).finally(() => {
|
|
2987
|
+
if (retry.isConnected) retry.disabled = false;
|
|
2988
|
+
});
|
|
2989
|
+
});
|
|
2990
|
+
actions.append(retry);
|
|
2991
|
+
}
|
|
2941
2992
|
} else if (message.dataset.messageId && message.classList.contains("assistant-message")) {
|
|
2942
2993
|
const fork = document.createElement("button");
|
|
2943
2994
|
fork.type = "button";
|
|
@@ -2952,6 +3003,7 @@ function renderMessageCardActions(message) {
|
|
|
2952
3003
|
return;
|
|
2953
3004
|
}
|
|
2954
3005
|
fork.disabled = true;
|
|
3006
|
+
const dismissForkingToast = persistentToast("正在创建分支对话…");
|
|
2955
3007
|
try {
|
|
2956
3008
|
const sourceTab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
|
|
2957
3009
|
if (!sourceTab?.conversationId) throw new Error("无法确定消息所属对话");
|
|
@@ -2965,6 +3017,8 @@ function renderMessageCardActions(message) {
|
|
|
2965
3017
|
} catch (error) {
|
|
2966
3018
|
fork.disabled = false;
|
|
2967
3019
|
toast(error.message, "error");
|
|
3020
|
+
} finally {
|
|
3021
|
+
dismissForkingToast();
|
|
2968
3022
|
}
|
|
2969
3023
|
});
|
|
2970
3024
|
actions.append(fork);
|
|
@@ -3258,14 +3312,14 @@ function handleInteractiveToolCallEvent(toolCall) {
|
|
|
3258
3312
|
return;
|
|
3259
3313
|
}
|
|
3260
3314
|
if (name !== "ask_user_question" || toolCall.status === "failed") return;
|
|
3261
|
-
const question = toolCall
|
|
3315
|
+
const question = parseInteractiveToolPayload(toolCall)?.question;
|
|
3262
3316
|
if (!question?.id) return;
|
|
3263
3317
|
cacheAiQuestionView(question);
|
|
3264
3318
|
const questionId = String(question.id);
|
|
3265
3319
|
if (autoOpenedQuestionIds.has(questionId)) return;
|
|
3266
3320
|
autoOpenedQuestionIds.add(questionId);
|
|
3267
3321
|
// 直接弹出回答框等待作者选择;不会预填任何答案。
|
|
3268
|
-
openAiUserQuestionDialog(questionId).catch(() => undefined);
|
|
3322
|
+
openAiUserQuestionDialog(questionId, question).catch(() => undefined);
|
|
3269
3323
|
}
|
|
3270
3324
|
|
|
3271
3325
|
function questionsEndpoint(path) {
|
|
@@ -3388,22 +3442,7 @@ async function fetchAiUserQuestion(questionId) {
|
|
|
3388
3442
|
}
|
|
3389
3443
|
|
|
3390
3444
|
function aiQuestionItems(question) {
|
|
3391
|
-
|
|
3392
|
-
? question.questions
|
|
3393
|
-
: [{
|
|
3394
|
-
question: question?.question ?? "",
|
|
3395
|
-
options: question?.options ?? [],
|
|
3396
|
-
selectedOption: question?.selectedOption ?? null,
|
|
3397
|
-
customAnswer: question?.customAnswer ?? "",
|
|
3398
|
-
answerText: question?.answerText ?? "",
|
|
3399
|
-
isCustomAnswer: question?.isCustomAnswer === true
|
|
3400
|
-
}];
|
|
3401
|
-
return items.map((item, index) => ({
|
|
3402
|
-
...item,
|
|
3403
|
-
index,
|
|
3404
|
-
question: String(item?.question ?? ""),
|
|
3405
|
-
options: Array.isArray(item?.options) ? item.options : []
|
|
3406
|
-
}));
|
|
3445
|
+
return normalizeAiQuestionItems(question);
|
|
3407
3446
|
}
|
|
3408
3447
|
|
|
3409
3448
|
function resetAiQuestionDialogState(question) {
|
|
@@ -3444,6 +3483,14 @@ function syncAiQuestionAnswerCount() {
|
|
|
3444
3483
|
$("#ai-question-answer-count").textContent = `已填写 ${input.value.length} / ${maximum}`;
|
|
3445
3484
|
}
|
|
3446
3485
|
|
|
3486
|
+
function syncAiQuestionDismissState() {
|
|
3487
|
+
const closeButton = $("#ai-question-close");
|
|
3488
|
+
const canClose = aiQuestionDialogCanClose(currentAiQuestionDialogView);
|
|
3489
|
+
closeButton.disabled = !canClose;
|
|
3490
|
+
closeButton.classList.toggle("hidden", !canClose);
|
|
3491
|
+
$("#ai-question-dialog").dataset.dismissLocked = String(!canClose);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3447
3494
|
function renderAiUserQuestionOptions(question) {
|
|
3448
3495
|
const host = $("#ai-question-options");
|
|
3449
3496
|
const customInput = $("#ai-question-custom-answer");
|
|
@@ -3498,6 +3545,7 @@ function renderAiUserQuestionOptions(question) {
|
|
|
3498
3545
|
// 提交按钮由选择状态驱动:待回答且已选择(或输入)时才可提交。
|
|
3499
3546
|
if (isPending) syncAiQuestionSubmitState();
|
|
3500
3547
|
else $("#ai-question-submit").disabled = true;
|
|
3548
|
+
syncAiQuestionDismissState();
|
|
3501
3549
|
$("#ai-question-submit").textContent = items.length > 1 ? "提交全部回答" : "提交回答";
|
|
3502
3550
|
$("#ai-question-skip").disabled = !isPending;
|
|
3503
3551
|
$("#ai-question-expiry").textContent = isPending
|
|
@@ -3513,17 +3561,25 @@ async function refreshAiQuestionDialog() {
|
|
|
3513
3561
|
return question;
|
|
3514
3562
|
}
|
|
3515
3563
|
|
|
3516
|
-
async function openAiUserQuestionDialog(questionId) {
|
|
3564
|
+
async function openAiUserQuestionDialog(questionId, initialQuestion = null) {
|
|
3517
3565
|
aiQuestionDialogQuestionId = String(questionId);
|
|
3518
3566
|
const dialog = $("#ai-question-dialog");
|
|
3519
3567
|
if (!dialog.open) dialog.showModal();
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3568
|
+
if (initialQuestion?.id && String(initialQuestion.id) === aiQuestionDialogQuestionId) {
|
|
3569
|
+
currentAiQuestionDialogView = initialQuestion;
|
|
3570
|
+
resetAiQuestionDialogState(initialQuestion);
|
|
3571
|
+
renderAiUserQuestionOptions(initialQuestion);
|
|
3572
|
+
} else {
|
|
3573
|
+
currentAiQuestionDialogView = null;
|
|
3574
|
+
$("#ai-question-text").textContent = "";
|
|
3575
|
+
$("#ai-question-options").replaceChildren();
|
|
3576
|
+
$("#ai-question-custom-answer").value = "";
|
|
3577
|
+
$("#ai-question-navigation").classList.add("hidden");
|
|
3578
|
+
$("#ai-question-progress").textContent = "正在加载问题";
|
|
3579
|
+
syncAiQuestionAnswerCount();
|
|
3580
|
+
$("#ai-question-expiry").textContent = "正在加载问题……";
|
|
3581
|
+
syncAiQuestionDismissState();
|
|
3582
|
+
}
|
|
3527
3583
|
try {
|
|
3528
3584
|
return await refreshAiQuestionDialog();
|
|
3529
3585
|
} catch (error) {
|
|
@@ -3532,18 +3588,23 @@ async function openAiUserQuestionDialog(questionId) {
|
|
|
3532
3588
|
}
|
|
3533
3589
|
}
|
|
3534
3590
|
|
|
3535
|
-
function beginAiQuestionContinuationUi(conversationId) {
|
|
3591
|
+
async function beginAiQuestionContinuationUi(conversationId) {
|
|
3536
3592
|
const tab = conversationId ? aiChatTabManager.findByConversation(conversationId) : null;
|
|
3537
3593
|
if (!tab) return null;
|
|
3594
|
+
const workId = state.work.id;
|
|
3595
|
+
await aiRequestManager.whenIdle(tab.id);
|
|
3596
|
+
if (state.work?.id !== workId || !aiChatTabManager.get(tab.id)) throw createAiRequestAbortError("AI 请求目标已切换");
|
|
3597
|
+
const requestHolder = { snapshot: aiRequestManager.begin({ tabId: tab.id, workId, conversationId }) };
|
|
3538
3598
|
aiQuestionContinuationTabIds.add(tab.id);
|
|
3539
3599
|
setAiChatTabStatus(tab, "streaming");
|
|
3540
3600
|
if (isActiveAiChatTab(tab)) syncAiRequestControls();
|
|
3541
3601
|
scrollAiFeedToBottom(tab.feed);
|
|
3542
|
-
return { tab };
|
|
3602
|
+
return { tab, requestHolder };
|
|
3543
3603
|
}
|
|
3544
3604
|
|
|
3545
3605
|
function finishAiQuestionContinuationUi(continuationUi, failed = false) {
|
|
3546
3606
|
if (!continuationUi) return;
|
|
3607
|
+
aiRequestManager.finish(continuationUi.requestHolder.snapshot);
|
|
3547
3608
|
aiQuestionContinuationTabIds.delete(continuationUi.tab.id);
|
|
3548
3609
|
if (aiChatTabManager.get(continuationUi.tab.id)) setAiChatTabStatus(continuationUi.tab, failed ? "error" : "ready");
|
|
3549
3610
|
if (isActiveAiChatTab(continuationUi.tab)) syncAiRequestControls();
|
|
@@ -3557,7 +3618,7 @@ async function reloadAiQuestionConversation(conversationId) {
|
|
|
3557
3618
|
if (String(conversation.workId ?? "") !== String(state.work?.id ?? "")) throw new Error("AI 对话不属于当前作品");
|
|
3558
3619
|
upsertAiConversationSummary(conversation);
|
|
3559
3620
|
applyConversationToAiChatTab(tab, conversation);
|
|
3560
|
-
activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
3621
|
+
if (isActiveAiChatTab(tab)) activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
3561
3622
|
return conversation;
|
|
3562
3623
|
}
|
|
3563
3624
|
|
|
@@ -3577,9 +3638,13 @@ async function respondAiUserQuestion(questionId, payload) {
|
|
|
3577
3638
|
const conversationId = typeof knownQuestion?.conversationId === "string" ? knownQuestion.conversationId : null;
|
|
3578
3639
|
if (questionDialog.open) questionDialog.close();
|
|
3579
3640
|
if (approvalCenterDialog.open) approvalCenterDialog.close();
|
|
3580
|
-
continuationUi = beginAiQuestionContinuationUi(conversationId);
|
|
3641
|
+
continuationUi = await beginAiQuestionContinuationUi(conversationId);
|
|
3581
3642
|
let question;
|
|
3582
|
-
if (
|
|
3643
|
+
if (continuationUi) {
|
|
3644
|
+
const endpoint = questionsEndpoint(`/${encodeURIComponent(String(questionId))}/${payload.action === "reject" ? "reject" : "answer"}`);
|
|
3645
|
+
const streamed = await streamChat(continuationUi.requestHolder, payload.action === "reject" ? {} : { answers: payload.answers }, createAiIdempotencyKey(), { endpoint });
|
|
3646
|
+
question = streamed.question;
|
|
3647
|
+
} else if (payload.action === "reject") {
|
|
3583
3648
|
question = await api(questionsEndpoint(`/${encodeURIComponent(String(questionId))}/reject`), { method: "POST" });
|
|
3584
3649
|
} else {
|
|
3585
3650
|
question = await api(questionsEndpoint(`/${encodeURIComponent(String(questionId))}/answer`), {
|
|
@@ -3588,8 +3653,7 @@ async function respondAiUserQuestion(questionId, payload) {
|
|
|
3588
3653
|
});
|
|
3589
3654
|
}
|
|
3590
3655
|
cacheAiQuestionView(question);
|
|
3591
|
-
currentAiQuestionDialogView = question;
|
|
3592
|
-
aiQuestionDialogQuestionId = String(question.id);
|
|
3656
|
+
if (aiQuestionDialogQuestionId === String(question.id)) currentAiQuestionDialogView = question;
|
|
3593
3657
|
await reloadAiQuestionConversation(question.conversationId ?? conversationId);
|
|
3594
3658
|
toast(payload.action === "reject" ? "已跳过该提问批次,AI 已继续处理" : "全部回答已提交,AI 已继续处理");
|
|
3595
3659
|
return question;
|
|
@@ -3609,6 +3673,7 @@ async function respondAiUserQuestion(questionId, payload) {
|
|
|
3609
3673
|
} finally {
|
|
3610
3674
|
finishAiQuestionContinuationUi(continuationUi, continuationFailed);
|
|
3611
3675
|
aiQuestionDialogBusy = false;
|
|
3676
|
+
if (questionDialog.open && currentAiQuestionDialogView?.status === "pending") renderAiUserQuestionOptions(currentAiQuestionDialogView);
|
|
3612
3677
|
}
|
|
3613
3678
|
}
|
|
3614
3679
|
|
|
@@ -5895,6 +5960,15 @@ function formatAiFailureMessage(error) {
|
|
|
5895
5960
|
return lines.join("\n");
|
|
5896
5961
|
}
|
|
5897
5962
|
|
|
5963
|
+
function aiFailureMessageMetadata(error) {
|
|
5964
|
+
const details = error?.details && typeof error.details === "object" && !Array.isArray(error.details) ? error.details : {};
|
|
5965
|
+
return {
|
|
5966
|
+
...(typeof error?.code === "string" ? { errorCode: error.code.slice(0, 100) } : {}),
|
|
5967
|
+
...(Number.isInteger(error?.status) ? { errorStatus: error.status } : {}),
|
|
5968
|
+
...(typeof details.questionId === "string" ? { pendingQuestionId: details.questionId } : {})
|
|
5969
|
+
};
|
|
5970
|
+
}
|
|
5971
|
+
|
|
5898
5972
|
function isAgentToolCallLimitFailure(text) {
|
|
5899
5973
|
return /more than \d+ tool calls in one response cycle\./u.test(String(text ?? ""));
|
|
5900
5974
|
}
|
|
@@ -9293,8 +9367,8 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
9293
9367
|
function updateChapterStats() {
|
|
9294
9368
|
if (!state.chapter) return;
|
|
9295
9369
|
const text = $("#chapter-content").value;
|
|
9296
|
-
const count =
|
|
9297
|
-
$("#chapter-stats").textContent = `${count} 字 · v${state.chapter.versionNo}`;
|
|
9370
|
+
const count = countProseWords(text);
|
|
9371
|
+
$("#chapter-stats").textContent = `${count.toLocaleString("zh-CN")} 字 · v${state.chapter.versionNo}`;
|
|
9298
9372
|
}
|
|
9299
9373
|
|
|
9300
9374
|
function readReadingStorage(key) {
|
|
@@ -18277,6 +18351,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18277
18351
|
return;
|
|
18278
18352
|
}
|
|
18279
18353
|
const failureMessage = formatAiFailureMessage(error);
|
|
18354
|
+
const failureMetadata = aiFailureMessageMetadata(error);
|
|
18280
18355
|
let persistedFailureMessage = null;
|
|
18281
18356
|
try {
|
|
18282
18357
|
persistedFailureMessage = await persistAiConversationMessage(
|
|
@@ -18284,13 +18359,13 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18284
18359
|
"assistant",
|
|
18285
18360
|
failureMessage,
|
|
18286
18361
|
[],
|
|
18287
|
-
|
|
18362
|
+
failureMetadata,
|
|
18288
18363
|
{ requestId: aiAssistantRequestId(request) }
|
|
18289
18364
|
);
|
|
18290
18365
|
updateAiConversationSummaryFromMessage(persistedFailureMessage);
|
|
18291
18366
|
} catch { /* 主请求错误已显示,历史记录保存失败不覆盖原始错误 */ }
|
|
18292
18367
|
if (aiRequestTargetsCurrentState(request)) {
|
|
18293
|
-
appendMessage("assistant", failureMessage, [], persistedFailureMessage?.createdAt,
|
|
18368
|
+
appendMessage("assistant", failureMessage, [], persistedFailureMessage?.createdAt, failureMetadata, persistedFailureMessage?.id, { tab });
|
|
18294
18369
|
}
|
|
18295
18370
|
} finally {
|
|
18296
18371
|
aiRequestManager.finish(requestHolder.snapshot);
|
|
@@ -18313,7 +18388,7 @@ function renderAiStreamingCharacterProgress(meta, visibleCharacters) {
|
|
|
18313
18388
|
meta.replaceChildren("正在生成 · ", createAiStreamCharacterCount(visible), " 字");
|
|
18314
18389
|
}
|
|
18315
18390
|
|
|
18316
|
-
async function streamChat(requestHolder, body, idempotencyKey) {
|
|
18391
|
+
async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null } = {}) {
|
|
18317
18392
|
const tab = aiChatTabForRequest(requestHolder.snapshot);
|
|
18318
18393
|
if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
|
|
18319
18394
|
const feed = tab.feed;
|
|
@@ -18336,7 +18411,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18336
18411
|
window.clearInterval(streamConnectionTimer);
|
|
18337
18412
|
streamConnectionTimer = null;
|
|
18338
18413
|
};
|
|
18339
|
-
const streamConnectionEstablishedEvents = new Set(["delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
|
|
18414
|
+
const streamConnectionEstablishedEvents = new Set(["continuation", "delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
|
|
18340
18415
|
const streamSpeedController = createStreamTypewriterSpeedController();
|
|
18341
18416
|
let messageMounted = false;
|
|
18342
18417
|
const mountAssistantMessage = () => {
|
|
@@ -18366,12 +18441,14 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18366
18441
|
let persistedMessageCreatedAt = null;
|
|
18367
18442
|
let conversationTitle = null;
|
|
18368
18443
|
let writingSuggestion = null;
|
|
18444
|
+
let question = null;
|
|
18369
18445
|
let persistedUserMessage = null;
|
|
18370
18446
|
let contextAction = "ready";
|
|
18371
18447
|
let warningOnly = false;
|
|
18372
18448
|
let streamContextCompacted = false;
|
|
18373
18449
|
const processStartedAt = Date.now();
|
|
18374
|
-
|
|
18450
|
+
let previousProcessDurationMs = 0;
|
|
18451
|
+
const elapsedProcessTime = () => previousProcessDurationMs + Math.max(0, Date.now() - processStartedAt);
|
|
18375
18452
|
const processStepTypewriters = new Map();
|
|
18376
18453
|
const processStepVisibleContents = new Map();
|
|
18377
18454
|
const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
|
|
@@ -18400,7 +18477,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18400
18477
|
};
|
|
18401
18478
|
try {
|
|
18402
18479
|
const request = assertAiRequestCurrent(requestHolder.snapshot);
|
|
18403
|
-
const response = await fetch(`/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
|
|
18480
|
+
const response = await fetch(endpoint ?? `/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
|
|
18404
18481
|
method: "POST",
|
|
18405
18482
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream", "X-CSRF-Token": state.csrfToken, "Idempotency-Key": idempotencyKey },
|
|
18406
18483
|
body: JSON.stringify(body),
|
|
@@ -18415,7 +18492,23 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18415
18492
|
const consume = async (eventName, payload) => {
|
|
18416
18493
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18417
18494
|
if (streamConnectionEstablishedEvents.has(eventName)) stopStreamConnectionTimer();
|
|
18418
|
-
if (eventName === "
|
|
18495
|
+
if (eventName === "continuation") {
|
|
18496
|
+
if (String(payload.conversationId ?? "") !== requestHolder.snapshot.conversationId) throw new Error("流式续接返回了其他对话");
|
|
18497
|
+
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : [];
|
|
18498
|
+
processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : [];
|
|
18499
|
+
previousProcessDurationMs = Math.max(0, Number(payload.processDurationMs) || 0);
|
|
18500
|
+
const previousMessage = [...feed.querySelectorAll(".assistant-message[data-message-id]")]
|
|
18501
|
+
.find((candidate) => candidate.dataset.messageId === String(payload.messageId));
|
|
18502
|
+
if (previousMessage) {
|
|
18503
|
+
attachMessageHeading(message, aiAssistantLabel("正在生成", tab.roleplayCharacter), undefined, tab);
|
|
18504
|
+
previousMessage.replaceWith(message);
|
|
18505
|
+
messageMounted = true;
|
|
18506
|
+
} else mountAssistantMessage();
|
|
18507
|
+
attachMessageIdentity(message, payload.messageId);
|
|
18508
|
+
renderStreamingProcessSteps(false);
|
|
18509
|
+
meta.textContent = "已收到回答,正在继续思考与执行……";
|
|
18510
|
+
scrollAiFeedToBottom(feed);
|
|
18511
|
+
} else if (eventName === "context") {
|
|
18419
18512
|
contextAction = typeof payload.action === "string" ? payload.action : "ready";
|
|
18420
18513
|
if (!tab.promptSent) setAiChatTabContextUsage(tab, payload.usage);
|
|
18421
18514
|
if (payload.conversation?.id) {
|
|
@@ -18512,6 +18605,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18512
18605
|
setAiChatTabContextUsage(tab, payload.contextUsage);
|
|
18513
18606
|
if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
|
|
18514
18607
|
} else if (eventName === "complete") {
|
|
18608
|
+
question = payload.question ?? null;
|
|
18515
18609
|
if (payload.warningOnly === true) {
|
|
18516
18610
|
warningOnly = true;
|
|
18517
18611
|
setAiChatTabContextUsage(tab, payload.contextUsage);
|
|
@@ -18568,7 +18662,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
18568
18662
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18569
18663
|
if (streamError) throw streamError;
|
|
18570
18664
|
assertAiStreamCompleted(streamCompleted);
|
|
18571
|
-
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, writingSuggestion, userMessage: persistedUserMessage };
|
|
18665
|
+
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, writingSuggestion, userMessage: persistedUserMessage, question };
|
|
18572
18666
|
} catch (error) {
|
|
18573
18667
|
const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
|
|
18574
18668
|
const interruptionCode = typeof streamFailure.code === "string" ? streamFailure.code.slice(0, 100) : "AI_STREAM_FAILED";
|
|
@@ -18654,6 +18748,9 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
18654
18748
|
const isInterrupted = role === "assistant" && metadata?.interrupted === true;
|
|
18655
18749
|
const interruptionCode = typeof metadata?.interruptionCode === "string" ? metadata.interruptionCode : "AI_STREAM_FAILED";
|
|
18656
18750
|
message.className = `${role === "user" ? "user-message" : "assistant-message"}${isFailure || isInterrupted ? " is-error" : ""}`;
|
|
18751
|
+
const errorCode = isFailure ? aiFailureCode(text, metadata) : "";
|
|
18752
|
+
if (errorCode) message.dataset.errorCode = errorCode;
|
|
18753
|
+
if (isFailure && typeof metadata?.pendingQuestionId === "string") message.dataset.pendingQuestionId = metadata.pendingQuestionId;
|
|
18657
18754
|
const parsedUserTurn = role === "user" ? parseRoleplayUserTurn(text) : null;
|
|
18658
18755
|
const messageBody = isFailure
|
|
18659
18756
|
? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
|
|
@@ -18691,11 +18788,20 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
18691
18788
|
scene.append(sceneLabel, sceneBody);
|
|
18692
18789
|
message.querySelector(".message-body")?.prepend(scene);
|
|
18693
18790
|
}
|
|
18791
|
+
const chapterReferences = state.work?.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
|
|
18792
|
+
id: chapter.id,
|
|
18793
|
+
name: `${volume.title} / ${chapter.title}`
|
|
18794
|
+
}))) ?? [];
|
|
18795
|
+
const settingReferences = state.settings.map((setting) => ({ id: setting.id, name: setting.title }));
|
|
18796
|
+
const contextSettingReferences = [{ id: "include-setting-info", name: "注入上下文设定" }];
|
|
18694
18797
|
const mentionGroups = role === "user"
|
|
18695
18798
|
? [
|
|
18696
18799
|
["角色", metadata?.mentionCharacterIds, state.characters],
|
|
18697
18800
|
["种族", metadata?.mentionRaceIds, state.races],
|
|
18698
|
-
["组织", metadata?.mentionOrganizationIds, state.organizations]
|
|
18801
|
+
["组织", metadata?.mentionOrganizationIds, state.organizations],
|
|
18802
|
+
["设定", metadata?.mentionSettingIds, settingReferences],
|
|
18803
|
+
["章节", metadata?.mentionChapterIds, chapterReferences],
|
|
18804
|
+
["能力", metadata?.mentionContextSettingIds, contextSettingReferences]
|
|
18699
18805
|
]
|
|
18700
18806
|
.flatMap(([kind, ids, items]) => userMessageMentionNames(ids, items).map((name) => ({ kind, name })))
|
|
18701
18807
|
: [];
|
|
@@ -18793,7 +18899,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
18793
18899
|
function continuationGuardMarkup(guard) {
|
|
18794
18900
|
if (!guard) return "";
|
|
18795
18901
|
const issues = Array.isArray(guard.issues) ? guard.issues : [];
|
|
18796
|
-
|
|
18902
|
+
const failure = typeof guard.failure === "string" && guard.failure.trim()
|
|
18903
|
+
? guard.failure.trim()
|
|
18904
|
+
: "无法完成检查,请谨慎采纳";
|
|
18905
|
+
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>`;
|
|
18797
18906
|
}
|
|
18798
18907
|
|
|
18799
18908
|
async function applyAcceptedWritingSuggestion(message, suggestion) {
|
|
@@ -21282,7 +21391,15 @@ $("#ai-write-plan-undo").addEventListener("click", () => {
|
|
|
21282
21391
|
if (!aiWritePlanDialogPlanId) return;
|
|
21283
21392
|
undoAiWritePlan(aiWritePlanDialogPlanId);
|
|
21284
21393
|
});
|
|
21285
|
-
$("#ai-question-close").addEventListener("click", () =>
|
|
21394
|
+
$("#ai-question-close").addEventListener("click", () => {
|
|
21395
|
+
if (!aiQuestionDialogCanClose(currentAiQuestionDialogView)) return;
|
|
21396
|
+
$("#ai-question-dialog").close();
|
|
21397
|
+
});
|
|
21398
|
+
$("#ai-question-dialog").addEventListener("cancel", (event) => {
|
|
21399
|
+
if (aiQuestionDialogCanClose(currentAiQuestionDialogView)) return;
|
|
21400
|
+
event.preventDefault();
|
|
21401
|
+
toast("请先提交全部回答,或选择“暂不回答”让 AI 继续", "warning");
|
|
21402
|
+
});
|
|
21286
21403
|
|
|
21287
21404
|
function syncAiQuestionSubmitState() {
|
|
21288
21405
|
const submit = $("#ai-question-submit");
|