| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- import { BaseAgent } from "./base.js";
- import type { LengthNormalizeMode, LengthSpec } from "../models/length-governance.js";
- import { countChapterLength, chooseNormalizeMode, isOutsideHardRange, isOutsideSoftRange } from "../utils/length-metrics.js";
- export interface NormalizeLengthInput {
- readonly chapterContent: string;
- readonly lengthSpec: LengthSpec;
- readonly chapterIntent?: string;
- readonly reducedControlBlock?: string;
- }
- export interface NormalizeLengthOutput {
- readonly normalizedContent: string;
- readonly finalCount: number;
- readonly applied: boolean;
- readonly mode: LengthNormalizeMode;
- readonly warning?: string;
- readonly tokenUsage?: {
- readonly promptTokens: number;
- readonly completionTokens: number;
- readonly totalTokens: number;
- };
- }
- export class LengthNormalizerAgent extends BaseAgent {
- get name(): string {
- return "length-normalizer";
- }
- async normalizeChapter(input: NormalizeLengthInput): Promise<NormalizeLengthOutput> {
- const originalCount = countChapterLength(input.chapterContent, input.lengthSpec.countingMode);
- const mode = input.lengthSpec.normalizeMode === "none"
- ? chooseNormalizeMode(originalCount, input.lengthSpec)
- : input.lengthSpec.normalizeMode;
- if (mode === "none") {
- return {
- normalizedContent: input.chapterContent,
- finalCount: originalCount,
- applied: false,
- mode,
- };
- }
- const systemPrompt = this.buildSystemPrompt(mode);
- const userPrompt = this.buildUserPrompt(input, originalCount, mode);
- const response = await this.chat(
- [
- { role: "system", content: systemPrompt },
- { role: "user", content: userPrompt },
- ],
- {
- temperature: 0.2,
- maxTokens: Math.max(4096, Math.ceil(originalCount * 1.2)),
- },
- );
- const normalizedContent = this.sanitizeNormalizedContent(response.content, input.chapterContent);
- const finalCount = countChapterLength(normalizedContent, input.lengthSpec.countingMode);
- const warning = this.buildWarning(finalCount, input.lengthSpec);
- return {
- normalizedContent,
- finalCount,
- applied: true,
- mode,
- warning,
- tokenUsage: response.usage,
- };
- }
- private buildSystemPrompt(mode: LengthNormalizeMode): string {
- const action = mode === "compress"
- ? "compress"
- : "expand";
- return `你是一位章节长度修正器。你的任务是对章节正文做一次单次修正,只能执行一次,不得递归重写。
- 修正目标:
- - ${action} 章节长度到给定目标区间
- - 保留章节原有事实、关键钩子、角色名和必须保留的标记
- - 不要引入新的支线、未来揭示或额外总结
- - 不要在正文外输出任何解释`;
- }
- private buildUserPrompt(
- input: NormalizeLengthInput,
- originalCount: number,
- mode: LengthNormalizeMode,
- ): string {
- const intentBlock = input.chapterIntent
- ? `\n## Chapter Intent\n${input.chapterIntent}\n`
- : "";
- const controlBlock = input.reducedControlBlock
- ? `\n## Reduced Control Block\n${input.reducedControlBlock}\n`
- : "";
- return `请对下面正文做一次${mode === "compress" ? "压缩" : "扩写"}修正。
- ## Length Spec
- - Target: ${input.lengthSpec.target}
- - Soft Range: ${input.lengthSpec.softMin}-${input.lengthSpec.softMax}
- - Hard Range: ${input.lengthSpec.hardMin}-${input.lengthSpec.hardMax}
- - Counting Mode: ${input.lengthSpec.countingMode}
- ## Current Count
- ${originalCount}
- ## Correction Rules
- - 只修正一次,不要递归
- - 保留正文中的关键标记、人物名、地点名和已有事实
- - 不要凭空新增子情节
- - 不要插入解释性总结或分析
- - 输出修正后的完整正文,不要加标签
- ${intentBlock}${controlBlock}
- ## Chapter Content
- ${input.chapterContent}`;
- }
- private buildWarning(finalCount: number, lengthSpec: LengthSpec): string | undefined {
- if (!isOutsideSoftRange(finalCount, lengthSpec)) {
- return undefined;
- }
- if (isOutsideHardRange(finalCount, lengthSpec)) {
- return `Final count ${finalCount} is outside the hard range ${lengthSpec.hardMin}-${lengthSpec.hardMax} after one normalization pass.`;
- }
- 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;
- const stripped = this.stripCommonWrappers(trimmed);
- if (stripped !== undefined) {
- // Empty after stripping = response was only wrapper text, use original
- if (!stripped) return fallbackContent;
- // Guard: if stripping removed more than 50% of content, the regex was too aggressive.
- if (stripped.length < trimmed.length * 0.5) return trimmed;
- 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 stripCommonWrappers(content: string): string | undefined {
- const lines = content.split("\n");
- let removedAny = false;
- const keptLines: string[] = [];
- for (const rawLine of lines) {
- const trimmed = rawLine.trim();
- if (this.isWrapperLine(trimmed)) {
- removedAny = true;
- continue;
- }
- keptLines.push(rawLine);
- }
- if (!removedAny) {
- return undefined;
- }
- return keptLines.join("\n").trim();
- }
- private isWrapperLine(line: string): boolean {
- if (!line) return false;
- if (/^```/.test(line)) return true;
- if (/^#+\s*(说明|解释|注释|analysis|analysis note)\b/i.test(line)) return true;
- if (/^(下面是|以下是).*(正文|章节|压缩|扩写|修正|修改|调整|改写|润色|结果|内容|输出|版本)/i.test(line)) {
- return true;
- }
- if (/^我先.*(压缩|扩写|修正|修改|调整|改写|润色|处理).*(正文|章节)?/i.test(line)) {
- return true;
- }
- if (/^(here(?:'s| is)|below is).*(chapter|draft|content|rewrite|revised|compressed|expanded|normalized|adjusted|output|version|result)/i.test(line)) {
- return true;
- }
- if (/^i(?:'ll| will)\s+(rewrite|revise|reword|compress|expand|normalize|adjust|shorten|lengthen|trim|fix)\b/i.test(line)) {
- return true;
- }
- return false;
- }
- }
|