ip-operator-scenario-seed.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. (() => {
  2. const scope = 'local-guest';
  3. function seedIpOperatorScenario(scenario = 'ready') {
  4. const now = new Date().toISOString();
  5. const user = {
  6. objectId: scope,
  7. username: 'ip-operator-e2e',
  8. displayName: 'IP Operator E2E',
  9. role: 'user',
  10. createdAt: now,
  11. };
  12. clearIpOperatorData();
  13. localStorage.setItem('videoWorkflow.authSession', JSON.stringify({ token: 'ip-operator-e2e-token', user }));
  14. localStorage.setItem('tiktok.currentTab', 'ip-operator');
  15. sessionStorage.clear();
  16. if (scenario === 'empty') return { scenario, profileId: '', planId: '' };
  17. const profileId = `ip_profile_e2e_${scenario}`;
  18. const planId = `ip_plan_e2e_${scenario}`;
  19. const profile = createProfile(profileId, now);
  20. const plan = createPlan({ scenario, profileId, planId, now });
  21. localStorage.setItem(`tiktok.ipOperator.profiles.${scope}`, JSON.stringify([profileId]));
  22. localStorage.setItem(`tiktok.ipOperator.profile.${profileId}.${scope}`, JSON.stringify(profile));
  23. localStorage.setItem(`tiktok.ipOperator.plans.${scope}`, JSON.stringify([planId]));
  24. localStorage.setItem(`tiktok.ipOperator.plan.${planId}.${scope}`, JSON.stringify(plan));
  25. localStorage.setItem(`tiktok.ipOperator.activeProfileId.${scope}`, profileId);
  26. localStorage.setItem(`tiktok.ipOperator.auditTrail.${scope}`, JSON.stringify([{
  27. id: 'ip_audit_e2e_switch',
  28. action: 'switch_profile',
  29. entityType: 'profile',
  30. entityId: profileId,
  31. userId: scope,
  32. createdAt: now,
  33. summary: `Switch current IP profile: ${profileId}`,
  34. }]));
  35. seedAccountWorkbench({ now, profileId, planId, scenario });
  36. return { scenario, profileId, planId };
  37. }
  38. function seedAccountWorkbench({ now, profileId, planId, scenario }) {
  39. const accountId = `ip_account_e2e_${scenario}`;
  40. const snapshotId = `ip_snapshot_e2e_${scenario}`;
  41. const proposalId = `ip_positioning_proposal_e2e_${scenario}`;
  42. const versionId = `ip_positioning_version_e2e_${scenario}`;
  43. const account = {
  44. id: accountId,
  45. userId: scope,
  46. platform: 'douyin',
  47. role: 'owned',
  48. displayName: '林川老板增长笔记',
  49. profileId,
  50. planId,
  51. homepageUrl: 'https://www.douyin.com/user/MS4wLjABAAAA_e2e',
  52. secUserId: 'MS4wLjABAAAA_e2e',
  53. accountId: 'MS4wLjABAAAA_e2e',
  54. intendedTrack: '传统行业老板 IP',
  55. intendedPersona: '增长顾问',
  56. intendedAudience: '传统行业中小企业老板',
  57. enabled: true,
  58. lastRefreshStatus: 'completed',
  59. lastRefreshedAt: now,
  60. createdAt: now,
  61. updatedAt: now,
  62. };
  63. const works = Array.from({ length: 10 }, (_, index) => {
  64. const awemeId = `738000000${index}`;
  65. const deep = index < 3;
  66. return {
  67. id: `work_${accountId}_${awemeId}`,
  68. accountId,
  69. awemeId,
  70. title: index === 0 ? '老板 IP 如何避免成为广告号' : `老板增长内容复盘 ${index + 1}`,
  71. desc: index === 0 ? '老板 IP 如何避免成为广告号,先讲信任再讲产品。' : `传统老板账号第 ${index + 1} 条内容复盘`,
  72. coverUrl: index === 0 ? 'http://127.0.0.1:9/expired-douyin-cover.jpg' : '',
  73. coverCandidates: index === 0
  74. ? [
  75. 'http://127.0.0.1:9/expired-douyin-cover.jpg',
  76. 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2272%22 height=%2296%22%3E%3Crect width=%2272%22 height=%2296%22 fill=%22%230f766e%22/%3E%3C/svg%3E',
  77. ]
  78. : [],
  79. publishTime: now,
  80. url: `https://www.douyin.com/video/${awemeId}`,
  81. metrics: {
  82. playCount: 10000 - index * 600,
  83. likeCount: 320 - index * 12,
  84. commentCount: 46 - index * 3,
  85. collectCount: 80 - index * 4,
  86. shareCount: 20 - index,
  87. },
  88. interactionScore: 320 - index * 12 + (46 - index * 3) * 4 + (80 - index * 4) * 3 + (20 - index) * 5,
  89. isDeepSampled: deep,
  90. structure: {
  91. hook: '为什么你的老板号越发越像广告?',
  92. topic: '老板 IP 信任表达',
  93. style: '问题/避坑表达',
  94. cta: '评论区留下最卡的问题',
  95. },
  96. comments: deep ? Array.from({ length: 30 }, (_, commentIndex) => ({
  97. id: `comment_${index + 1}_${commentIndex + 1}`,
  98. workId: `work_${accountId}_${awemeId}`,
  99. text: commentIndex % 3 === 0
  100. ? '我们传统行业老板确实不知道怎么讲案例'
  101. : commentIndex % 3 === 1
  102. ? '想知道怎么把客户问题变成选题'
  103. : '不要讲太虚,最好给具体脚本结构',
  104. likeCount: 12 - (commentIndex % 5),
  105. replyCount: commentIndex % 4,
  106. authorName: `用户${commentIndex + 1}`,
  107. capturedAt: now,
  108. })) : [],
  109. capturedAt: now,
  110. };
  111. });
  112. const snapshot = {
  113. id: snapshotId,
  114. accountId,
  115. profile: {
  116. nickname: '林川老板增长笔记',
  117. signature: '传统行业老板 IP 增长与短视频内容转化',
  118. avatarUrl: '',
  119. followerCount: 2860,
  120. followingCount: 38,
  121. totalFavorited: 15800,
  122. awemeCount: 42,
  123. },
  124. works,
  125. dataMode: 'data_diagnosis',
  126. evidenceItemIds: [],
  127. capturedAt: now,
  128. warnings: [],
  129. };
  130. const proposedVersion = {
  131. accountId,
  132. targetAudience: '传统行业中小企业老板',
  133. persona: '传统行业老板增长顾问',
  134. followReason: '用真实案例和短视频结构帮助老板解决表达、信任和转化问题。',
  135. contentPillars: ['老板 IP 定位与认知纠偏', '老板表达案例与完整方法', '真实项目复盘与评论答疑'],
  136. expressionStyle: ['口语化', '案例优先', '前 3 秒直接给判断'],
  137. boundaries: ['不夸大收益', '不照搬对标账号话术', '不做无关泛流量内容'],
  138. suitableViralPatterns: ['反常识开头', '问题-方案结构', '评论问题反推选题'],
  139. evidenceItemIds: [],
  140. assumptionsToValidate: ['关注理由是否清晰', '评论痛点能否反推选题'],
  141. observeMetrics: ['播放量', '评论数', '收藏数', '涨粉'],
  142. };
  143. const proposal = {
  144. id: proposalId,
  145. accountId,
  146. snapshotId,
  147. status: 'proposed',
  148. reason: '基于最近作品表现、重点评论和对标证据生成的定位提案。',
  149. proposedVersion,
  150. evidenceItemIds: [],
  151. createdAt: now,
  152. updatedAt: now,
  153. };
  154. const diagnosis = {
  155. id: `ip_account_diagnosis_e2e_${scenario}`,
  156. accountId,
  157. snapshotId,
  158. mode: 'data_diagnosis',
  159. reportMarkdown: [
  160. '# 账号诊断报告',
  161. '',
  162. '## 当前账号像什么 IP',
  163. '当前更像传统行业老板增长顾问,内容需要继续收窄到老板表达、信任和转化。',
  164. '',
  165. '## 高互动作品共性',
  166. '问题开头、具体场景、明确判断。',
  167. '',
  168. '## 评论区用户关心的问题',
  169. '用户集中关心如何讲案例、如何把客户问题变成选题。',
  170. '',
  171. '## 当前最大增长障碍',
  172. '内容主题仍有分散,用户关注理由需要稳定。',
  173. '',
  174. '## 未来 7 天优先动作',
  175. '确认定位版本,分别测试涨粉、信任、互动三个方向。',
  176. ].join('\n'),
  177. scores: {
  178. positioningClarity: 82,
  179. contentStructure: 78,
  180. interactionConversion: 75,
  181. recognizability: 80,
  182. sustainability: 76,
  183. },
  184. evidenceItemIds: [],
  185. topWorkIds: works.slice(0, 3).map((item) => item.id),
  186. commentSampleCount: 90,
  187. createdAt: now,
  188. updatedAt: now,
  189. };
  190. const version = {
  191. id: versionId,
  192. ...proposedVersion,
  193. version: 1,
  194. status: 'active',
  195. createdAt: now,
  196. updatedAt: now,
  197. };
  198. const directions = [
  199. createDirection(accountId, versionId, 'growth', '老板 IP 定位与认知纠偏', '围绕老板做内容时的常见误区和判断标准,帮助新用户快速建立正确认知。', ['播放量', '分享数', '新粉'], now),
  200. createDirection(accountId, versionId, 'trust', '老板表达案例与完整方法', '通过真实案例、表达框架和执行过程建立专业信任。', ['收藏数', '咨询意向', '关注率'], now),
  201. createDirection(accountId, versionId, 'interaction', '真实项目复盘与评论答疑', '把高互动评论归纳成问题类型,用项目复盘和集中答疑形成持续栏目。', ['评论数', '评论率', '高频问题'], now),
  202. ];
  203. const tasks = [
  204. createTask(accountId, 'today', 'positioning', '确认账号当前定位提案', '定位版本会约束后续方向、选题、脚本和发布包。', 95, now),
  205. createTask(accountId, 'today', 'direction', '确认涨粉破圈方向', '先稳定最影响播放和涨粉的内容方向。', 90, now),
  206. createTask(accountId, 'this_week', 'pain', '复核重点作品评论痛点', '评论问题能反推互动型选题和脚本切入点。', 82, now),
  207. createTask(accountId, 'later', 'publish_pack', '绑定发布包与真实作品', '发布后绑定作品才能形成复盘闭环。', 72, now),
  208. ];
  209. const binding = {
  210. id: `publish_binding_${planId}_1`,
  211. publishPackageId: `${planId}_publish_1`,
  212. accountId,
  213. awemeId: works[0].awemeId,
  214. workUrl: works[0].url,
  215. status: 'auto_matched',
  216. matchScore: 86,
  217. matchReason: '根据标题/文案相似度自动匹配,请人工复核。',
  218. createdAt: now,
  219. updatedAt: now,
  220. };
  221. saveEntity('tiktok.ipOperator.accounts', 'tiktok.ipOperator.account.', account);
  222. saveEntity('tiktok.ipOperator.accountSnapshots', 'tiktok.ipOperator.accountSnapshot.', snapshot);
  223. saveEntity('tiktok.ipOperator.accountDiagnoses', 'tiktok.ipOperator.accountDiagnosis.', diagnosis);
  224. saveEntity('tiktok.ipOperator.positioningProposals', 'tiktok.ipOperator.positioningProposal.', proposal);
  225. saveEntity('tiktok.ipOperator.positioningVersions', 'tiktok.ipOperator.positioningVersion.', version);
  226. directions.forEach((item) => saveEntity('tiktok.ipOperator.contentDirections', 'tiktok.ipOperator.contentDirection.', item));
  227. tasks.forEach((item) => saveEntity('tiktok.ipOperator.operationTasks', 'tiktok.ipOperator.operationTask.', item));
  228. saveEntity('tiktok.ipOperator.publishBindings', 'tiktok.ipOperator.publishBinding.', binding);
  229. }
  230. function createDirection(accountId, positioningVersionId, role, title, purpose, observeMetrics, now) {
  231. return {
  232. id: `direction_${accountId}_${role}`,
  233. accountId,
  234. positioningVersionId,
  235. role,
  236. title,
  237. targetAudience: role === 'growth' ? '尚未关注账号的新用户' : role === 'trust' ? '正在判断账号是否可靠的用户' : '有具体问题并愿意评论追问的用户',
  238. purpose,
  239. sourceEvidenceIds: [],
  240. viralPatternRefs: ['反常识开头', '问题-方案结构'],
  241. commentPainRefs: [],
  242. topicIds: [],
  243. cadenceSuggestion: role === 'growth' ? '每周 2-3 条' : '每周 1-2 条',
  244. observeMetrics,
  245. createdAt: now,
  246. updatedAt: now,
  247. };
  248. }
  249. function createTask(accountId, column, type, title, reason, growthImpactScore, now) {
  250. return {
  251. id: `task_${accountId}_${type}_${column}`,
  252. accountId,
  253. column,
  254. type,
  255. title,
  256. reason,
  257. growthImpactScore,
  258. relatedEvidenceIds: [],
  259. createdAt: now,
  260. updatedAt: now,
  261. };
  262. }
  263. function saveEntity(indexKey, prefix, entity) {
  264. const scopedIndex = `${indexKey}.${scope}`;
  265. const ids = JSON.parse(localStorage.getItem(scopedIndex) || '[]');
  266. localStorage.setItem(scopedIndex, JSON.stringify(Array.from(new Set([entity.id, ...ids]))));
  267. localStorage.setItem(`${prefix}${entity.id}.${scope}`, JSON.stringify(entity));
  268. }
  269. function clearIpOperatorData() {
  270. for (const key of Object.keys(localStorage)) {
  271. if (
  272. key.startsWith('tiktok.ipOperator.')
  273. || key === 'videoWorkflow.authSession'
  274. || key === 'videoWorkflow.topicPool.items'
  275. ) {
  276. localStorage.removeItem(key);
  277. }
  278. }
  279. }
  280. function createProfile(id, now) {
  281. return {
  282. id,
  283. userId: scope,
  284. name: 'E2E IP Operator Profile',
  285. stage: 'started',
  286. identity: 'Traditional industry consultant for local business owners',
  287. industry: 'Local service business',
  288. targetAudience: 'Owners who need practical IP-driven content and trust conversion',
  289. audiencePainPoints: ['Content looks like advertising', 'No stable topic system'],
  290. productsOrServices: 'IP positioning and content operation consulting',
  291. personalStories: ['Helped a local service team turn cases into short video topics'],
  292. expertise: ['Positioning', 'Topic planning', 'Script diagnosis'],
  293. expressionStyle: ['Direct', 'Practical'],
  294. boundaries: ['No exaggerated income promise'],
  295. goals: ['Build trust', 'Collect consultation leads'],
  296. questionnaire: [],
  297. createdAt: now,
  298. updatedAt: now,
  299. };
  300. }
  301. function createPlan({ scenario, profileId, planId, now }) {
  302. const topics = createTopics();
  303. const scripts = createScripts(topics);
  304. const failed = scenario === 'failed';
  305. const needsInput = scenario === 'needs_input';
  306. return {
  307. id: planId,
  308. userId: scope,
  309. profileId,
  310. status: failed ? 'failed' : needsInput ? 'needs_input' : 'ready',
  311. generationProgress: [
  312. { stage: 'intake_summary', label: 'Intake summary', status: 'completed', durationMs: 120 },
  313. { stage: 'diagnosis', label: 'Diagnosis', status: failed ? 'failed' : 'completed', durationMs: 120, failureCode: failed ? 'llm_malformed_json' : undefined, errorMessage: failed ? 'Malformed JSON in diagnosis stage' : undefined },
  314. ],
  315. qualityCheck: {
  316. overallScore: needsInput ? 64 : failed ? 40 : 88,
  317. positioningClarityScore: needsInput ? 60 : 90,
  318. audienceClarityScore: needsInput ? 62 : 86,
  319. evidenceSupportScore: needsInput ? 50 : 84,
  320. benchmarkTransferScore: needsInput ? 58 : 88,
  321. topicUsabilityScore: needsInput ? 65 : 91,
  322. passed: !needsInput && !failed,
  323. blockingIssues: needsInput ? ['Missing real cases and proof material'] : failed ? ['Generation failed before quality gate'] : [],
  324. improvementSuggestions: needsInput ? ['Add two real client cases before syncing topics'] : ['Validate the first three full scripts with real material.'],
  325. },
  326. intakeSummary: {
  327. identitySummary: 'Local business IP operator',
  328. audienceSummary: 'Traditional business owners',
  329. advantageSummary: 'Can translate business cases into practical content',
  330. businessGoalSummary: 'Build trust and collect consultation leads',
  331. missingInputs: needsInput ? [{ type: 'case', description: 'Add real client before-after cases', priority: 'high' }] : [],
  332. },
  333. diagnosis: failed ? undefined : {
  334. positioningStatement: 'A practical operator who turns local business experience into trusted owner IP content.',
  335. targetAudience: {
  336. summary: 'Owners who need short video content but do not want to become ad accounts.',
  337. painPoints: ['No topic system', 'Content lacks trust', 'Hard to convert'],
  338. },
  339. marketOpportunity: 'Many local businesses have real cases but lack content packaging.',
  340. ipType: 'Operator partner',
  341. personaTags: ['Practical', 'Case-driven', 'Trust-first'],
  342. differentiation: 'Turns benchmark hot points into positioning-matched topics instead of copying formats.',
  343. contentTracks: ['Positioning diagnosis', 'Trust cases', 'Conversion path'],
  344. monetizationPath: 'Consultation and operation service leads.',
  345. risks: ['Avoid exaggerated conversion promises'],
  346. alternativeDirections: ['Focus on one local industry first'],
  347. missingInputs: needsInput ? [{ type: 'case', description: 'Add real proof material', priority: 'high' }] : [],
  348. },
  349. benchmarkAnalyses: failed ? [] : [{
  350. benchmarkId: 'benchmark_e2e_1',
  351. accountName: 'Benchmark Owner Account',
  352. positioning: 'Direct business advice for owners',
  353. contentColumns: ['Pain point diagnosis', 'Case breakdown'],
  354. viralPatterns: ['Question hook', 'Concrete contrast'],
  355. scriptStructure: 'Hook - pain - viewpoint - case - action',
  356. conversionPath: 'Content trust to consultation',
  357. borrowablePoints: ['Direct question hooks', 'Case contrast'],
  358. nonCopyablePoints: ['Personal background', 'Exact industry claims'],
  359. migrationDirection: 'Use the hook logic, replace the case and judgement with this IP assets.',
  360. fitScore: 86,
  361. }],
  362. opportunityMap: failed ? [] : [{
  363. id: 'ip_opp_e2e_1',
  364. type: 'Trust diagnosis',
  365. fitScore: 90,
  366. reason: 'Matches audience pain and available expertise.',
  367. audiencePainPoint: 'Users do not trust product-only content.',
  368. requiredMaterials: ['Before-after case'],
  369. extendableTopics: ['Ad account problem', 'Trust building'],
  370. riskBoundary: 'No guaranteed result claim.',
  371. }],
  372. topics: failed ? [] : topics,
  373. scripts: failed ? [] : scripts,
  374. sevenDayTestPlan: failed ? [] : topics.slice(0, 7).map((topic, index) => ({
  375. day: index + 1,
  376. topicId: topic.id,
  377. topicTitle: topic.title,
  378. testGoal: 'Validate hook clarity and trust response.',
  379. contentType: topic.trafficLayer,
  380. shootingFocus: 'Talk to camera with one real scenario.',
  381. observeMetrics: ['Completion', 'Comment quality', 'Consultation intent'],
  382. reviewQuestions: ['Did users understand the pain?', 'Was the CTA too hard?'],
  383. })),
  384. thirtyDayDirection: failed ? undefined : {
  385. columnStructure: [
  386. { column: 'Positioning diagnosis', ratio: 40, role: 'Trust entry' },
  387. { column: 'Trust content', ratio: 35, role: 'Relationship building' },
  388. { column: 'Conversion path', ratio: 25, role: 'Lead collection' },
  389. ],
  390. weeklyFocus: ['Validate hooks', 'Add case material', 'Test conversion CTA', 'Review topic columns'],
  391. materialDirections: ['Customer doubt list', 'Before-after cases', 'Service process screenshots'],
  392. iterationPath: ['Keep high-comment questions', 'Rewrite weak hooks', 'Promote proven topics'],
  393. },
  394. evidenceItems: [],
  395. commentPainInsights: [],
  396. accountStrategyReports: failed ? [] : createAccountStrategyReports({ scenario, planId, now }),
  397. calibrationRecords: failed ? [] : createCalibrationRecords({ scenario, planId, now }),
  398. publishRetrospectives: failed ? [] : createPublishRetrospectives({ scenario, planId, now }),
  399. commentLabelSystems: [],
  400. contentCalendar: failed ? [] : createContentCalendar({ planId, topics, now }),
  401. publishPackages: failed ? [] : createPublishPackages({ planId, topics, scripts, now }),
  402. retrospectiveFollowUpTopics: [],
  403. matrixAccounts: [],
  404. missingInputs: needsInput ? [{ type: 'case', description: 'Add real client before-after cases', priority: 'high' }] : [],
  405. errorMessage: failed ? 'Generation stopped because diagnosis JSON was malformed.' : undefined,
  406. createdAt: now,
  407. updatedAt: now,
  408. };
  409. }
  410. function createAccountStrategyReports({ scenario, planId, now }) {
  411. const accountId = `ip_account_e2e_${scenario}`;
  412. const snapshotId = `ip_snapshot_e2e_${scenario}`;
  413. const workId = `work_${accountId}_7380000000`;
  414. return [{
  415. id: `ip_strategy_report_e2e_${scenario}`,
  416. accountId,
  417. snapshotId,
  418. positioningProposalId: `ip_positioning_proposal_e2e_${scenario}`,
  419. sourceMode: 'llm',
  420. sourceLabel: 'LLM 证据诊断',
  421. diagnosisSummary: {
  422. accountSnapshot: '账号已读取最近作品和重点评论样本。',
  423. currentIpGuess: '传统行业老板增长顾问',
  424. actualAudience: '传统行业中小企业老板',
  425. positioningMismatch: '需要减少泛工具话题,强化经营问题和真实案例。',
  426. strongestFollowReason: '能把老板经营问题拆成短视频表达和执行步骤。',
  427. biggestGrowthBlocker: '定位校准和发布复盘还需要持续继承。',
  428. highInteractionPattern: '先指出老板号误区,再给可执行步骤。',
  429. commentPainSummary: '用户集中关心案例怎么讲、问题怎么变选题。',
  430. sevenDayPriority: '围绕经营问题继续测试 3 条内容。',
  431. },
  432. evidenceRefs: [{
  433. id: `strategy_ref_${scenario}_work_1`,
  434. sourceType: 'owned_work',
  435. sourceId: workId,
  436. label: '高互动经营问题作品',
  437. quote: '收藏和评论集中说明用户需要可执行的老板号内容顺序。',
  438. reason: '支持继续测试“经营问题拆解”方向,而不是复制原作品简介。',
  439. confidence: 'medium',
  440. gaps: ['仍需发布后转化表现'],
  441. analyzed: true,
  442. }],
  443. directions: [],
  444. taskSuggestions: [],
  445. appliedCalibrationIds: [`calibration_${planId}_1`],
  446. appliedCalibrationSummary: ['已采用用户校准:减少纯工具合集,强调老板经营问题。'],
  447. confidenceLevel: 'medium',
  448. evidenceGaps: ['仍缺发布后复盘数据的长期对比'],
  449. dataScopeSummary: {
  450. workCount: 10,
  451. commentCount: 90,
  452. hasPositioningVersion: true,
  453. calibrationCount: 1,
  454. publishBindingCount: 1,
  455. },
  456. createdAt: now,
  457. updatedAt: now,
  458. }];
  459. }
  460. function createCalibrationRecords({ scenario, planId, now }) {
  461. const accountId = `ip_account_e2e_${scenario}`;
  462. return [{
  463. id: `calibration_${planId}_1`,
  464. accountId,
  465. reportId: `ip_strategy_report_e2e_${scenario}`,
  466. source: 'manual_calibration',
  467. accuracy: 'partial',
  468. targetAudienceNotes: '更偏传统行业中小企业老板。',
  469. positioningNotes: '减少纯工具合集,强调老板经营问题。',
  470. personaNotes: '懂业务的增长顾问。',
  471. forbiddenTopics: ['纯工具合集'],
  472. forbiddenExpressions: ['保姆级万能'],
  473. confirmedDirectionIds: [`direction_${accountId}_trust`],
  474. rejectedDirectionIds: [],
  475. rejectedEvidenceIds: [],
  476. operatorExperienceNotes: '历史内容里案例拆解更容易带来收藏和有效评论。',
  477. createdAt: now,
  478. updatedAt: now,
  479. }];
  480. }
  481. function createPublishRetrospectives({ scenario, planId, now }) {
  482. const accountId = `ip_account_e2e_${scenario}`;
  483. return [{
  484. id: `publish_review_${planId}_1`,
  485. accountId,
  486. publishPackageId: `${planId}_publish_1`,
  487. bindingId: `publish_binding_${planId}_1`,
  488. workId: `work_${accountId}_7380000000`,
  489. outcome: 'met_expectation',
  490. expectedGoal: ['验证信任表达是否能带来评论和关注'],
  491. actualSignals: ['收藏和评论达到预期', '评论集中在案例怎么讲'],
  492. diagnosis: '信任表达方向可继续测试,但需要增加真实案例材料。',
  493. nextAction: 'continue',
  494. calibrationRecordId: `calibration_${planId}_1`,
  495. createdAt: now,
  496. updatedAt: now,
  497. }];
  498. }
  499. function createTopics() {
  500. return Array.from({ length: 12 }, (_, index) => ({
  501. id: `ip_topic_e2e_${index + 1}`,
  502. title: index === 0 ? 'Why owner IP should not look like an ad account' : `Owner IP trust topic ${index + 1}`,
  503. column: index % 3 === 0 ? 'Positioning diagnosis' : index % 3 === 1 ? 'Trust content' : 'Conversion path',
  504. trafficLayer: index < 4 ? 'vertical' : index < 8 ? 'conversion' : 'broad',
  505. contentGoal: 'Help the target user understand the trust problem and next action',
  506. priority: index < 4 ? 'high' : index < 9 ? 'medium' : 'low',
  507. qualityLevel: index < 4 ? 'make_now' : index < 8 ? 'polish_first' : 'needs_material',
  508. fitReason: 'Derived from benchmark hooks and adapted to this IP positioning',
  509. source: 'Benchmark migration',
  510. requiredMaterials: ['Real case', 'Service before-after comparison'],
  511. scriptStatus: index < 3 ? 'full' : 'outline',
  512. riskNote: 'Avoid hard selling and exaggerated claims',
  513. }));
  514. }
  515. function createScripts(topics) {
  516. return topics.map((topic, index) => ({
  517. id: `ip_script_e2e_${index + 1}`,
  518. topicId: topic.id,
  519. type: index < 3 ? 'full' : 'outline',
  520. title: topic.title,
  521. hook: 'If your account keeps introducing products, users will treat it as advertising.',
  522. painPoint: 'The content has information but no trust context.',
  523. viewpoint: 'Owner IP must first transmit judgement, not product inventory.',
  524. caseOrMethod: 'Use one benchmark hook, one real scenario, and one conversion boundary.',
  525. goldenSentence: 'Trust comes before transaction.',
  526. closingCta: 'Save this and check whether your next video is selling or building trust.',
  527. fullScript: index < 3
  528. ? 'Opening: many owner accounts look professional but still fail to convert. Middle: users do not reject business, they reject content that only pushes products. Example: when a local service owner changed from product introductions to decision-making stories, consultation quality improved. Ending: the next video should answer one real customer doubt before mentioning the product.'
  529. : '',
  530. shootingTips: ['Talk to camera', 'Use one service case'],
  531. subtitleHighlights: ['Do not sell first', 'Show judgement first'],
  532. replaceableSlots: ['Insert local case'],
  533. riskNote: 'Do not claim guaranteed conversion.',
  534. }));
  535. }
  536. function createContentCalendar({ planId, topics, now }) {
  537. return [{
  538. id: `${planId}_calendar_1`,
  539. planId,
  540. topicId: topics[0]?.id,
  541. scriptId: 'ip_script_e2e_1',
  542. date: now.slice(0, 10),
  543. timeSlot: '19:30',
  544. platform: 'douyin',
  545. accountName: '林川老板增长笔记',
  546. pillar: '信任建立内容',
  547. title: '老板 IP 如何避免成为广告号',
  548. contentGoal: '验证信任表达是否能带来评论和关注',
  549. materialNeeds: ['真实账号截图', '客户问题截图'],
  550. evidenceItemIds: [],
  551. painInsightIds: [],
  552. status: 'ready_for_review',
  553. publishPackageId: `${planId}_publish_1`,
  554. createdAt: now,
  555. updatedAt: now,
  556. }];
  557. }
  558. function createPublishPackages({ planId, topics, scripts, now }) {
  559. return [{
  560. id: `${planId}_publish_1`,
  561. calendarItemId: `${planId}_calendar_1`,
  562. topicId: topics[0]?.id || '',
  563. scriptId: scripts[0]?.id || '',
  564. platform: 'douyin',
  565. accountName: '林川老板增长笔记',
  566. titleOptions: ['老板 IP 如何避免成为广告号', '为什么你的老板号越发越不被信任'],
  567. coverSuggestions: ['左侧:广告号,右侧:信任号', '老板别再只讲产品'],
  568. caption: '老板 IP 如何避免成为广告号,先讲信任再讲产品。',
  569. scriptText: scripts[0]?.fullScript || scripts[0]?.hook || '',
  570. hashtags: ['老板IP', '短视频运营', '账号定位'],
  571. materialFiles: [
  572. { id: 'material_1', name: '账号主页截图', status: 'missing' },
  573. { id: 'material_2', name: '客户问题截图', status: 'missing' },
  574. ],
  575. scheduleSuggestion: '今天 19:30 人工发布,发布后回填作品链接。',
  576. isOriginal: true,
  577. isDraft: true,
  578. materialChecklist: [
  579. { id: 'material_check_1', label: '确认案例素材可公开', checked: false, required: true },
  580. { id: 'material_check_2', label: '检查封面文字不夸张', checked: true, required: true },
  581. ],
  582. riskChecklist: [
  583. { id: 'risk_check_1', label: '不承诺具体收益', checked: true, required: true },
  584. { id: 'risk_check_2', label: '不使用诱导互动话术', checked: true, required: true },
  585. ],
  586. platformVariants: {
  587. xiaohongshu: {
  588. title: '老板 IP 别再拍成广告号',
  589. coverText: '先讲信任,再讲产品',
  590. imageNoteOutline: ['问题', '原因', '改法'],
  591. hashtags: ['老板IP', '内容运营'],
  592. },
  593. douyin: {
  594. firstThreeSecondsHook: '为什么你的老板号越发越像广告?',
  595. shotList: ['正面口播', '账号截图', '标题对比'],
  596. spokenScript: scripts[0]?.fullScript || '',
  597. subtitleEmphasis: ['别急着卖产品', '先给关注理由'],
  598. },
  599. wechat: {
  600. calmerTitle: '老板内容如何建立信任',
  601. intro: '很多老板号的问题不是不专业,而是太像广告。',
  602. body: '先讲清楚用户问题,再放服务能力。',
  603. endingCta: '欢迎把你的账号问题发来一起看。',
  604. },
  605. },
  606. evidenceItemIds: [],
  607. painInsightIds: [],
  608. status: 'ready_for_review',
  609. createdAt: now,
  610. updatedAt: now,
  611. }];
  612. }
  613. window.__seedIpOperatorScenario = seedIpOperatorScenario;
  614. })();