import { expect, Page, test } from '@playwright/test'; import fs from 'node:fs'; import path from 'node:path'; declare global { interface Window { __seedIpOperatorScenario?: (scenario: 'empty') => unknown; } } const scenarioSeedPath = path.resolve(__dirname, '../scripts/validation/ip-operator-scenario-seed.js'); const sampleRecordPath = path.resolve( __dirname, '../docs/项目概括/20-实施方案/10-IP操盘工作台V1/IP操盘工作台V1-真实LLM样例验收记录.md', ); const debugPlanPath = path.resolve(__dirname, '../test-results/ip-operator-real-llm-plan.json'); test.describe('IP操盘真实 LLM 样例验收', () => { test.skip(process.env.IP_OPERATOR_REAL_LLM !== '1', 'Set IP_OPERATOR_REAL_LLM=1 to run the paid/network real LLM validation.'); test('can generate a real evaluable IP operator plan', async ({ page }) => { test.setTimeout(300_000); await seedEmptyLoggedInWorkbench(page); await page.goto('/'); const profileCard = page.locator('.ip-card').first(); await profileCard.locator('input').nth(0).fill('林川老板增长笔记'); await profileCard.locator('input').nth(1).fill('传统行业增长顾问'); await profileCard.locator('input').nth(2).fill('本地生活 / 家居建材 / 教育培训'); await profileCard.locator('input').nth(3).fill('传统行业中小企业老板'); await profileCard.locator('input').nth(4).fill('老板 IP 诊断与私域转化陪跑'); await profileCard.locator('textarea').nth(0).fill('内容像广告、不知道如何表达专业判断、缺少真实案例支撑。'); await profileCard.locator('textarea').nth(1).fill('服务过本地商家,做过短视频账号定位和私域转化。'); await profileCard.locator('textarea').nth(2).fill('账号定位、选题策划、脚本诊断、私域转化路径设计。'); await profileCard.locator('textarea').nth(3).fill('直接、务实、案例化。'); await profileCard.locator('textarea').nth(4).fill('不夸大收益,不做成功学。'); await profileCard.locator('textarea').nth(5).fill('生成可执行的 7 天内容测试方案。'); await profileCard.locator('.btn-primary').click(); const questionnaireCard = page.locator('.ip-card').first(); const answers = [ '老板想用内容建立信任,但担心拍出来像广告。', '我能把真实服务经验翻译成用户听得懂的判断。', '过去服务过本地门店和中小企业,有转化案例。', '目前通过咨询、诊断和陪跑服务承接。', '不想被误解成卖课或夸大结果的人。', '擅长直接讲问题、讲案例、给判断标准。', '对标本地商业顾问和老板 IP 账号。', '借鉴他们的问题切入、案例表达和评论区痛点捕捉。', '我的不同是更强调本地业务和私域转化。', '目前最缺稳定选题、脚本和拍摄节奏。', ]; for (let index = 0; index < answers.length; index += 1) { await questionnaireCard.locator('textarea').nth(index).fill(answers[index]); } await questionnaireCard.locator('.btn-primary').click(); const benchmarkCard = page.locator('.ip-card').first(); await benchmarkCard.locator('input').nth(0).fill('商业小纸条'); await benchmarkCard.locator('input').nth(1).fill('https://example.com/benchmark-account'); await benchmarkCard.locator('input').nth(2).fill('https://www.douyin.com/video/7123456789012345678'); await benchmarkCard.locator('textarea').nth(0).fill('问题切入直接,能让老板快速代入;希望 AI 判断爆款点如何迁移到本地老板 IP。'); const optionalDetails = benchmarkCard.locator('.ip-benchmark-optional'); await optionalDetails.locator('summary').click(); await benchmarkCard.locator('input').nth(3).fill('为什么你的短视频越拍越像广告?'); await benchmarkCard.locator('input').nth(4).fill('点赞 3.2 万,评论 1800+,收藏明显高于普通内容'); await benchmarkCard.locator('textarea').nth(1).fill('开头直接戳中老板害怕广告感的痛点,中段用咨询案例拆解信任建立,结尾引导评论区自查。'); await benchmarkCard.locator('textarea').nth(2).fill('评论区大量用户问“怎么表达才不像卖东西”“私域怎么接”,说明转化链路可以从诊断清单切入。'); await benchmarkCard.locator('.btn-primary').click(); const generateCard = page.locator('.ip-card').first(); await expect(generateCard.locator('.btn-primary')).toBeEnabled(); await generateCard.locator('.btn-primary').click(); await expect(page.locator('.ip-result')).toBeVisible({ timeout: 240_000 }); const plan = await page.evaluate(() => { const planIndex = JSON.parse(localStorage.getItem('tiktok.ipOperator.plans.local-guest') || '[]'); const planId = planIndex[0]; return JSON.parse(localStorage.getItem(`tiktok.ipOperator.plan.${planId}.local-guest`) || 'null'); }); writeDebugPlan(plan); expect(plan).toBeTruthy(); expect(['ready', 'needs_input']).toContain(plan.status); expect(plan.diagnosis?.positioningStatement || '').not.toHaveLength(0); expect(plan.benchmarkAnalyses?.[0]?.borrowablePoints?.length || 0).toBeGreaterThanOrEqual(2); expect(plan.benchmarkAnalyses?.[0]?.nonCopyablePoints?.length || 0).toBeGreaterThanOrEqual(2); expect(plan.opportunityMap?.length || 0).toBeGreaterThanOrEqual(3); expect(plan.topics?.length || 0).toBeGreaterThanOrEqual(8); expect((plan.scripts || []).filter((script: any) => script.type === 'full').length).toBeGreaterThanOrEqual(3); expect(plan.sevenDayTestPlan?.length || 0).toBe(7); expect(plan.qualityCheck?.overallScore).toBeGreaterThanOrEqual(0); expect(plan.qualityCheck?.overallScore).toBeLessThanOrEqual(100); const topicsWithQuality = (plan.topics || []).filter((topic: any) => topic.qualityLevel && typeof topic.productionReadinessScore === 'number'); const scriptsWithQuality = (plan.scripts || []).filter((script: any) => script.qualityLevel && script.scriptSummary); expect(topicsWithQuality.length).toBeGreaterThanOrEqual(8); expect(scriptsWithQuality.length).toBeGreaterThanOrEqual(3); expect(plan.qualityAudit?.overallScore).toBeGreaterThanOrEqual(0); expect(plan.qualityAudit?.overallScore).toBeLessThanOrEqual(100); expect(plan.qualityAudit?.dimensions?.length || 0).toBeGreaterThanOrEqual(6); expect(plan.qualityAudit?.productionGate?.reason || '').not.toHaveLength(0); writeRealLlmSampleRecord(plan); }); }); async function seedEmptyLoggedInWorkbench(page: Page) { await page.addInitScript({ path: scenarioSeedPath }); await page.addInitScript(() => { window.__seedIpOperatorScenario?.('empty'); localStorage.setItem('tiktok.currentTab', 'ip-operator'); }); } function writeDebugPlan(plan: any): void { fs.mkdirSync(path.dirname(debugPlanPath), { recursive: true }); fs.writeFileSync(debugPlanPath, `${JSON.stringify(plan, null, 2)}\n`, 'utf8'); } function writeRealLlmSampleRecord(plan: any): void { const fullScript = (plan.scripts || []).find((script: any) => script.type === 'full'); const lines = [ '# IP操盘工作台 V1 真实 LLM 样例验收记录', '', `验收时间:${new Date().toISOString()}`, '', '## 输入 IP 档案', '', '- IP 名称:林川老板增长笔记', '- 身份:传统行业增长顾问', '- 行业:本地生活 / 家居建材 / 教育培训', '- 目标用户:传统行业中小企业老板', '- 目标:生成可执行的 7 天内容测试方案', '', '## 输出摘要', '', `- 最终状态:${plan.status}`, `- 质量分:${plan.qualityCheck?.overallScore ?? '未返回'}/100`, `- P2A 质量审计分:${plan.qualityAudit?.overallScore ?? '未返回'}/100`, `- 选题质量分层数量:${(plan.topics || []).filter((topic: any) => topic.qualityLevel).length}`, `- 脚本质量分层数量:${(plan.scripts || []).filter((script: any) => script.qualityLevel).length}`, `- 制作门槛说明:${text(plan.qualityAudit?.productionGate?.reason)}`, `- 定位一句话:${text(plan.diagnosis?.positioningStatement)}`, `- 待补资料:${text((plan.missingInputs || []).map((item: any) => `${item.type}:${item.description}`))}`, '', '## 代表选题', '', ...(plan.topics || []).slice(0, 3).map((topic: any) => `- ${text(topic.title)}:${text(topic.fitReason)};质量 ${text(topic.qualityLevel)};可制作分 ${text(topic.productionReadinessScore)}`), '', '## 完整口播摘要', '', `- 标题:${text(fullScript?.title)}`, `- 脚本质量:${text(fullScript?.qualityLevel)}`, `- 脚本摘要:${text(fullScript?.scriptSummary)}`, `- 钩子:${text(fullScript?.hook)}`, `- 观点:${text(fullScript?.viewpoint)}`, '', '## 人工复核结论', '', '- 结论:自动结构验收通过;仍需人工复核内容是否符合具体客户语气、案例真实性和业务边界。', '', ]; fs.mkdirSync(path.dirname(sampleRecordPath), { recursive: true }); fs.writeFileSync(sampleRecordPath, `${lines.join('\n').trim()}\n`, 'utf8'); } function text(value: unknown): string { if (value === null || value === undefined || value === '') return '待补充'; if (Array.isArray(value)) return value.map(text).filter(Boolean).join(';') || '待补充'; if (typeof value === 'object') return JSON.stringify(value); return String(value); }