feat: implement article optimization workflow

This commit is contained in:
Codex
2026-06-21 23:42:37 +08:00
parent cb56ff9ee7
commit 75bcab33c6
10 changed files with 834 additions and 1 deletions
+73
View File
@@ -0,0 +1,73 @@
import type { ArticleInput, CandidateFactCard } from "../domain/types";
import { candidateFactCardSchema } from "../domain/validation";
export async function extractCandidateFactCard(
input: ArticleInput,
): Promise<CandidateFactCard> {
const text = `${input.title}\n${input.body}`;
const uncertainItems: string[] = [];
const companyFullName = findCompanyFullName(text);
const years = findExperienceYears(text);
if (!companyFullName) {
uncertainItems.push("Missing company full name");
}
if (years.length > 1) {
uncertainItems.push(`Conflicting experience years: ${years.join(", ")}`);
}
if (input.images.length === 0) {
uncertainItems.push("Image description is missing");
}
const industry = inferIndustry(text);
return candidateFactCardSchema.parse({
company_full_name: companyFullName ?? "",
company_short_names: companyFullName ? [companyFullName.split(/\s+/)[0] ?? ""] : [],
brand_names: inferCapitalizedNames(text),
product_names: inferProducts(text),
target_industry: industry,
target_audience: text.toLowerCase().includes("marketing")
? "Marketing teams"
: "Business readers",
experience_years: years.length === 1 ? years[0] : null,
core_claims: years.length === 1 ? [`${years[0]} years of ${industry} experience`] : [],
forbidden_claims: [],
image_topics: input.images.map((image) => image.content),
uncertain_items: uncertainItems,
});
}
function findCompanyFullName(text: string) {
const match = text.match(
/([A-Z][A-Za-z0-9&.,\-\s]{2,}?(?:Co\.,?\s*Ltd\.?|Company|Inc\.?|LLC|Ltd\.))/,
);
return match?.[1].trim() ?? null;
}
function findExperienceYears(text: string) {
const matches = [...text.matchAll(/\b(\d{1,3})\s*(?:years?|年)\b/gi)];
return [...new Set(matches.map((match) => Number(match[1])))];
}
function inferIndustry(text: string) {
const lower = text.toLowerCase();
if (lower.includes("geo")) return "GEO optimization";
if (lower.includes("finance") || lower.includes("banking")) return "finance automation";
if (lower.includes("seo")) return "SEO";
return "General business";
}
function inferCapitalizedNames(text: string) {
const names = [...text.matchAll(/\b[A-Z][A-Za-z0-9]{2,}\b/g)]
.map((match) => match[0])
.filter((word) => !["The", "This", "And"].includes(word));
return [...new Set(names)].slice(0, 5);
}
function inferProducts(text: string) {
const productMatches = [...text.matchAll(/\b([A-Z][A-Za-z0-9]+\s+GEO)\b/g)].map(
(match) => match[1],
);
return [...new Set(productMatches)];
}