Преглед изворни кода

fix: sanitize length normalizer output

Ma пре 7 месеци
родитељ
комит
071084ede4

+ 36 - 0
packages/core/src/__tests__/length-normalizer.test.ts

@@ -129,4 +129,40 @@ describe("LengthNormalizerAgent", () => {
     expect(result.mode).toBe("compress");
     expect(result.warning).toContain("outside");
   });
+
+  it("strips explanatory wrappers from malformed normalizer output", async () => {
+    const agent = createAgent();
+    const chatSpy = vi.spyOn(BaseAgent.prototype as never, "chat").mockResolvedValue({
+      content: [
+        "我先压缩一下正文。",
+        "",
+        "```markdown",
+        "压缩后的正文。[[KEEP_ME]]",
+        "```",
+      ].join("\n"),
+      usage: ZERO_USAGE,
+    });
+    const lengthSpec = LengthSpecSchema.parse({
+      target: 220,
+      softMin: 190,
+      softMax: 250,
+      hardMin: 160,
+      hardMax: 280,
+      countingMode: "zh_chars",
+      normalizeMode: "compress",
+    });
+    const draft = "开头。" + "冗余句子。".repeat(50) + "[[KEEP_ME]]";
+
+    const result = await agent.normalizeChapter({
+      chapterContent: draft,
+      lengthSpec,
+      chapterIntent: "Preserve [[KEEP_ME]] only.",
+      reducedControlBlock: "No extra commentary.",
+    });
+
+    expect(chatSpy).toHaveBeenCalledTimes(1);
+    expect(result.normalizedContent).toBe("压缩后的正文。[[KEEP_ME]]");
+    expect(result.normalizedContent).not.toContain("我先压缩一下正文");
+    expect(result.normalizedContent).not.toContain("```");
+  });
 });

+ 41 - 1
packages/core/src/agents/length-normalizer.ts

@@ -55,7 +55,7 @@ export class LengthNormalizerAgent extends BaseAgent {
       },
     );
 
-    const normalizedContent = response.content.trim() || input.chapterContent;
+    const normalizedContent = this.sanitizeNormalizedContent(response.content, input.chapterContent);
     const finalCount = countChapterLength(normalizedContent, input.lengthSpec.countingMode);
     const warning = this.buildWarning(finalCount, input.lengthSpec);
 
@@ -129,4 +129,44 @@ ${input.chapterContent}`;
 
     return `Final count ${finalCount} is outside the soft range ${lengthSpec.softMin}-${lengthSpec.softMax} after one normalization pass.`;
   }
+
+  private sanitizeNormalizedContent(rawContent: string, fallbackContent: string): string {
+    const trimmed = rawContent.trim();
+    if (!trimmed) return fallbackContent;
+
+    const fenced = this.extractFirstFencedBlock(trimmed);
+    if (fenced) return fenced;
+
+    if (this.looksLikeResponseWrapper(trimmed)) {
+      const stripped = this.stripCommonWrappers(trimmed);
+      if (stripped) return stripped;
+    }
+
+    return trimmed;
+  }
+
+  private extractFirstFencedBlock(content: string): string | undefined {
+    const match = content.match(/```(?:[a-zA-Z-]+)?\s*\n([\s\S]*?)\n```/);
+    if (!match) return undefined;
+    const body = match[1]?.trim();
+    return body ? body : undefined;
+  }
+
+  private looksLikeResponseWrapper(content: string): boolean {
+    return /```/.test(content) || /^(我先|下面是|以下是|Here is|I will)/i.test(content);
+  }
+
+  private stripCommonWrappers(content: string): string {
+    return content
+      .split("\n")
+      .map((line) => line.trim())
+      .filter((line) =>
+        line.length > 0 &&
+        !/^```/.test(line) &&
+        !/^(我先|下面是|以下是|Here is|I will)/i.test(line) &&
+        !/^#+\s*(说明|解释|注释|analysis|analysis note)/i.test(line),
+      )
+      .join("\n")
+      .trim();
+  }
 }