diff --git a/src/lib/domain/__tests__/validation.test.ts b/src/lib/domain/__tests__/validation.test.ts index 1234ca0..8f2359c 100644 --- a/src/lib/domain/__tests__/validation.test.ts +++ b/src/lib/domain/__tests__/validation.test.ts @@ -182,6 +182,40 @@ describe("domain validation", () => { ).toThrow(); }); + it("normalizes near-valid LLM QA report values", () => { + const parsed = qaReportSchema.parse({ + overall_status: "警告", + checks: [ + { + rule_id: "标题质量", + status: "警告", + evidence: { detail: "标题仍然偏营销化" }, + reason: { reason: "官网标题需要更克制" }, + suggested_fix: null, + target_agent: "", + }, + { + rule_id: "company_name_integrity", + status: "通过", + evidence: "公司全称一致", + reason: "正文保留了事实卡中的公司全称", + target_agent: "无", + }, + ], + }); + + expect(parsed.overall_status).toBe("warn"); + expect(parsed.checks[0]).toEqual({ + rule_id: "title_quality", + status: "warn", + evidence: "标题仍然偏营销化", + reason: "官网标题需要更克制", + suggested_fix: "", + target_agent: null, + }); + expect(parsed.checks[1]?.target_agent).toBeNull(); + }); + it("normalizes object-shaped changed sections from LLM output", () => { const parsed = optimizedArticleSchema.parse({ title: "Optimized article", diff --git a/src/lib/domain/validation.ts b/src/lib/domain/validation.ts index d1eba38..5c7c9b8 100644 --- a/src/lib/domain/validation.ts +++ b/src/lib/domain/validation.ts @@ -20,24 +20,89 @@ export const publishPlatformSchema = z.enum([ "recommendation_list", ]) satisfies z.ZodType; -export const checkStatusSchema = z.enum([ - "pass", - "warn", - "fail", -]) satisfies z.ZodType; +const checkStatusAliases: Record = { + pass: "pass", + passed: "pass", + ok: "pass", + "通过": "pass", + "合格": "pass", + warn: "warn", + warning: "warn", + "警告": "warn", + "提醒": "warn", + fail: "fail", + failed: "fail", + failure: "fail", + "失败": "fail", + "不通过": "fail", +}; -export const qualityRuleIdSchema = z.enum([ - "industry_alignment", - "image_text_match", - "voice_consistency", - "platform_fit", - "company_name_integrity", - "title_quality", - "body_quality", - "hallucination_risk", - "claim_consistency", - "context_sensitive_terms", -]) satisfies z.ZodType; +function normalizeCheckStatus(value: unknown): unknown { + const normalized = normalizedStringOrNull(value); + if (!normalized) return value; + return ( + checkStatusAliases[normalized.toLowerCase()] ?? + checkStatusAliases[normalized] ?? + normalized + ); +} + +export const checkStatusSchema = z.preprocess( + normalizeCheckStatus, + z.enum(["pass", "warn", "fail"]), +) satisfies z.ZodType; + +const qualityRuleIdAliases: Record = { + industry_alignment: "industry_alignment", + "行业对齐": "industry_alignment", + "行业一致性": "industry_alignment", + image_text_match: "image_text_match", + "图文匹配": "image_text_match", + "图片文本匹配": "image_text_match", + voice_consistency: "voice_consistency", + "语气一致性": "voice_consistency", + "口吻一致性": "voice_consistency", + platform_fit: "platform_fit", + "平台适配": "platform_fit", + company_name_integrity: "company_name_integrity", + "公司名一致性": "company_name_integrity", + "公司名称一致性": "company_name_integrity", + title_quality: "title_quality", + "标题质量": "title_quality", + body_quality: "body_quality", + "正文质量": "body_quality", + hallucination_risk: "hallucination_risk", + "幻觉风险": "hallucination_risk", + "虚构风险": "hallucination_risk", + claim_consistency: "claim_consistency", + "事实一致性": "claim_consistency", + "主张一致性": "claim_consistency", + context_sensitive_terms: "context_sensitive_terms", + "语境敏感词": "context_sensitive_terms", + "敏感词": "context_sensitive_terms", +}; + +function normalizeQualityRuleId(value: unknown): unknown { + const normalized = normalizedStringOrNull(value); + if (!normalized) return value; + return qualityRuleIdAliases[normalized] ?? normalized; +} + +export const qualityRuleIdSchema = z.preprocess( + normalizeQualityRuleId, + z.enum([ + "industry_alignment", + "image_text_match", + "voice_consistency", + "platform_fit", + "company_name_integrity", + "title_quality", + "body_quality", + "hallucination_risk", + "claim_consistency", + "context_sensitive_terms", + ]), +) satisfies z.ZodType; export const imageInputSchema = z.object({ type: z.enum(["description", "link"]), @@ -308,13 +373,44 @@ export const optimizedArticleSchema = z.object({ requires_user_confirmation: stringListSchema, }) satisfies z.ZodType; +function normalizeTargetAgent(value: unknown): unknown { + const normalized = normalizedStringOrNull(value); + if (!normalized) return null; + const aliases: Record = { + none: null, + null: null, + "无": null, + "无需": null, + title: "title", + "标题": "title", + body: "body", + "正文": "body", + fact_card: "fact_card", + factcard: "fact_card", + "事实卡": "fact_card", + }; + const normalizedKey = normalized.toLowerCase(); + if (Object.prototype.hasOwnProperty.call(aliases, normalizedKey)) { + return aliases[normalizedKey]; + } + if (Object.prototype.hasOwnProperty.call(aliases, normalized)) { + return aliases[normalized]; + } + return normalized; +} + +const targetAgentSchema = z.preprocess( + normalizeTargetAgent, + z.string().trim().min(1).nullable().default(null), +); + export const qaCheckSchema = z.object({ rule_id: qualityRuleIdSchema, status: checkStatusSchema, - evidence: z.string().trim().min(1), - reason: z.string().trim().min(1), - suggested_fix: z.string().trim().default(""), - target_agent: z.string().trim().min(1).nullable().default(null), + evidence: requiredLlmStringSchema, + reason: requiredLlmStringSchema, + suggested_fix: optionalLlmStringSchema, + target_agent: targetAgentSchema, }) satisfies z.ZodType; export const qaReportSchema = z.object({