| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- 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);
- }
|