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

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,
} from "../calibration/types";
import type {
ConfirmedFactCard,
OptimizationFactCard,
ImageInput,
OptimizedArticle,
PublishPlatform,
@@ -285,7 +285,9 @@ export function createD1Repository(db: D1Database): AppRepository {
)
.bind(
jobId,
"auto_extract_then_user_confirmed",
factCard.confirmed_by_user
? "auto_extract_then_user_confirmed"
: "auto_extract_for_optimization",
serialize(factCard),
factCard.confirmed_by_user ? 1 : 0,
timestamp,
@@ -300,7 +302,7 @@ export function createD1Repository(db: D1Database): AppRepository {
.bind(jobId)
.first<FactCardRow>();
return row
? { job_id: row.job_id, ...parseJson<ConfirmedFactCard>(row.fact_card) }
? { job_id: row.job_id, ...parseJson<OptimizationFactCard>(row.fact_card) }
: null;
},
async saveOptimizedArticle(jobId, article) {
+6 -4
View File
@@ -8,7 +8,7 @@ import type {
ScoringRun,
} from "../calibration/types";
import type {
ConfirmedFactCard,
OptimizationFactCard,
ImageInput,
OptimizedArticle,
PublishPlatform,
@@ -335,7 +335,7 @@ export function updateArticleJob(
export function saveFactCard(
dbPath: string | undefined,
jobId: string,
factCard: ConfirmedFactCard,
factCard: OptimizationFactCard,
) {
return withDb(dbPath, (db) => {
const timestamp = nowIso();
@@ -351,7 +351,9 @@ export function saveFactCard(
updated_at = excluded.updated_at`,
).run({
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),
confirmed_by_user: factCard.confirmed_by_user ? 1 : 0,
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 = ?")
.get(jobId) as FactCardRow | undefined;
return row
? { job_id: row.job_id, ...parseJson<ConfirmedFactCard>(row.fact_card) }
? { job_id: row.job_id, ...parseJson<OptimizationFactCard>(row.fact_card) }
: null;
});
}
+4 -4
View File
@@ -5,7 +5,7 @@ import type {
RubricVersion,
ScoringRun,
} from "../calibration/types";
import type { ConfirmedFactCard, OptimizedArticle, QaReport } from "../domain/types";
import type { OptimizationFactCard, OptimizedArticle, QaReport } from "../domain/types";
import type {
ArticleJob,
BrandTemplate,
@@ -28,9 +28,9 @@ export interface AppRepository {
): Promise<ArticleJob | null>;
saveFactCard(
jobId: string,
factCard: ConfirmedFactCard,
): Promise<{ job_id: string } & ConfirmedFactCard>;
getFactCard(jobId: string): Promise<({ job_id: string } & ConfirmedFactCard) | null>;
factCard: OptimizationFactCard,
): Promise<{ job_id: string } & OptimizationFactCard>;
getFactCard(jobId: string): Promise<({ job_id: string } & OptimizationFactCard) | null>;
saveOptimizedArticle(jobId: string, article: OptimizedArticle): Promise<OptimizedArticle>;
getLatestOptimizedArticle(jobId: string): Promise<OptimizedArticle | null>;
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 {
createArticleJob,
@@ -50,7 +50,7 @@ export function createSqliteRepository(dbPath?: string): AppRepository {
) {
return Promise.resolve(updateArticleJob(dbPath, id, changes));
},
saveFactCard(jobId: string, factCard: ConfirmedFactCard) {
saveFactCard(jobId: string, factCard: OptimizationFactCard) {
return Promise.resolve(saveFactCard(dbPath, jobId, factCard));
},
getFactCard(jobId: string) {
@@ -4,6 +4,7 @@ import {
articleInputSchema,
confirmedFactCardSchema,
candidateFactCardSchema,
optimizationFactCardSchema,
optimizedArticleSchema,
qaReportSchema,
} from "../validation";
@@ -25,6 +26,54 @@ describe("domain validation", () => {
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", () => {
const parsed = candidateFactCardSchema.parse({
company_full_name: "Example Technology Co., Ltd.",
+5 -1
View File
@@ -46,7 +46,11 @@ export interface CandidateFactCard {
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;
is_ready_for_optimization: true;
}
+13 -1
View File
@@ -6,6 +6,7 @@ import type {
CheckStatus,
ConfirmedFactCard,
ImageInput,
OptimizationFactCard,
OptimizedArticle,
PublishPlatform,
QaCheck,
@@ -110,7 +111,7 @@ export const imageInputSchema = z.object({
}) satisfies z.ZodType<ImageInput>;
export const articleInputSchema = z.object({
title: z.string().trim().min(1),
title: z.string().trim().default(""),
body: z.string().trim().min(1),
images: z.array(imageInputSchema).default([]),
platform: publishPlatformSchema,
@@ -280,6 +281,17 @@ export const candidateFactCardSchema = factCardBaseSchema
is_ready_for_optimization: card.uncertain_items.length === 0,
})) 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
.extend({
company_full_name: z.string().trim().min(1),
+4 -4
View File
@@ -1,6 +1,6 @@
import type {
ArticleInput,
ConfirmedFactCard,
OptimizationFactCard,
OptimizedArticle,
PublishPlatform,
QaCheck,
@@ -157,7 +157,7 @@ export function buildFactExtractorPrompt(input: ArticleInput) {
export function buildArticleOptimizerPrompt(
input: ArticleInput,
factCard: ConfirmedFactCard,
factCard: OptimizationFactCard,
) {
return [
"Return an OptimizedArticle JSON object with these exact keys:",
@@ -187,7 +187,7 @@ export function buildArticleOptimizerPrompt(
export function buildQualityInspectorPrompt(input: {
article: OptimizedArticle;
factCard: ConfirmedFactCard;
factCard: OptimizationFactCard;
platform: PublishPlatform;
deterministicChecks: QaCheck[];
}) {
@@ -218,7 +218,7 @@ export function buildQualityInspectorPrompt(input: {
export function buildTargetedRewritePrompt(input: {
article: OptimizedArticle;
factCard: ConfirmedFactCard;
factCard: OptimizationFactCard;
failedChecks: QaCheck[];
}) {
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", () => {
const report = inspectQuality({
article: {
+2 -2
View File
@@ -1,6 +1,6 @@
import type {
ArticleInput,
ConfirmedFactCard,
OptimizationFactCard,
OptimizedArticle,
} from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation";
@@ -12,7 +12,7 @@ import {
export interface OptimizeArticleInput {
input: ArticleInput;
factCard: ConfirmedFactCard;
factCard: OptimizationFactCard;
}
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";
export interface RawArticleInput {
title: string;
title?: string;
body: string;
image_lines?: string;
images?: ImageInput[];
@@ -23,7 +23,7 @@ export function normalizeInput(input: RawArticleInput) {
}));
const articleInput = articleInputSchema.parse({
title: input.title,
title: input.title ?? "",
body: input.body,
images,
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 { inspectQualityWithLlm } from "./quality-inspector";
@@ -6,7 +6,7 @@ import { rewriteFailedSections } from "./targeted-rewriter";
export interface RunOptimizationWorkflowInput {
input: ArticleInput;
factCard: ConfirmedFactCard;
factCard: OptimizationFactCard;
onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>;
}
+4 -4
View File
@@ -1,6 +1,6 @@
import type {
CheckStatus,
ConfirmedFactCard,
OptimizationFactCard,
ImageInput,
OptimizedArticle,
PublishPlatform,
@@ -40,7 +40,7 @@ const llmQaPatchSchema = z.object({
export interface InspectQualityInput {
article: OptimizedArticle;
factCard: ConfirmedFactCard;
factCard: OptimizationFactCard;
platform: PublishPlatform;
sourceImages: ImageInput[];
}
@@ -262,7 +262,7 @@ function check(
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(
[
factCard.experience_years,
@@ -278,7 +278,7 @@ function findUnsupportedNumbers(text: string, factCard: ConfirmedFactCard) {
.filter((number) => !allowed.has(number));
}
function extractNumbersFromFactCard(factCard: ConfirmedFactCard) {
function extractNumbersFromFactCard(factCard: OptimizationFactCard) {
return [
factCard.company_full_name,
...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 { generateValidatedJson } from "../llm/client";
import {
@@ -8,7 +8,7 @@ import {
export interface RewriteFailedSectionsInput {
article: OptimizedArticle;
factCard: ConfirmedFactCard;
factCard: OptimizationFactCard;
failedChecks: QaCheck[];
}