| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- import { of, throwError } from 'rxjs';
- import { describe, expect, it, vi } from 'vitest';
- import {
- IpAccountSnapshot,
- IpAccountWorkSnapshot,
- IpAccountWorkTranscriptSnapshot,
- IpMonitoredAccount,
- } from '../models/ip-operator.model';
- import { IpAccountWorkAnalysisService } from './ip-account-work-analysis.service';
- import { IpAccountWorkReportService } from './ip-account-work-report.service';
- import { buildAccountWorkAnalysisPrompt } from './ip-operator-prompts';
- import { LlmService } from './llm.service';
- describe('buildAccountWorkAnalysisPrompt', () => {
- it('uses transcript and comments for one work without including other works', () => {
- const prompt = buildAccountWorkAnalysisPrompt({
- accountName: 'FredTalk',
- accountIntentSummary: '账号定位摘要,不应包含第二个作品文字稿',
- work: {
- workId: 'work-1',
- awemeId: 'aweme-1',
- title: '新手小白安装这六个 Skills 就行够了',
- desc: '工具安装顺序和选择建议',
- metricsSummary: '点赞 58850 / 评论 1036',
- transcriptText: '当前作品完整视频文字稿:先判断任务,再选择工具,并说明安装顺序。',
- comments: [
- { id: 'comment-1', text: '刚开始不知道先用哪个工具', likeCount: 20 },
- ],
- },
- });
- expect(prompt).toContain('当前作品完整视频文字稿');
- expect(prompt).toContain('刚开始不知道先用哪个工具');
- expect(prompt).toContain('一次只分析当前 workId 对应的一个作品');
- expect(prompt).toContain('评论只能作为辅助证据');
- expect(prompt).not.toContain('其他作品完整视频文字稿');
- });
- });
- describe('IpAccountWorkAnalysisService', () => {
- it('generates one LLM work report from one transcript-backed work', async () => {
- const llm = llmMock(JSON.stringify({
- workId: 'work-1',
- reports: [{
- kind: 'structure',
- headline: '先纠偏再给步骤',
- summary: '文字稿先指出新手误区,再给工具选择顺序。',
- evidenceSignals: ['文字稿出现安装误区', '收藏和评论都高'],
- strategyJudgment: '适合沉淀成新手工具选择栏目。',
- supportingEvidenceIds: ['work-1', 'comment-1'],
- confidence: 'high',
- gaps: [],
- }, {
- kind: 'operation',
- headline: '可迁移为答疑栏目',
- summary: '评论说明用户在开工顺序上有强需求。',
- evidenceSignals: ['评论问先用哪个', '分享收藏高'],
- strategyJudgment: '后续可以围绕真实项目阶段持续答疑。',
- supportingEvidenceIds: ['comment-1'],
- confidence: 'medium',
- gaps: ['需要验证转粉数据'],
- }],
- }));
- const service = new IpAccountWorkAnalysisService(
- llm as unknown as LlmService,
- new IpAccountWorkReportService(),
- );
- const result = await service.analyzeWork({
- account: accountFixture(),
- snapshot: snapshotFixture(),
- work: workFixture(),
- transcript: transcriptFixture(),
- });
- expect(result.run.status).toBe('completed');
- expect(result.run.failureReason).toBeUndefined();
- expect(result.report?.sourceMode).toBe('llm');
- expect(result.report?.workId).toBe('work-1');
- expect(result.report?.reports).toHaveLength(2);
- expect(result.report?.reports[0].summary).toContain('文字稿先指出');
- expect(result.run.diagnostics.map((item) => item.stage)).toEqual([
- 'context_built',
- 'llm_request',
- 'llm_response',
- 'llm_parse',
- 'result_built',
- ]);
- const prompt = llm.askWithSystem.mock.calls[0][1] as string;
- expect(prompt).toContain(transcriptFixture().text);
- expect(prompt).toContain('刚开始不知道先用哪个工具');
- });
- it('records LLM failure and returns a local fallback report without pretending it is LLM analysis', async () => {
- const llm = {
- askWithSystem: vi.fn().mockReturnValue(throwError(() => new Error('Timeout has occurred'))),
- };
- const service = new IpAccountWorkAnalysisService(
- llm as unknown as LlmService,
- new IpAccountWorkReportService(),
- );
- const result = await service.analyzeWork({
- account: accountFixture(),
- snapshot: snapshotFixture(),
- work: workFixture(),
- transcript: transcriptFixture(),
- });
- expect(result.run.status).toBe('failed');
- expect(result.run.failureReason).toContain('Timeout has occurred');
- expect(result.report?.sourceMode).toBe('local_fallback');
- expect(result.report?.reports[0].gaps).toContain('未经过 LLM 作品级分析');
- expect(result.run.diagnostics.some((item) => item.stage === 'llm_fallback' && item.status === 'fallback')).toBe(true);
- });
- it('fails before LLM request when transcript text is empty', async () => {
- const llm = llmMock('{}');
- const service = new IpAccountWorkAnalysisService(
- llm as unknown as LlmService,
- new IpAccountWorkReportService(),
- );
- const result = await service.analyzeWork({
- account: accountFixture(),
- snapshot: snapshotFixture(),
- work: workFixture(),
- transcript: { ...transcriptFixture(), text: ' ' },
- });
- expect(result.run.status).toBe('failed');
- expect(result.run.failureReason).toContain('缺少视频文字稿');
- expect(result.report).toBeUndefined();
- expect(llm.askWithSystem).not.toHaveBeenCalled();
- });
- });
- function llmMock(raw: string) {
- return {
- askWithSystem: vi.fn().mockReturnValue(of(raw)),
- };
- }
- function accountFixture(): IpMonitoredAccount {
- return {
- id: 'account-1',
- userId: 'user-1',
- platform: 'douyin',
- role: 'owned',
- displayName: 'FredTalk',
- accountId: '917997605',
- enabled: true,
- lastRefreshStatus: 'completed',
- createdAt: now(),
- updatedAt: now(),
- };
- }
- function snapshotFixture(): IpAccountSnapshot {
- return {
- id: 'snapshot-1',
- accountId: 'account-1',
- dataMode: 'data_diagnosis',
- capturedAt: now(),
- warnings: [],
- evidenceItemIds: [],
- profile: {
- nickname: 'FredTalk',
- signature: '关注小白如何从 0 到 1 学习 AI 实操能力',
- followerCount: 1000,
- followingCount: 10,
- totalFavorited: 2000,
- awemeCount: 10,
- },
- works: [workFixture(), {
- ...workFixture(),
- id: 'work-2',
- awemeId: 'aweme-2',
- title: '其他作品标题',
- transcript: {
- ...transcriptFixture(),
- text: '其他作品完整视频文字稿,不应该进入单作品 prompt。',
- },
- }],
- };
- }
- function workFixture(): IpAccountWorkSnapshot {
- return {
- id: 'work-1',
- accountId: 'account-1',
- awemeId: 'aweme-1',
- title: '新手小白安装这六个 Skills 就行够了',
- desc: '工具安装顺序和选择建议',
- publishTime: '2026-06-30T00:00:00.000Z',
- metrics: { playCount: 0, likeCount: 58850, commentCount: 1036, collectCount: 61773, shareCount: 9581 },
- interactionScore: 120,
- isDeepSampled: true,
- structure: { hook: '新手安装清单', topic: '工具选择', style: '教程', cta: '收藏' },
- comments: [
- { id: 'comment-1', workId: 'work-1', text: '刚开始不知道先用哪个工具', likeCount: 20, capturedAt: '2026-06-30T01:00:00.000Z' },
- { id: 'comment-2', workId: 'work-1', text: '安装了很多但是不知道流程', likeCount: 12, capturedAt: '2026-06-30T02:00:00.000Z' },
- ],
- capturedAt: now(),
- };
- }
- function transcriptFixture(): IpAccountWorkTranscriptSnapshot {
- return {
- text: '当前作品完整视频文字稿:先判断任务,再选择对应工具。视频逐步讲了安装顺序、常见误区和每个工具的使用边界。',
- source: 'asr',
- confidence: 'high',
- capturedAt: now(),
- };
- }
- function now(): string {
- return '2026-07-01T00:00:00.000Z';
- }
|