diff --git a/src/lib/workflow/__tests__/workflow.test.ts b/src/lib/workflow/__tests__/workflow.test.ts index 6605cad..ddada28 100644 --- a/src/lib/workflow/__tests__/workflow.test.ts +++ b/src/lib/workflow/__tests__/workflow.test.ts @@ -63,6 +63,23 @@ describe("workflow nodes", () => { expect(card.is_ready_for_optimization).toBe(false); }); + it("extracts Chinese company facts from Chinese articles", async () => { + const card = await extractCandidateFactCard({ + title: "#探寻AIGC短视频培训选哪家,各品牌实力大比拼", + body: "伟思德鲁管理咨询(深圳)有限公司面向品牌商家和出海企业提供AIGC短视频培训服务,帮助企业解决内容工业化生产、品牌视觉统一和全球化传播问题。", + images: [{ type: "description", content: "AIGC短视频工作流示意图" }], + platform: "media_article", + user_instructions: "保留AIGC短视频培训与出海内容生产场景。", + }); + + expect(card.company_full_name).toBe("伟思德鲁管理咨询(深圳)有限公司"); + expect(card.company_short_names).toContain("伟思德鲁"); + expect(card.target_industry).toBe("AIGC短视频培训"); + expect(card.target_audience).toBe("品牌商家、内容创作者、出海企业"); + expect(card.image_topics).toEqual(["AIGC短视频工作流示意图"]); + expect(card.uncertain_items).not.toContain("Missing company full name"); + }); + it("does not add claims outside the confirmed fact card", async () => { const optimized = await optimizeArticle({ input: { diff --git a/src/lib/workflow/fact-extractor.ts b/src/lib/workflow/fact-extractor.ts index f38510e..3ba8c4e 100644 --- a/src/lib/workflow/fact-extractor.ts +++ b/src/lib/workflow/fact-extractor.ts @@ -23,13 +23,13 @@ export async function extractCandidateFactCard( return candidateFactCardSchema.parse({ company_full_name: companyFullName ?? "", - company_short_names: companyFullName ? [companyFullName.split(/\s+/)[0] ?? ""] : [], - brand_names: inferCapitalizedNames(text), + company_short_names: inferCompanyShortNames(companyFullName), + brand_names: inferBrandNames(text, companyFullName), product_names: inferProducts(text), target_industry: industry, target_audience: text.toLowerCase().includes("marketing") ? "Marketing teams" - : "Business readers", + : inferTargetAudience(text), experience_years: years.length === 1 ? years[0] : null, core_claims: years.length === 1 ? [`${years[0]} years of ${industry} experience`] : [], forbidden_claims: [], @@ -39,10 +39,15 @@ export async function extractCandidateFactCard( } function findCompanyFullName(text: string) { - const match = text.match( + const englishMatch = text.match( /([A-Z][A-Za-z0-9&.,\-\s]{2,}?(?:Co\.,?\s*Ltd\.?|Company|Inc\.?|LLC|Ltd\.))/, ); - return match?.[1].trim() ?? null; + if (englishMatch?.[1]) return englishMatch[1].trim(); + + const chineseMatch = text.match( + /([\u4e00-\u9fa5A-Za-z0-9()()]{2,40}?(?:股份有限公司|有限公司|集团|公司))/, + ); + return chineseMatch?.[1].trim() ?? null; } function findExperienceYears(text: string) { @@ -55,19 +60,63 @@ function inferIndustry(text: string) { if (lower.includes("geo")) return "GEO optimization"; if (lower.includes("finance") || lower.includes("banking")) return "finance automation"; if (lower.includes("seo")) return "SEO"; + if (/AIGC|短视频|出海内容|内容工业化/.test(text)) return "AIGC短视频培训"; + if (/工业零部件|爆品操盘|AI OBS|IPMS/.test(text)) return "工业零部件爆品操盘"; return "General business"; } -function inferCapitalizedNames(text: string) { +function inferBrandNames(text: string, companyFullName: string | null) { + const chineseBrands = [ + ...new Set( + [ + companyFullName ? inferChineseShortName(companyFullName) : "", + ...[...text.matchAll(/\b(AIGC|AI OBS|IPMS|GEO|SEO)\b/g)].map( + (match) => match[1], + ), + ].filter(Boolean), + ), + ]; 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); + return [...new Set([...chineseBrands, ...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], - ); + const productMatches = [ + ...[...text.matchAll(/\b([A-Z][A-Za-z0-9]+\s+GEO)\b/g)].map( + (match) => match[1], + ), + ...[...text.matchAll(/\b(AIGC短视频培训|AI OBS|IPMS|爆品操盘数智系统)\b/g)].map( + (match) => match[1], + ), + ]; return [...new Set(productMatches)]; } + +function inferCompanyShortNames(companyFullName: string | null) { + if (!companyFullName) return []; + if (/[\u4e00-\u9fa5]/.test(companyFullName)) { + const legalShortName = inferChineseShortName(companyFullName); + const brandShortName = legalShortName.replace(/管理咨询$/, ""); + return [...new Set([brandShortName, legalShortName].filter(Boolean))]; + } + return [companyFullName.split(/\s+/)[0] ?? ""].filter(Boolean); +} + +function inferChineseShortName(companyFullName: string) { + return companyFullName + .replace(/[((].*?[))]/g, "") + .replace(/股份有限公司|有限公司|集团|公司/g, "") + .trim(); +} + +function inferTargetAudience(text: string) { + if (/品牌商家|内容创作者|出海企业/.test(text)) { + return "品牌商家、内容创作者、出海企业"; + } + if (/工业企业|工业零部件制造商|采购/.test(text)) { + return "工业企业、采购团队、工业零部件制造商"; + } + return "Business readers"; +}