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