ip-operator-evidence-loop.spec.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { expect, Page, test } from '@playwright/test';
  2. import path from 'node:path';
  3. declare global {
  4. interface Window {
  5. __seedIpOperatorScenario?: (scenario: 'ready') => unknown;
  6. }
  7. }
  8. const scenarioSeedPath = path.resolve(__dirname, '../scripts/validation/ip-operator-scenario-seed.js');
  9. async function seedEvidenceLoop(page: Page) {
  10. await page.addInitScript({ path: scenarioSeedPath });
  11. await page.addInitScript(() => {
  12. window.__seedIpOperatorScenario?.('ready');
  13. const scope = 'local-guest';
  14. const planId = 'ip_plan_e2e_ready';
  15. const accountId = 'ip_account_e2e_ready';
  16. const now = new Date().toISOString();
  17. const ownedWorkId = 'evidence_owned_work';
  18. const commentId = 'evidence_owned_comment';
  19. const benchmarkId = 'evidence_benchmark_work';
  20. const painId = 'pain_evidence_loop';
  21. const planKey = `tiktok.ipOperator.plan.${planId}.${scope}`;
  22. const plan = JSON.parse(localStorage.getItem(planKey) || '{}');
  23. plan.evidenceItems = [
  24. {
  25. id: ownedWorkId,
  26. platform: 'douyin',
  27. kind: 'owned_post',
  28. bucket: 'accounts',
  29. accountId,
  30. sourceRole: 'owned',
  31. title: '自有账号作品:老板 IP 如何避免成为广告号',
  32. rawText: '先讲客户判断,再讲产品。',
  33. summary: '播放 50000,评论 120,收藏 380。',
  34. tags: ['自有账号', '账号作品'],
  35. relatedTopicIds: [],
  36. relatedScriptIds: [],
  37. sourceId: 'work-owned-1',
  38. evidenceRefs: ['https://www.douyin.com/video/7304943423281171749'],
  39. selectedForGeneration: true,
  40. confidence: 'high',
  41. createdAt: now,
  42. updatedAt: now,
  43. },
  44. {
  45. id: commentId,
  46. platform: 'douyin',
  47. kind: 'comment',
  48. bucket: 'topics',
  49. accountId,
  50. sourceRole: 'owned',
  51. title: '自有账号评论:老板 IP 如何避免成为广告号',
  52. rawText: '传统行业老板怎么讲案例才不像广告?',
  53. summary: '传统行业老板怎么讲案例才不像广告?',
  54. tags: ['自有账号', '评论痛点'],
  55. relatedTopicIds: [],
  56. relatedScriptIds: [],
  57. sourceId: 'comment-owned-1',
  58. evidenceRefs: [],
  59. selectedForGeneration: true,
  60. confidence: 'high',
  61. createdAt: now,
  62. updatedAt: now,
  63. },
  64. {
  65. id: benchmarkId,
  66. platform: 'douyin',
  67. kind: 'competitor_post',
  68. bucket: 'accounts',
  69. sourceRole: 'benchmark',
  70. title: '对标作品:先讲判断标准再讲方法',
  71. rawText: '反常识开头,案例证明。',
  72. summary: '可借鉴问题钩子和案例结构。',
  73. tags: ['对标账号'],
  74. relatedTopicIds: [],
  75. relatedScriptIds: [],
  76. sourceId: 'benchmark-work-1',
  77. evidenceRefs: [],
  78. selectedForGeneration: true,
  79. confidence: 'medium',
  80. createdAt: now,
  81. updatedAt: now,
  82. },
  83. ];
  84. plan.commentPainInsights = [{
  85. id: painId,
  86. title: '用户追问:怎么讲案例',
  87. category: 'question',
  88. severity: 'high',
  89. summary: '评论集中追问老板如何讲案例而不显得像广告。',
  90. commentCount: 8,
  91. keywordFrequency: [{ keyword: '怎么', count: 8 }],
  92. representativeComments: ['传统行业老板怎么讲案例才不像广告?'],
  93. evidenceItemIds: [commentId],
  94. suggestedTopics: ['案例表达三步法'],
  95. scriptAngles: ['用评论原话开头'],
  96. riskBoundaries: ['不承诺结果'],
  97. createdAt: now,
  98. updatedAt: now,
  99. }];
  100. plan.accountStrategyReports = [{
  101. id: 'strategy_evidence_loop',
  102. accountId,
  103. snapshotId: 'ip_snapshot_e2e_ready',
  104. positioningProposalId: 'ip_positioning_proposal_e2e_ready',
  105. sourceMode: 'llm',
  106. sourceLabel: 'LLM 证据诊断',
  107. diagnosisSummary: {},
  108. evidenceRefs: [],
  109. directions: [],
  110. taskSuggestions: [],
  111. createdAt: now,
  112. updatedAt: now,
  113. }];
  114. plan.topics = (plan.topics || []).map((topic: Record<string, unknown>, index: number) => ({
  115. ...topic,
  116. evidenceRefs: index === 0 ? [ownedWorkId, commentId] : [ownedWorkId],
  117. }));
  118. localStorage.setItem(planKey, JSON.stringify(plan));
  119. const directionIndexKey = `tiktok.ipOperator.contentDirections.${scope}`;
  120. for (const directionId of JSON.parse(localStorage.getItem(directionIndexKey) || '[]')) {
  121. const key = `tiktok.ipOperator.contentDirection.${directionId}.${scope}`;
  122. const direction = JSON.parse(localStorage.getItem(key) || '{}');
  123. direction.sourceEvidenceIds = [ownedWorkId, benchmarkId];
  124. direction.commentPainRefs = [painId];
  125. localStorage.setItem(key, JSON.stringify(direction));
  126. }
  127. const taskIndexKey = `tiktok.ipOperator.operationTasks.${scope}`;
  128. for (const taskId of JSON.parse(localStorage.getItem(taskIndexKey) || '[]')) {
  129. const key = `tiktok.ipOperator.operationTask.${taskId}.${scope}`;
  130. const task = JSON.parse(localStorage.getItem(key) || '{}');
  131. task.relatedEvidenceIds = [ownedWorkId, commentId];
  132. localStorage.setItem(key, JSON.stringify(task));
  133. }
  134. const proposalKey = `tiktok.ipOperator.positioningProposal.ip_positioning_proposal_e2e_ready.${scope}`;
  135. const proposal = JSON.parse(localStorage.getItem(proposalKey) || '{}');
  136. proposal.evidenceItemIds = [ownedWorkId, commentId];
  137. localStorage.setItem(proposalKey, JSON.stringify(proposal));
  138. localStorage.setItem('tiktok.currentTab', 'ip-operator');
  139. });
  140. }
  141. test('IP operator shows traceable evidence drawer for direction conclusions', async ({ page }) => {
  142. await seedEvidenceLoop(page);
  143. await page.goto('/');
  144. await expect(page.getByRole('heading', { name: 'IP操盘工作台' })).toBeVisible();
  145. await page.getByRole('button', { name: /方向选题/ }).click();
  146. await page.getByRole('button', { name: '查看方向证据' }).first().click();
  147. const drawer = page.getByRole('complementary', { name: '证据来源' });
  148. await expect(drawer).toBeVisible();
  149. await expect(drawer.getByText('自有账号作品').first()).toBeVisible();
  150. await expect(drawer.getByText('评论痛点').first()).toBeVisible();
  151. await expect(drawer.getByText('对标作品').first()).toBeVisible();
  152. });