放宽标题和事实卡优化约束

This commit is contained in:
czj
2026-07-01 13:07:44 +08:00
parent 9dad89ae43
commit f9b8bae714
14 changed files with 113 additions and 31 deletions
+5 -3
View File
@@ -6,7 +6,7 @@ import type {
ScoringRun, ScoringRun,
} from "../calibration/types"; } from "../calibration/types";
import type { import type {
ConfirmedFactCard, OptimizationFactCard,
ImageInput, ImageInput,
OptimizedArticle, OptimizedArticle,
PublishPlatform, PublishPlatform,
@@ -285,7 +285,9 @@ export function createD1Repository(db: D1Database): AppRepository {
) )
.bind( .bind(
jobId, jobId,
"auto_extract_then_user_confirmed", factCard.confirmed_by_user
? "auto_extract_then_user_confirmed"
: "auto_extract_for_optimization",
serialize(factCard), serialize(factCard),
factCard.confirmed_by_user ? 1 : 0, factCard.confirmed_by_user ? 1 : 0,
timestamp, timestamp,
@@ -300,7 +302,7 @@ export function createD1Repository(db: D1Database): AppRepository {
.bind(jobId) .bind(jobId)
.first<FactCardRow>(); .first<FactCardRow>();
return row return row
? { job_id: row.job_id, ...parseJson<ConfirmedFactCard>(row.fact_card) } ? { job_id: row.job_id, ...parseJson<OptimizationFactCard>(row.fact_card) }
: null; : null;
}, },
async saveOptimizedArticle(jobId, article) { async saveOptimizedArticle(jobId, article) {
+6 -4
View File
@@ -8,7 +8,7 @@ import type {
ScoringRun, ScoringRun,
} from "../calibration/types"; } from "../calibration/types";
import type { import type {
ConfirmedFactCard, OptimizationFactCard,
ImageInput, ImageInput,
OptimizedArticle, OptimizedArticle,
PublishPlatform, PublishPlatform,
@@ -335,7 +335,7 @@ export function updateArticleJob(
export function saveFactCard( export function saveFactCard(
dbPath: string | undefined, dbPath: string | undefined,
jobId: string, jobId: string,
factCard: ConfirmedFactCard, factCard: OptimizationFactCard,
) { ) {
return withDb(dbPath, (db) => { return withDb(dbPath, (db) => {
const timestamp = nowIso(); const timestamp = nowIso();
@@ -351,7 +351,9 @@ export function saveFactCard(
updated_at = excluded.updated_at`, updated_at = excluded.updated_at`,
).run({ ).run({
job_id: jobId, job_id: jobId,
source: "auto_extract_then_user_confirmed", source: factCard.confirmed_by_user
? "auto_extract_then_user_confirmed"
: "auto_extract_for_optimization",
fact_card: serialize(factCard), fact_card: serialize(factCard),
confirmed_by_user: factCard.confirmed_by_user ? 1 : 0, confirmed_by_user: factCard.confirmed_by_user ? 1 : 0,
created_at: timestamp, created_at: timestamp,
@@ -368,7 +370,7 @@ export function getFactCard(dbPath: string | undefined, jobId: string) {
.prepare("select job_id, fact_card from fact_cards where job_id = ?") .prepare("select job_id, fact_card from fact_cards where job_id = ?")
.get(jobId) as FactCardRow | undefined; .get(jobId) as FactCardRow | undefined;
return row return row
? { job_id: row.job_id, ...parseJson<ConfirmedFactCard>(row.fact_card) } ? { job_id: row.job_id, ...parseJson<OptimizationFactCard>(row.fact_card) }
: null; : null;
}); });
} }
+4 -4
View File
@@ -5,7 +5,7 @@ import type {
RubricVersion, RubricVersion,
ScoringRun, ScoringRun,
} from "../calibration/types"; } from "../calibration/types";
import type { ConfirmedFactCard, OptimizedArticle, QaReport } from "../domain/types"; import type { OptimizationFactCard, OptimizedArticle, QaReport } from "../domain/types";
import type { import type {
ArticleJob, ArticleJob,
BrandTemplate, BrandTemplate,
@@ -28,9 +28,9 @@ export interface AppRepository {
): Promise<ArticleJob | null>; ): Promise<ArticleJob | null>;
saveFactCard( saveFactCard(
jobId: string, jobId: string,
factCard: ConfirmedFactCard, factCard: OptimizationFactCard,
): Promise<{ job_id: string } & ConfirmedFactCard>; ): Promise<{ job_id: string } & OptimizationFactCard>;
getFactCard(jobId: string): Promise<({ job_id: string } & ConfirmedFactCard) | null>; getFactCard(jobId: string): Promise<({ job_id: string } & OptimizationFactCard) | null>;
saveOptimizedArticle(jobId: string, article: OptimizedArticle): Promise<OptimizedArticle>; saveOptimizedArticle(jobId: string, article: OptimizedArticle): Promise<OptimizedArticle>;
getLatestOptimizedArticle(jobId: string): Promise<OptimizedArticle | null>; getLatestOptimizedArticle(jobId: string): Promise<OptimizedArticle | null>;
saveQaReport(jobId: string, revision: number, report: QaReport): Promise<QaReport>; saveQaReport(jobId: string, revision: number, report: QaReport): Promise<QaReport>;
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ConfirmedFactCard, OptimizedArticle, QaReport } from "../domain/types"; import type { OptimizationFactCard, OptimizedArticle, QaReport } from "../domain/types";
import type { AppRepository } from "./repository"; import type { AppRepository } from "./repository";
import { import {
createArticleJob, createArticleJob,
@@ -50,7 +50,7 @@ export function createSqliteRepository(dbPath?: string): AppRepository {
) { ) {
return Promise.resolve(updateArticleJob(dbPath, id, changes)); return Promise.resolve(updateArticleJob(dbPath, id, changes));
}, },
saveFactCard(jobId: string, factCard: ConfirmedFactCard) { saveFactCard(jobId: string, factCard: OptimizationFactCard) {
return Promise.resolve(saveFactCard(dbPath, jobId, factCard)); return Promise.resolve(saveFactCard(dbPath, jobId, factCard));
}, },
getFactCard(jobId: string) { getFactCard(jobId: string) {
@@ -4,6 +4,7 @@ import {
articleInputSchema, articleInputSchema,
confirmedFactCardSchema, confirmedFactCardSchema,
candidateFactCardSchema, candidateFactCardSchema,
optimizationFactCardSchema,
optimizedArticleSchema, optimizedArticleSchema,
qaReportSchema, qaReportSchema,
} from "../validation"; } from "../validation";
@@ -25,6 +26,54 @@ describe("domain validation", () => {
expect(parsed.platform).toBe("official_site"); expect(parsed.platform).toBe("official_site");
}); });
it("accepts article input with an empty optional title", () => {
const parsed = articleInputSchema.parse({
title: " ",
body: "完整文章正文可以直接粘贴在这里。",
images: [],
platform: "official_site",
user_instructions: "",
});
expect(parsed.title).toBe("");
expect(parsed.body).toBe("完整文章正文可以直接粘贴在这里。");
});
it("still rejects article input with an empty body", () => {
expect(() =>
articleInputSchema.parse({
title: "",
body: " ",
images: [],
platform: "official_site",
user_instructions: "",
}),
).toThrow();
});
it("accepts an unconfirmed optimization fact card with unresolved items", () => {
const parsed = optimizationFactCardSchema.parse({
company_full_name: "",
company_short_names: ["示例科技"],
brand_names: [],
product_names: ["GEO内容优化平台"],
target_industry: "",
target_audience: "市场团队",
experience_years: "",
core_claims: ["提供GEO内容优化服务"],
forbidden_claims: [],
image_topics: [],
uncertain_items: ["客户案例需要确认"],
confirmed_by_user: false,
});
expect(parsed.company_full_name).toBe("");
expect(parsed.experience_years).toBeNull();
expect(parsed.confirmed_by_user).toBe(false);
expect(parsed.is_ready_for_optimization).toBe(false);
expect(parsed.uncertain_items).toEqual(["客户案例需要确认"]);
});
it("marks a fact card with unresolved uncertain items as not ready for optimization", () => { it("marks a fact card with unresolved uncertain items as not ready for optimization", () => {
const parsed = candidateFactCardSchema.parse({ const parsed = candidateFactCardSchema.parse({
company_full_name: "Example Technology Co., Ltd.", company_full_name: "Example Technology Co., Ltd.",
+5 -1
View File
@@ -46,7 +46,11 @@ export interface CandidateFactCard {
is_ready_for_optimization: boolean; is_ready_for_optimization: boolean;
} }
export interface ConfirmedFactCard extends CandidateFactCard { export interface OptimizationFactCard extends CandidateFactCard {
confirmed_by_user: boolean;
}
export interface ConfirmedFactCard extends OptimizationFactCard {
confirmed_by_user: true; confirmed_by_user: true;
is_ready_for_optimization: true; is_ready_for_optimization: true;
} }
+13 -1
View File
@@ -6,6 +6,7 @@ import type {
CheckStatus, CheckStatus,
ConfirmedFactCard, ConfirmedFactCard,
ImageInput, ImageInput,
OptimizationFactCard,
OptimizedArticle, OptimizedArticle,
PublishPlatform, PublishPlatform,
QaCheck, QaCheck,
@@ -110,7 +111,7 @@ export const imageInputSchema = z.object({
}) satisfies z.ZodType<ImageInput>; }) satisfies z.ZodType<ImageInput>;
export const articleInputSchema = z.object({ export const articleInputSchema = z.object({
title: z.string().trim().min(1), title: z.string().trim().default(""),
body: z.string().trim().min(1), body: z.string().trim().min(1),
images: z.array(imageInputSchema).default([]), images: z.array(imageInputSchema).default([]),
platform: publishPlatformSchema, platform: publishPlatformSchema,
@@ -280,6 +281,17 @@ export const candidateFactCardSchema = factCardBaseSchema
is_ready_for_optimization: card.uncertain_items.length === 0, is_ready_for_optimization: card.uncertain_items.length === 0,
})) satisfies z.ZodType<CandidateFactCard>; })) satisfies z.ZodType<CandidateFactCard>;
export const optimizationFactCardSchema = factCardBaseSchema
.extend({
confirmed_by_user: z.boolean().optional().default(false),
is_ready_for_optimization: z.boolean().optional(),
})
.transform((card) => ({
...card,
confirmed_by_user: card.confirmed_by_user,
is_ready_for_optimization: card.uncertain_items.length === 0,
})) satisfies z.ZodType<OptimizationFactCard>;
export const confirmedFactCardSchema = factCardBaseSchema export const confirmedFactCardSchema = factCardBaseSchema
.extend({ .extend({
company_full_name: z.string().trim().min(1), company_full_name: z.string().trim().min(1),
+4 -4
View File
@@ -1,6 +1,6 @@
import type { import type {
ArticleInput, ArticleInput,
ConfirmedFactCard, OptimizationFactCard,
OptimizedArticle, OptimizedArticle,
PublishPlatform, PublishPlatform,
QaCheck, QaCheck,
@@ -157,7 +157,7 @@ export function buildFactExtractorPrompt(input: ArticleInput) {
export function buildArticleOptimizerPrompt( export function buildArticleOptimizerPrompt(
input: ArticleInput, input: ArticleInput,
factCard: ConfirmedFactCard, factCard: OptimizationFactCard,
) { ) {
return [ return [
"Return an OptimizedArticle JSON object with these exact keys:", "Return an OptimizedArticle JSON object with these exact keys:",
@@ -187,7 +187,7 @@ export function buildArticleOptimizerPrompt(
export function buildQualityInspectorPrompt(input: { export function buildQualityInspectorPrompt(input: {
article: OptimizedArticle; article: OptimizedArticle;
factCard: ConfirmedFactCard; factCard: OptimizationFactCard;
platform: PublishPlatform; platform: PublishPlatform;
deterministicChecks: QaCheck[]; deterministicChecks: QaCheck[];
}) { }) {
@@ -218,7 +218,7 @@ export function buildQualityInspectorPrompt(input: {
export function buildTargetedRewritePrompt(input: { export function buildTargetedRewritePrompt(input: {
article: OptimizedArticle; article: OptimizedArticle;
factCard: ConfirmedFactCard; factCard: OptimizationFactCard;
failedChecks: QaCheck[]; failedChecks: QaCheck[];
}) { }) {
return [ return [
@@ -41,6 +41,19 @@ describe("workflow nodes", () => {
]); ]);
}); });
it("normalizes missing article titles to an empty string", () => {
const normalized = normalizeInput({
body: "只有正文也可以开始优化。",
image_lines: "",
platform: "official_site",
user_instructions: "",
});
expect(normalized.article_draft.title).toBe("");
expect(normalized.articleInput.title).toBe("");
expect(normalized.articleInput.body).toBe("只有正文也可以开始优化。");
});
it("returns the 10 required quality checks", () => { it("returns the 10 required quality checks", () => {
const report = inspectQuality({ const report = inspectQuality({
article: { article: {
+2 -2
View File
@@ -1,6 +1,6 @@
import type { import type {
ArticleInput, ArticleInput,
ConfirmedFactCard, OptimizationFactCard,
OptimizedArticle, OptimizedArticle,
} from "../domain/types"; } from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation"; import { optimizedArticleSchema } from "../domain/validation";
@@ -12,7 +12,7 @@ import {
export interface OptimizeArticleInput { export interface OptimizeArticleInput {
input: ArticleInput; input: ArticleInput;
factCard: ConfirmedFactCard; factCard: OptimizationFactCard;
} }
export async function optimizeArticle({ export async function optimizeArticle({
+2 -2
View File
@@ -2,7 +2,7 @@ import type { ArticleInput, ImageInput, PublishPlatform } from "../domain/types"
import { articleInputSchema } from "../domain/validation"; import { articleInputSchema } from "../domain/validation";
export interface RawArticleInput { export interface RawArticleInput {
title: string; title?: string;
body: string; body: string;
image_lines?: string; image_lines?: string;
images?: ImageInput[]; images?: ImageInput[];
@@ -23,7 +23,7 @@ export function normalizeInput(input: RawArticleInput) {
})); }));
const articleInput = articleInputSchema.parse({ const articleInput = articleInputSchema.parse({
title: input.title, title: input.title ?? "",
body: input.body, body: input.body,
images, images,
platform: input.platform, platform: input.platform,
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ArticleInput, ConfirmedFactCard } from "../domain/types"; import type { ArticleInput, OptimizationFactCard } from "../domain/types";
import { optimizeArticle } from "./article-optimizer"; import { optimizeArticle } from "./article-optimizer";
import { inspectQualityWithLlm } from "./quality-inspector"; import { inspectQualityWithLlm } from "./quality-inspector";
@@ -6,7 +6,7 @@ import { rewriteFailedSections } from "./targeted-rewriter";
export interface RunOptimizationWorkflowInput { export interface RunOptimizationWorkflowInput {
input: ArticleInput; input: ArticleInput;
factCard: ConfirmedFactCard; factCard: OptimizationFactCard;
onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>; onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>;
} }
+4 -4
View File
@@ -1,6 +1,6 @@
import type { import type {
CheckStatus, CheckStatus,
ConfirmedFactCard, OptimizationFactCard,
ImageInput, ImageInput,
OptimizedArticle, OptimizedArticle,
PublishPlatform, PublishPlatform,
@@ -40,7 +40,7 @@ const llmQaPatchSchema = z.object({
export interface InspectQualityInput { export interface InspectQualityInput {
article: OptimizedArticle; article: OptimizedArticle;
factCard: ConfirmedFactCard; factCard: OptimizationFactCard;
platform: PublishPlatform; platform: PublishPlatform;
sourceImages: ImageInput[]; sourceImages: ImageInput[];
} }
@@ -262,7 +262,7 @@ function check(
return { rule_id, status, evidence, reason, suggested_fix, target_agent }; return { rule_id, status, evidence, reason, suggested_fix, target_agent };
} }
function findUnsupportedNumbers(text: string, factCard: ConfirmedFactCard) { function findUnsupportedNumbers(text: string, factCard: OptimizationFactCard) {
const allowed = new Set( const allowed = new Set(
[ [
factCard.experience_years, factCard.experience_years,
@@ -278,7 +278,7 @@ function findUnsupportedNumbers(text: string, factCard: ConfirmedFactCard) {
.filter((number) => !allowed.has(number)); .filter((number) => !allowed.has(number));
} }
function extractNumbersFromFactCard(factCard: ConfirmedFactCard) { function extractNumbersFromFactCard(factCard: OptimizationFactCard) {
return [ return [
factCard.company_full_name, factCard.company_full_name,
...factCard.company_short_names, ...factCard.company_short_names,
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ConfirmedFactCard, OptimizedArticle, QaCheck } from "../domain/types"; import type { OptimizationFactCard, OptimizedArticle, QaCheck } from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation"; import { optimizedArticleSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client"; import { generateValidatedJson } from "../llm/client";
import { import {
@@ -8,7 +8,7 @@ import {
export interface RewriteFailedSectionsInput { export interface RewriteFailedSectionsInput {
article: OptimizedArticle; article: OptimizedArticle;
factCard: ConfirmedFactCard; factCard: OptimizationFactCard;
failedChecks: QaCheck[]; failedChecks: QaCheck[];
} }