@musnows/scriverse 0.9.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-write-plans.js +136 -48
- package/dist/ai-write-plans.js.map +1 -1
- package/dist/ai.js +404 -100
- package/dist/ai.js.map +1 -1
- package/dist/app.js +366 -2
- package/dist/app.js.map +1 -1
- package/dist/cli-core.js +95 -3
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +549 -4
- package/dist/database.js.map +1 -1
- package/dist/im-orchestrator.js +1317 -0
- package/dist/im-orchestrator.js.map +1 -0
- package/dist/im.js +1690 -0
- package/dist/im.js.map +1 -0
- package/dist/public/ai-interactive.js +14 -13
- package/dist/public/app.js +279 -82
- package/dist/public/im.d.ts +71 -0
- package/dist/public/im.js +1899 -0
- package/dist/public/index.html +108 -5
- package/dist/public/page-route.d.ts +2 -1
- package/dist/public/page-route.js +4 -1
- package/dist/public/styles.css +335 -3
- package/dist/s3-backup.js +5 -1
- package/dist/s3-backup.js.map +1 -1
- package/dist/security.js +1 -0
- package/dist/security.js.map +1 -1
- package/dist/store.js +4 -1
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +9 -4
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/ai-write-plans.js
CHANGED
|
@@ -50,7 +50,7 @@ export const aiWriteToolDescriptions = {
|
|
|
50
50
|
outlines: "允许侧边栏 AI 编辑章节大纲以及创建或编辑伏笔(不能删除)。",
|
|
51
51
|
annotations: "允许侧边栏 AI 复用现有批注能力,在正文指定位置创建评论或待办。",
|
|
52
52
|
analysis_tasks: "允许侧边栏 AI 触发已有类型的分析任务进入现有队列。",
|
|
53
|
-
ask_user_questions: "允许侧边栏 AI 通过 AskUserQuestions
|
|
53
|
+
ask_user_questions: "允许侧边栏 AI 通过 AskUserQuestions 向用户批量提出单选问题。"
|
|
54
54
|
};
|
|
55
55
|
/** 全部关闭时的默认开关状态。 */
|
|
56
56
|
export function defaultAiWriteToolToggles() {
|
|
@@ -388,22 +388,43 @@ export const createAiWritePlanInputSchema = z.object({
|
|
|
388
388
|
aiSummary: z.string().trim().min(1).max(2000),
|
|
389
389
|
operations: z.unknown()
|
|
390
390
|
}).strict();
|
|
391
|
-
/**
|
|
391
|
+
/** 单次工具调用可批量提出 1-5 个问题,每题包含 2-6 个预设选项。 */
|
|
392
|
+
export const MIN_AI_QUESTIONS_PER_CALL = 1;
|
|
393
|
+
export const MAX_AI_QUESTIONS_PER_CALL = 5;
|
|
392
394
|
export const MIN_AI_QUESTION_OPTIONS = 2;
|
|
393
395
|
export const MAX_AI_QUESTION_OPTIONS = 6;
|
|
394
396
|
export const MAX_AI_QUESTION_ANSWER_CHARS = 3000;
|
|
395
|
-
|
|
397
|
+
const aiUserQuestionItemSchema = z.object({
|
|
396
398
|
question: z.string().trim().min(1).max(2000),
|
|
397
399
|
options: z.array(z.string().trim().min(1).max(200))
|
|
398
400
|
.min(MIN_AI_QUESTION_OPTIONS)
|
|
399
401
|
.max(MAX_AI_QUESTION_OPTIONS)
|
|
400
402
|
}).strict();
|
|
401
|
-
export const
|
|
403
|
+
export const askAiUserQuestionInputSchema = z.union([
|
|
404
|
+
z.object({
|
|
405
|
+
questions: z.array(aiUserQuestionItemSchema)
|
|
406
|
+
.min(MIN_AI_QUESTIONS_PER_CALL)
|
|
407
|
+
.max(MAX_AI_QUESTIONS_PER_CALL)
|
|
408
|
+
}).strict(),
|
|
409
|
+
aiUserQuestionItemSchema
|
|
410
|
+
]).transform((input) => "questions" in input ? input : { questions: [input] });
|
|
411
|
+
const aiUserQuestionAnswerSchema = z.object({
|
|
402
412
|
selectedOption: z.number().int().min(0).optional(),
|
|
403
|
-
customAnswer: z.string().trim()
|
|
413
|
+
customAnswer: z.string().trim()
|
|
414
|
+
.min(1, "自定义回答不能为空")
|
|
415
|
+
.max(MAX_AI_QUESTION_ANSWER_CHARS, `自定义回答不能超过 ${MAX_AI_QUESTION_ANSWER_CHARS} 个字符`)
|
|
416
|
+
.optional()
|
|
404
417
|
}).strict().refine(
|
|
405
418
|
// 至少提供一种回答;选择预设项后仍可附带自定义补充信息。
|
|
406
419
|
(input) => input.selectedOption !== undefined || input.customAnswer !== undefined, { message: "必须选择预设选项或填写自定义回答" });
|
|
420
|
+
export const answerAiUserQuestionSchema = z.union([
|
|
421
|
+
z.object({
|
|
422
|
+
answers: z.array(aiUserQuestionAnswerSchema)
|
|
423
|
+
.min(MIN_AI_QUESTIONS_PER_CALL)
|
|
424
|
+
.max(MAX_AI_QUESTIONS_PER_CALL)
|
|
425
|
+
}).strict(),
|
|
426
|
+
aiUserQuestionAnswerSchema
|
|
427
|
+
]).transform((input) => "answers" in input ? input : { answers: [input] });
|
|
407
428
|
// ---------------------------------------------------------------------------
|
|
408
429
|
// 标签与展示辅助
|
|
409
430
|
// ---------------------------------------------------------------------------
|
|
@@ -897,17 +918,76 @@ function planInputError(index, error) {
|
|
|
897
918
|
const firstIssue = error.issues[0];
|
|
898
919
|
return new AppError(400, "AI_PLAN_OPERATION_INVALID", `第 ${index + 1} 个操作的 ${firstIssue?.path.join(".") || "input"} 字段无效:${firstIssue?.message ?? "输入不符合要求"}`);
|
|
899
920
|
}
|
|
900
|
-
function
|
|
901
|
-
const
|
|
902
|
-
const
|
|
921
|
+
function storedQuestionDefinitions(row) {
|
|
922
|
+
const stored = json(row.questions_json, []);
|
|
923
|
+
const questions = stored.flatMap((value) => {
|
|
924
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
925
|
+
return [];
|
|
926
|
+
const record = value;
|
|
927
|
+
const question = typeof record.question === "string" ? record.question.trim() : "";
|
|
928
|
+
const options = Array.isArray(record.options)
|
|
929
|
+
? record.options.filter((option) => typeof option === "string" && option.trim().length > 0)
|
|
930
|
+
: [];
|
|
931
|
+
return question && options.length >= MIN_AI_QUESTION_OPTIONS ? [{ question, options }] : [];
|
|
932
|
+
});
|
|
933
|
+
if (questions.length > 0)
|
|
934
|
+
return questions;
|
|
935
|
+
return [{ question: row.question, options: json(row.options_json, []) }];
|
|
936
|
+
}
|
|
937
|
+
function resolveQuestionAnswer(options, selectedOption, customAnswer) {
|
|
903
938
|
const selectedOptionLabel = selectedOption !== null && selectedOption >= 0 && selectedOption < options.length
|
|
904
939
|
? options[selectedOption] ?? null
|
|
905
940
|
: null;
|
|
906
|
-
const customAnswer = row.is_custom_answer === 1 ? row.answer_text : "";
|
|
907
941
|
const answerText = selectedOptionLabel
|
|
908
942
|
? (customAnswer ? `${selectedOptionLabel}\n补充信息:${customAnswer}` : selectedOptionLabel)
|
|
909
|
-
:
|
|
910
|
-
return {
|
|
943
|
+
: customAnswer;
|
|
944
|
+
return {
|
|
945
|
+
selectedOption,
|
|
946
|
+
selectedOptionLabel,
|
|
947
|
+
customAnswer,
|
|
948
|
+
answerText,
|
|
949
|
+
isCustomAnswer: customAnswer.length > 0
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function resolvedQuestionAnswers(row) {
|
|
953
|
+
const questions = storedQuestionDefinitions(row);
|
|
954
|
+
const storedAnswers = json(row.answers_json, []);
|
|
955
|
+
return questions.map((definition, index) => {
|
|
956
|
+
const value = storedAnswers[index];
|
|
957
|
+
const record = value && typeof value === "object" && !Array.isArray(value)
|
|
958
|
+
? value
|
|
959
|
+
: null;
|
|
960
|
+
const storedSelectedOption = record?.selectedOption;
|
|
961
|
+
const selectedOption = Number.isInteger(storedSelectedOption)
|
|
962
|
+
? Number(storedSelectedOption)
|
|
963
|
+
: index === 0 && row.selected_option !== null ? Number(row.selected_option) : null;
|
|
964
|
+
const customAnswer = typeof record?.customAnswer === "string"
|
|
965
|
+
? record.customAnswer
|
|
966
|
+
: index === 0 && row.is_custom_answer === 1 ? row.answer_text : "";
|
|
967
|
+
return {
|
|
968
|
+
index,
|
|
969
|
+
question: definition.question,
|
|
970
|
+
options: definition.options.map((label, optionIndex) => ({ index: optionIndex, label, recommended: optionIndex === 0 })),
|
|
971
|
+
...resolveQuestionAnswer(definition.options, selectedOption, customAnswer)
|
|
972
|
+
};
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
function combinedQuestionAnswerText(questions) {
|
|
976
|
+
const answered = questions.filter((question) => question.answerText);
|
|
977
|
+
if (answered.length === 0)
|
|
978
|
+
return "";
|
|
979
|
+
if (questions.length === 1)
|
|
980
|
+
return questions[0]?.answerText ?? "";
|
|
981
|
+
return answered.map((question) => `问题 ${question.index + 1}:${question.question}\n回答:${question.answerText}`).join("\n\n");
|
|
982
|
+
}
|
|
983
|
+
function firstQuestionAnswer(row) {
|
|
984
|
+
const answer = resolvedQuestionAnswers(row)[0];
|
|
985
|
+
return {
|
|
986
|
+
selectedOption: answer?.selectedOption ?? null,
|
|
987
|
+
selectedOptionLabel: answer?.selectedOptionLabel ?? null,
|
|
988
|
+
customAnswer: answer?.customAnswer ?? "",
|
|
989
|
+
answerText: answer?.answerText ?? ""
|
|
990
|
+
};
|
|
911
991
|
}
|
|
912
992
|
export class AiWritePlanManager {
|
|
913
993
|
database;
|
|
@@ -1972,7 +2052,10 @@ export class AiWritePlanManager {
|
|
|
1972
2052
|
// --------------------------------------------------------------- 用户提问
|
|
1973
2053
|
createQuestion(input) {
|
|
1974
2054
|
this.assertToolEnabled(input.workId, "ask_user_questions");
|
|
1975
|
-
const parsed = askAiUserQuestionInputSchema.parse(
|
|
2055
|
+
const parsed = askAiUserQuestionInputSchema.parse(input.questions === undefined
|
|
2056
|
+
? { question: input.question, options: input.options }
|
|
2057
|
+
: { questions: input.questions });
|
|
2058
|
+
const firstQuestion = parsed.questions[0];
|
|
1976
2059
|
const questionId = randomId("aiQ");
|
|
1977
2060
|
const timestamp = now();
|
|
1978
2061
|
const expiresAt = isoFromNow(timestamp, this.questionTtlMs);
|
|
@@ -1984,47 +2067,43 @@ export class AiWritePlanManager {
|
|
|
1984
2067
|
throw new AppError(409, "AI_QUESTION_PENDING", "当前对话已有一个待回答问题");
|
|
1985
2068
|
this.database.run(`INSERT INTO ai_user_questions (
|
|
1986
2069
|
id, work_id, conversation_id, initiator_user_id, recipient_user_id, question,
|
|
1987
|
-
options_json, status, tool_call_id, created_at, expires_at
|
|
1988
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`, questionId, input.workId, input.conversationId, input.initiator?.userId ?? null, input.recipientUserId,
|
|
2070
|
+
options_json, questions_json, status, tool_call_id, created_at, expires_at
|
|
2071
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`, questionId, input.workId, input.conversationId, input.initiator?.userId ?? null, input.recipientUserId, firstQuestion.question, JSON.stringify(firstQuestion.options), JSON.stringify(parsed.questions), input.toolCallId ?? null, timestamp, expiresAt);
|
|
1989
2072
|
});
|
|
1990
2073
|
this.store.audit(input.workId, "ai.question.asked", "ai_user_question", questionId, {
|
|
1991
2074
|
conversationId: input.conversationId,
|
|
1992
|
-
askedBy: input.initiator?.userId ?? null
|
|
2075
|
+
askedBy: input.initiator?.userId ?? null,
|
|
2076
|
+
questionCount: parsed.questions.length
|
|
1993
2077
|
});
|
|
1994
2078
|
return this.getQuestion(questionId, input.workId, input.initiator);
|
|
1995
2079
|
}
|
|
1996
2080
|
answerQuestion(questionId, workId, respondent, payload) {
|
|
1997
2081
|
const row = this.assertAnswerable(questionId, workId, respondent);
|
|
1998
|
-
const
|
|
1999
|
-
const
|
|
2000
|
-
if (
|
|
2001
|
-
throw new AppError(400, "
|
|
2002
|
-
}
|
|
2003
|
-
if (customAnswer.length > MAX_AI_QUESTION_ANSWER_CHARS) {
|
|
2004
|
-
throw new AppError(400, "AI_QUESTION_CUSTOM_ANSWER_TOO_LONG", `自定义回答不能超过 ${MAX_AI_QUESTION_ANSWER_CHARS} 个字符`);
|
|
2082
|
+
const questions = storedQuestionDefinitions(row);
|
|
2083
|
+
const parsed = answerAiUserQuestionSchema.parse(payload);
|
|
2084
|
+
if (parsed.answers.length !== questions.length) {
|
|
2085
|
+
throw new AppError(400, "AI_QUESTION_ANSWERS_INCOMPLETE", `必须一次提交全部 ${questions.length} 个问题的回答`);
|
|
2005
2086
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
if (
|
|
2010
|
-
throw new AppError(400, "AI_QUESTION_OPTION_INVALID",
|
|
2087
|
+
const answers = parsed.answers.map((answer, index) => {
|
|
2088
|
+
const options = questions[index].options;
|
|
2089
|
+
const selectedOption = answer.selectedOption ?? null;
|
|
2090
|
+
if (selectedOption !== null && selectedOption >= options.length) {
|
|
2091
|
+
throw new AppError(400, "AI_QUESTION_OPTION_INVALID", `第 ${index + 1} 个问题的选项编号无效`);
|
|
2011
2092
|
}
|
|
2012
|
-
selectedOption
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
throw new AppError(400, "AI_QUESTION_ANSWER_REQUIRED", "必须提供回答");
|
|
2017
|
-
const isCustom = Boolean(customAnswer);
|
|
2018
|
-
const storedAnswerText = customAnswer || selectedOptionLabel;
|
|
2093
|
+
return { selectedOption, customAnswer: answer.customAnswer ?? "" };
|
|
2094
|
+
});
|
|
2095
|
+
const firstAnswer = answers[0];
|
|
2096
|
+
const firstResolved = resolveQuestionAnswer(questions[0].options, firstAnswer.selectedOption, firstAnswer.customAnswer);
|
|
2019
2097
|
const updated = this.database.run(`UPDATE ai_user_questions
|
|
2020
|
-
SET status = 'answered', selected_option = ?, answer_text = ?, is_custom_answer = ?, decided_at = ?
|
|
2021
|
-
WHERE id = ? AND status = 'pending'`, selectedOption,
|
|
2098
|
+
SET status = 'answered', selected_option = ?, answer_text = ?, is_custom_answer = ?, answers_json = ?, decided_at = ?
|
|
2099
|
+
WHERE id = ? AND status = 'pending'`, firstAnswer.selectedOption, firstAnswer.customAnswer || firstResolved.selectedOptionLabel || "", firstAnswer.customAnswer ? 1 : 0, JSON.stringify(answers), now(), questionId);
|
|
2022
2100
|
if (updated.changes !== 1)
|
|
2023
2101
|
throw new AppError(409, "AI_QUESTION_ALREADY_DECIDED", "该问题已被处理");
|
|
2024
2102
|
this.store.audit(row.work_id, "ai.question.answered", "ai_user_question", questionId, {
|
|
2025
2103
|
answeredBy: respondent?.userId ?? null,
|
|
2026
|
-
|
|
2027
|
-
|
|
2104
|
+
questionCount: questions.length,
|
|
2105
|
+
customAnswerCount: answers.filter((answer) => answer.customAnswer).length,
|
|
2106
|
+
selectedOptionCount: answers.filter((answer) => answer.selectedOption !== null).length
|
|
2028
2107
|
});
|
|
2029
2108
|
return this.getQuestion(questionId, workId, respondent);
|
|
2030
2109
|
}
|
|
@@ -2101,7 +2180,8 @@ export class AiWritePlanManager {
|
|
|
2101
2180
|
const claimed = this.database.run("UPDATE ai_user_questions SET resume_state = 'claimed', resumed_at = ? WHERE id = ? AND resume_state = 'pending' AND status IN ('answered', 'rejected', 'expired')", now(), questionId);
|
|
2102
2181
|
if (claimed.changes !== 1)
|
|
2103
2182
|
return null;
|
|
2104
|
-
const
|
|
2183
|
+
const questionView = this.toQuestionView(row);
|
|
2184
|
+
const answer = firstQuestionAnswer(row);
|
|
2105
2185
|
return {
|
|
2106
2186
|
...continuation,
|
|
2107
2187
|
questionId,
|
|
@@ -2111,7 +2191,13 @@ export class AiWritePlanManager {
|
|
|
2111
2191
|
selectedOptionLabel: answer.selectedOptionLabel,
|
|
2112
2192
|
customAnswer: answer.customAnswer,
|
|
2113
2193
|
toolCallId: row.tool_call_id,
|
|
2114
|
-
|
|
2194
|
+
answers: questionView.questions.map((question) => ({
|
|
2195
|
+
question: question.question,
|
|
2196
|
+
answer: question.answerText,
|
|
2197
|
+
selectedOption: question.selectedOptionLabel,
|
|
2198
|
+
supplementalAnswer: question.customAnswer || null
|
|
2199
|
+
})),
|
|
2200
|
+
questionView
|
|
2115
2201
|
};
|
|
2116
2202
|
}
|
|
2117
2203
|
finishQuestionContinuation(questionId, result, failed = false) {
|
|
@@ -2151,21 +2237,23 @@ export class AiWritePlanManager {
|
|
|
2151
2237
|
return this.database.get("SELECT * FROM ai_user_questions WHERE id = ?", row.id);
|
|
2152
2238
|
}
|
|
2153
2239
|
toQuestionView(row) {
|
|
2154
|
-
const
|
|
2155
|
-
const
|
|
2240
|
+
const questions = resolvedQuestionAnswers(row);
|
|
2241
|
+
const firstQuestion = questions[0];
|
|
2156
2242
|
return {
|
|
2157
2243
|
id: row.id,
|
|
2158
2244
|
workId: row.work_id,
|
|
2159
2245
|
conversationId: row.conversation_id,
|
|
2160
2246
|
question: row.question,
|
|
2247
|
+
questionCount: questions.length,
|
|
2248
|
+
questions,
|
|
2161
2249
|
status: row.status,
|
|
2162
2250
|
statusLabel: aiQuestionStatusLabels[row.status] ?? row.status,
|
|
2163
|
-
options: options
|
|
2164
|
-
selectedOption:
|
|
2165
|
-
selectedOptionLabel:
|
|
2166
|
-
customAnswer:
|
|
2167
|
-
answerText:
|
|
2168
|
-
isCustomAnswer:
|
|
2251
|
+
options: firstQuestion.options,
|
|
2252
|
+
selectedOption: firstQuestion.selectedOption,
|
|
2253
|
+
selectedOptionLabel: firstQuestion.selectedOptionLabel,
|
|
2254
|
+
customAnswer: firstQuestion.customAnswer,
|
|
2255
|
+
answerText: combinedQuestionAnswerText(questions),
|
|
2256
|
+
isCustomAnswer: firstQuestion.isCustomAnswer,
|
|
2169
2257
|
createdAt: row.created_at,
|
|
2170
2258
|
expiresAt: row.expires_at,
|
|
2171
2259
|
decidedAt: row.decided_at,
|