@musnows/scriverse 0.9.8 → 0.9.10

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.
@@ -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
- /** 提问选项数量限制:一次一个问题和 2-6 个预设选项。 */
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
- export const askAiUserQuestionInputSchema = z.object({
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 answerAiUserQuestionSchema = z.object({
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().min(1).max(MAX_AI_QUESTION_ANSWER_CHARS).optional()
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 resolvedQuestionAnswer(row) {
901
- const options = json(row.options_json, []);
902
- const selectedOption = row.selected_option === null ? null : Number(row.selected_option);
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
- : (customAnswer || row.answer_text);
910
- return { selectedOption, selectedOptionLabel, customAnswer, answerText };
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({ question: input.question, options: input.options });
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, parsed.question, JSON.stringify(parsed.options), input.toolCallId ?? null, timestamp, expiresAt);
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 options = json(row.options_json, []);
1999
- const customAnswer = payload.customAnswer?.trim() ?? "";
2000
- if (payload.customAnswer !== undefined && !customAnswer) {
2001
- throw new AppError(400, "AI_QUESTION_CUSTOM_ANSWER_INVALID", "自定义回答不能为空");
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
- let selectedOption = null;
2007
- let selectedOptionLabel = "";
2008
- if (payload.selectedOption !== undefined) {
2009
- if (payload.selectedOption < 0 || payload.selectedOption >= options.length) {
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 = payload.selectedOption;
2013
- selectedOptionLabel = options[selectedOption] ?? "";
2014
- }
2015
- if (selectedOption === null && !customAnswer)
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, storedAnswerText, isCustom ? 1 : 0, now(), questionId);
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
- isCustomAnswer: isCustom,
2027
- hasSelectedOption: selectedOption !== null
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 answer = resolvedQuestionAnswer(row);
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
- questionView: this.toQuestionView(row)
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 options = json(row.options_json, []);
2155
- const answer = resolvedQuestionAnswer(row);
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.map((label, index) => ({ index, label, recommended: index === 0 })),
2164
- selectedOption: answer.selectedOption,
2165
- selectedOptionLabel: answer.selectedOptionLabel,
2166
- customAnswer: answer.customAnswer,
2167
- answerText: answer.answerText,
2168
- isCustomAnswer: row.is_custom_answer === 1,
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,