Просмотр исходного кода

feat(chapter): 过短失败时把模型原文写入聊天记录

字数门禁只报「约 N 字」时无法判断是拒写还是空输出。
仅在任务书/初稿/扩写低于完成线时写入 result 与 params.preview。

Co-authored-by: Cursor <cursoragent@cursor.com>
darknessomi 13 часов назад
Родитель
Сommit
cd56c01993

+ 20 - 8
src/lib/novel/deep-chapter-generation.spec.ts

@@ -2149,24 +2149,30 @@ describe("runDeepChapterGeneration", () => {
         callbacks.onDone()
       }),
     }
-    const events: Array<{ type: string; name: string; result?: string }> = []
+    const events: Array<{ type: string; name: string; result?: string; params?: Record<string, unknown> }> = []
 
     await expect(runDeepChapterGeneration(
       { projectPath: "E:/Novel", userRequest: "生成第239章", chapterNumber: 239, llmConfig },
       { onWorkflowEvent: (event) => events.push(event) },
       deps,
-    )).rejects.toThrow(/扩写后仅约 .*低于最低完成线/)
+    )).rejects.toThrow(/扩写后仅约 .*低于最低完成线[\s\S]*原文:当前资料不足,无法扩写。/)
 
     expect(deps.reviewChapter).not.toHaveBeenCalled()
     expect(events).toContainEqual(expect.objectContaining({
       type: "completed",
       name: "chapter_draft",
-      result: expect.stringContaining("低于最低完成线"),
+      result: expect.stringContaining("原文:请提供第239章章纲后再继续。"),
+      params: expect.objectContaining({
+        preview: "请提供第239章章纲后再继续。",
+      }),
     }))
     expect(events).toContainEqual(expect.objectContaining({
       type: "error",
       name: "chapter_expansion",
-      result: expect.stringContaining("章节正文生成失败"),
+      result: expect.stringContaining("原文:当前资料不足,无法扩写。"),
+      params: expect.objectContaining({
+        preview: "当前资料不足,无法扩写。",
+      }),
     }))
     expect(events).not.toContainEqual(expect.objectContaining({
       type: "completed",
@@ -2447,24 +2453,30 @@ describe("runDeepChapterGeneration", () => {
         callbacks.onDone()
       }),
     }
-    const events: Array<{ type: string; name: string; result?: string }> = []
+    const events: Array<{ type: string; name: string; result?: string; params?: Record<string, unknown> }> = []
 
     await expect(runDeepChapterGeneration(
       { projectPath: "E:/Novel", userRequest: "生成第3章", chapterNumber: 3, llmConfig },
       { onWorkflowEvent: (event) => events.push(event) },
       deps,
-    )).rejects.toThrow(/重生成后仅约 .*低于最低完成线/)
+    )).rejects.toThrow(/重生成后仅约 .*低于最低完成线[\s\S]*原文:无法生成。/)
 
     expect(deps.reviewChapter).not.toHaveBeenCalled()
     expect(events).toContainEqual(expect.objectContaining({
       type: "completed",
       name: "chapter_task_brief",
-      result: expect.stringContaining("进入重新生成"),
+      result: expect.stringContaining("原文:无法生成。"),
+      params: expect.objectContaining({
+        preview: "无法生成。",
+      }),
     }))
     expect(events).toContainEqual(expect.objectContaining({
       type: "error",
       name: "chapter_task_brief_retry",
-      result: expect.stringContaining("写作任务书生成失败"),
+      result: expect.stringContaining("原文:无法生成。"),
+      params: expect.objectContaining({
+        preview: "无法生成。",
+      }),
     }))
     expect(events).not.toContainEqual(expect.objectContaining({
       name: "chapter_draft",

+ 48 - 6
src/lib/novel/deep-chapter-generation.ts

@@ -404,6 +404,10 @@ function errorChapterWorkflowStep(
 ): void {
   emitChapterWorkflowEvent(callbacks, "error", spec, {
     result: getErrorMessage(error),
+    params:
+      error instanceof ShortModelOutputError
+        ? { chars: error.chars, preview: error.preview }
+        : undefined,
   });
 }
 
@@ -871,10 +875,13 @@ export async function runDeepChapterGeneration(
       (value) => {
         const chars = countChapterChars(value);
         return chars < DEEP_CHAPTER_BRIEF_MIN_CHARS
-          ? `写作任务书仅约 ${chars} 字,低于最低完成线 ${DEEP_CHAPTER_BRIEF_MIN_CHARS} 字,进入重新生成。`
+          ? formatShortOutputResult(
+              `写作任务书仅约 ${chars} 字,低于最低完成线 ${DEEP_CHAPTER_BRIEF_MIN_CHARS} 字,进入重新生成。`,
+              value,
+            )
           : `写作任务书完成,约 ${chars} 字。`;
       },
-      (value) => ({ chars: countChapterChars(value) }),
+      (value) => shortOutputStepParams(value, DEEP_CHAPTER_BRIEF_MIN_CHARS),
     );
     if (!isUsableTaskBrief(taskBrief)) {
       taskBrief = await runChapterWorkflowStep(
@@ -889,8 +896,9 @@ export async function runDeepChapterGeneration(
           const regenerated = await collectTaskBrief();
           const chars = countChapterChars(regenerated);
           if (chars < DEEP_CHAPTER_BRIEF_MIN_CHARS) {
-            throw new Error(
+            throw new ShortModelOutputError(
               `写作任务书生成失败:重生成后仅约 ${chars} 字,低于最低完成线 ${DEEP_CHAPTER_BRIEF_MIN_CHARS} 字。`,
+              regenerated,
             );
           }
           return regenerated;
@@ -975,10 +983,13 @@ export async function runDeepChapterGeneration(
       (value) => {
         const chars = countChapterChars(value);
         return chars < lengthSpec.minChars
-          ? `正文初稿仅约 ${chars} 字,低于最低完成线 ${lengthSpec.minChars} 字,进入扩写补足。`
+          ? formatShortOutputResult(
+              `正文初稿仅约 ${chars} 字,低于最低完成线 ${lengthSpec.minChars} 字,进入扩写补足。`,
+              value,
+            )
           : `正文初稿完成,约 ${chars} 字。`;
       },
-      (value) => ({ chars: countChapterChars(value) }),
+      (value) => shortOutputStepParams(value, lengthSpec.minChars),
     );
     assertNotAborted(signal);
     if (countChapterChars(draftContent) < lengthSpec.minChars) {
@@ -1020,8 +1031,9 @@ export async function runDeepChapterGeneration(
           );
           const expandedChars = countChapterChars(expanded);
           if (expandedChars < lengthSpec.minChars) {
-            throw new Error(
+            throw new ShortModelOutputError(
               `章节正文生成失败:扩写后仅约 ${expandedChars} 字,低于最低完成线 ${lengthSpec.minChars} 字。`,
+              expanded,
             );
           }
           return expanded;
@@ -2084,6 +2096,36 @@ function countChapterChars(content: string): number {
   return content.replace(/\s+/g, "").length;
 }
 
+function shortOutputPreview(content: string): string {
+  return content.trim() || "(空)";
+}
+
+function formatShortOutputResult(message: string, content: string): string {
+  return `${message}\n原文:${shortOutputPreview(content)}`;
+}
+
+function shortOutputStepParams(
+  content: string,
+  minChars: number,
+): Record<string, unknown> {
+  const chars = countChapterChars(content);
+  if (chars >= minChars) return { chars };
+  return { chars, preview: shortOutputPreview(content) };
+}
+
+class ShortModelOutputError extends Error {
+  readonly chars: number;
+  readonly preview: string;
+
+  constructor(message: string, content: string) {
+    const preview = shortOutputPreview(content);
+    super(`${message}\n原文:${preview}`);
+    this.name = "ShortModelOutputError";
+    this.chars = countChapterChars(content);
+    this.preview = preview;
+  }
+}
+
 function isUsableTaskBrief(content: string): boolean {
   return countChapterChars(content) >= DEEP_CHAPTER_BRIEF_MIN_CHARS;
 }