放宽标题和事实卡优化约束
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user