mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
feat: 增加日报模板
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
buildGroupReportInput,
|
||||
parseGroupDailyReport
|
||||
@@ -29,6 +29,16 @@ const media = {
|
||||
funBadges: []
|
||||
}
|
||||
|
||||
const previousWindow = (globalThis as { window?: unknown }).window
|
||||
|
||||
afterEach(() => {
|
||||
if (previousWindow === undefined) {
|
||||
Reflect.deleteProperty(globalThis, 'window')
|
||||
} else {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow })
|
||||
}
|
||||
})
|
||||
|
||||
describe('group report parsing', () => {
|
||||
it('keeps only distinct real participants in the hero avatar list', () => {
|
||||
expect(
|
||||
@@ -113,6 +123,109 @@ describe('group report parsing', () => {
|
||||
expect(input.prompt).toContain('微信系统消息:由于账号安全原因,无法加入当前群聊。')
|
||||
})
|
||||
|
||||
it('injects successful image insights into the model prompt and reports partial failures', async () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
id: 'image-1',
|
||||
from: 'member',
|
||||
type: '图片',
|
||||
datetime: '2026-08-12 10:00:00',
|
||||
content: '[图片]',
|
||||
name: '成员一',
|
||||
isSender: false,
|
||||
sessionId: 'group@chatroom',
|
||||
contentData: { type: 'image', md5: 'a'.repeat(32), datName: 'one.dat' }
|
||||
},
|
||||
{
|
||||
id: 'image-2',
|
||||
from: 'member',
|
||||
type: '图片',
|
||||
datetime: '2026-08-12 10:01:00',
|
||||
content: '[图片]',
|
||||
name: '成员二',
|
||||
isSender: false,
|
||||
sessionId: 'group@chatroom',
|
||||
contentData: { type: 'image', md5: 'b'.repeat(32), datName: 'two.dat' }
|
||||
}
|
||||
]
|
||||
const progress = vi.fn()
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
api: {
|
||||
imageListCandidates: vi.fn(async () => ({
|
||||
success: true,
|
||||
candidates: [
|
||||
{
|
||||
messageId: 'image-1',
|
||||
imageHash: 'a'.repeat(32),
|
||||
md5: 'a'.repeat(32),
|
||||
datName: 'one.dat',
|
||||
sessionId: 'group@chatroom',
|
||||
sender: '成员一',
|
||||
sentAt: new Date('2026-08-12 10:00:00').getTime(),
|
||||
heatScore: 10
|
||||
},
|
||||
{
|
||||
messageId: 'image-2',
|
||||
imageHash: 'b'.repeat(32),
|
||||
md5: 'b'.repeat(32),
|
||||
datName: 'two.dat',
|
||||
sessionId: 'group@chatroom',
|
||||
sender: '成员二',
|
||||
sentAt: new Date('2026-08-12 10:01:00').getTime(),
|
||||
heatScore: 9
|
||||
}
|
||||
]
|
||||
})),
|
||||
getImage: vi.fn(async () => ({
|
||||
success: true,
|
||||
data: 'data:image/png;base64,fixture'
|
||||
})),
|
||||
imageAnalyze: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
insight: {
|
||||
id: 'insight-1',
|
||||
messageId: 'image-1',
|
||||
imageHash: 'a'.repeat(32),
|
||||
description: '一张表格型网页截图,包含多列数据。',
|
||||
ocrText: '项目 状态 负责人',
|
||||
tags: ['表格', '管理界面'],
|
||||
category: 'screenshot',
|
||||
importance: 'medium',
|
||||
provider: 'vision-provider',
|
||||
model: 'vision-model',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
sender: '成员一',
|
||||
sentAt: new Date('2026-08-12 10:00:00').getTime(),
|
||||
sessionId: 'group@chatroom'
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({ success: false, error: 'fetch failed' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const input = await buildGroupReportInput(messages, null, true, 'full', {
|
||||
onProgress: progress
|
||||
})
|
||||
|
||||
expect(input.prompt).toContain('AI 图片识别摘要:')
|
||||
expect(input.prompt).toContain('一张表格型网页截图,包含多列数据。')
|
||||
expect(input.prompt).toContain('OCR: 项目 状态 负责人')
|
||||
expect(input.imageInsightSummary).toMatchObject({ total: 2, succeeded: 1, failed: 1 })
|
||||
expect(input.imageInsightSummary.failures[0]).toMatchObject({
|
||||
messageId: 'image-2',
|
||||
error: 'fetch failed'
|
||||
})
|
||||
expect(progress).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ stage: 'recognizingImages', completed: 2, total: 2 })
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to topic keywords when the model omits top-level keywords', () => {
|
||||
const report = parseGroupDailyReport(
|
||||
JSON.stringify({
|
||||
@@ -133,4 +246,66 @@ describe('group report parsing', () => {
|
||||
|
||||
expect(report.keywords).toEqual(['肌酸', '训练', '健身安排'])
|
||||
})
|
||||
|
||||
it('does not render the legacy gallery even when old report data contains it', () => {
|
||||
const report = parseGroupDailyReport(
|
||||
JSON.stringify({
|
||||
topics: [{ title: '图片话题', summary: '围绕图片展开讨论。', keywords: ['图片'] }]
|
||||
}),
|
||||
[],
|
||||
'',
|
||||
[],
|
||||
metadata,
|
||||
{
|
||||
gallery: [
|
||||
{
|
||||
sender: '成员一',
|
||||
time: '10:00',
|
||||
imageUrl: 'data:image/png;base64,fixture',
|
||||
note: '旧相册数据'
|
||||
}
|
||||
],
|
||||
voiceHighlights: [],
|
||||
funBadges: []
|
||||
}
|
||||
)
|
||||
|
||||
expect(report.media.gallery).toEqual([])
|
||||
expect(report.sectionMeta?.gallery).toMatchObject({ enabled: false, displayedCount: 0 })
|
||||
})
|
||||
|
||||
it('allows a text report with zero AI images when no image reaches the hot threshold', async () => {
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
api: {
|
||||
imageListCandidates: vi.fn(async () => ({ success: true, candidates: [] })),
|
||||
getImage: vi.fn(),
|
||||
imageAnalyze: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const input = await buildGroupReportInput(
|
||||
[
|
||||
{
|
||||
id: 'cold-image',
|
||||
from: 'member',
|
||||
type: '图片',
|
||||
datetime: '2026-08-12 10:00:00',
|
||||
content: '[图片]',
|
||||
name: '成员一',
|
||||
isSender: false,
|
||||
sessionId: 'group@chatroom',
|
||||
contentData: { type: 'image', md5: 'c'.repeat(32), datName: 'cold.dat' }
|
||||
}
|
||||
],
|
||||
null,
|
||||
true,
|
||||
'full'
|
||||
)
|
||||
|
||||
expect(input.imageInsightSummary).toMatchObject({ total: 0, succeeded: 0, failed: 0 })
|
||||
expect(input.media.gallery).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { getByHash } = vi.hoisted(() => ({ getByHash: vi.fn() }))
|
||||
|
||||
vi.mock('../../src/main/db/image-insights-store', () => ({
|
||||
imageInsightsStore: {
|
||||
getByHash,
|
||||
upsert: vi.fn(),
|
||||
listBySession: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
import { imageInsightService } from '../../src/main/services/image-insight-service'
|
||||
|
||||
const query = {
|
||||
sessionId: 'group@chatroom',
|
||||
startTime: 0,
|
||||
endTime: Date.now(),
|
||||
limit: 3
|
||||
}
|
||||
|
||||
const input = (id: string, responseCount: number, interactionCount: number): {
|
||||
messageId: string
|
||||
md5: string
|
||||
sessionId: string
|
||||
sender: string
|
||||
sentAt: number
|
||||
responseCount: number
|
||||
interactionCount: number
|
||||
} => ({
|
||||
messageId: id,
|
||||
md5: id.repeat(32).slice(0, 32),
|
||||
sessionId: 'group@chatroom',
|
||||
sender: id,
|
||||
sentAt: Date.now(),
|
||||
responseCount,
|
||||
interactionCount
|
||||
})
|
||||
|
||||
describe('ImageInsightService hot image selection', () => {
|
||||
beforeEach(() => {
|
||||
getByHash.mockReset()
|
||||
getByHash.mockReturnValue(null)
|
||||
})
|
||||
|
||||
it('returns fewer than three images when only two pass the hot threshold', async () => {
|
||||
const result = await imageInsightService.listTopHotImages(query, [
|
||||
input('a', 3, 0),
|
||||
input('b', 1, 1),
|
||||
input('c', 1, 0),
|
||||
input('d', 0, 5),
|
||||
input('e', 0, 0)
|
||||
])
|
||||
|
||||
expect(result.map((item) => item.messageId)).toEqual(['a', 'b'])
|
||||
})
|
||||
|
||||
it('keeps the three highest-scoring hot images when more are eligible', async () => {
|
||||
const result = await imageInsightService.listTopHotImages(query, [
|
||||
input('a', 2, 0),
|
||||
input('b', 5, 0),
|
||||
input('c', 1, 1),
|
||||
input('d', 3, 1)
|
||||
])
|
||||
|
||||
expect(result.map((item) => item.messageId)).toEqual(['b', 'd', 'a'])
|
||||
expect(result).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('returns no candidates when no image has meaningful follow-up activity', async () => {
|
||||
const result = await imageInsightService.listTopHotImages(query, [
|
||||
input('a', 1, 0),
|
||||
input('b', 0, 4),
|
||||
input('c', 0, 0)
|
||||
])
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps a cached insight attached to an eligible candidate', async () => {
|
||||
const cached = { imageHash: 'a'.repeat(32), description: '缓存识别结果' }
|
||||
getByHash.mockImplementation((hash: string) => (hash === 'a'.repeat(32) ? cached : null))
|
||||
|
||||
const result = await imageInsightService.listTopHotImages(query, [input('a', 2, 0)])
|
||||
|
||||
expect(result[0]).toMatchObject({ messageId: 'a', insight: cached })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,187 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||
import type { GroupDailyReport, GroupReportMetadata } from '../../src/shared/group-report'
|
||||
|
||||
const root = mkdtempSync(join(tmpdir(), 'tracememo-report-history-'))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => root },
|
||||
nativeImage: {
|
||||
createFromPath: () => ({
|
||||
isEmpty: () => false,
|
||||
getSize: () => ({ width: 430, height: 1200 })
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
const reportSnapshot = {
|
||||
overview: '已生成的结构化日报',
|
||||
topics: [],
|
||||
resources: [],
|
||||
importantMessages: [],
|
||||
quotes: [],
|
||||
qa: [],
|
||||
todos: [],
|
||||
unresolved: [],
|
||||
storylines: [],
|
||||
reversals: [],
|
||||
participantChains: [],
|
||||
analytics: {
|
||||
topicHeat: [],
|
||||
activeTimeline: '',
|
||||
topSpeakers: [],
|
||||
voiceLeaderboard: []
|
||||
},
|
||||
keywords: [],
|
||||
media: { gallery: [], visionGallery: [], voiceHighlights: [], funBadges: [] }
|
||||
} satisfies GroupDailyReport
|
||||
|
||||
const reportMetadata = {
|
||||
groupName: '测试群',
|
||||
reportDate: '2026-08-12',
|
||||
dateRange: '今天',
|
||||
messageCount: 10,
|
||||
activeUsers: 3,
|
||||
timeSpan: '09:00–18:00',
|
||||
generatedAt: '2026-08-12T10:00:00.000Z',
|
||||
recordNote: '',
|
||||
footerNote: '',
|
||||
heroParticipants: [],
|
||||
avatars: {}
|
||||
} satisfies GroupReportMetadata
|
||||
|
||||
describe('generated report template history', () => {
|
||||
afterAll(() => {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('replaces the current report assets while preserving its structured snapshot and id', async () => {
|
||||
const originalHtml = join(root, 'original.html')
|
||||
const switchedHtml = join(root, 'switched.html')
|
||||
writeFileSync(originalHtml, '<h1>Mobile 01</h1>')
|
||||
writeFileSync(switchedHtml, '<h1>Mobile 03</h1>')
|
||||
const { saveGeneratedReport, updateGeneratedReportTemplate } =
|
||||
await import('../../src/main/report-history-service')
|
||||
|
||||
const saved = await saveGeneratedReport({
|
||||
contactId: 'group-md5',
|
||||
contactName: '测试群',
|
||||
dateRange: '今天',
|
||||
messageCount: 10,
|
||||
generatedAt: '2026-08-12T10:00:00.000Z',
|
||||
generatedImage: `data:image/png;base64,${Buffer.from('mobile-01').toString('base64')}`,
|
||||
htmlPath: originalHtml,
|
||||
reportSnapshot,
|
||||
reportMetadata,
|
||||
templateId: 'mobile-feed'
|
||||
})
|
||||
expect(saved.success).toBe(true)
|
||||
expect(saved.record).toBeDefined()
|
||||
|
||||
const updated = await updateGeneratedReportTemplate({
|
||||
reportId: saved.record!.id,
|
||||
templateId: 'mobile-dashboard',
|
||||
generatedImage: `data:image/png;base64,${Buffer.from('mobile-03').toString('base64')}`,
|
||||
htmlPath: switchedHtml
|
||||
})
|
||||
|
||||
expect(updated.success).toBe(true)
|
||||
expect(updated.record).toMatchObject({
|
||||
id: saved.record!.id,
|
||||
templateId: 'mobile-dashboard',
|
||||
reportSnapshot,
|
||||
reportMetadata
|
||||
})
|
||||
expect(readFileSync(updated.record!.htmlPath!, 'utf8')).toContain('Mobile 03')
|
||||
expect(readFileSync(updated.record!.pngPath!).toString()).toBe('mobile-03')
|
||||
expect(JSON.parse(readFileSync(updated.record!.jsonPath!, 'utf8'))).toMatchObject({
|
||||
id: saved.record!.id,
|
||||
templateId: 'mobile-dashboard',
|
||||
reportSnapshot,
|
||||
reportMetadata
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps legacy records viewable but rejects a lossless template switch', async () => {
|
||||
const legacyHtml = join(root, 'legacy.html')
|
||||
writeFileSync(legacyHtml, '<h1>Legacy</h1>')
|
||||
const { saveGeneratedReport, updateGeneratedReportTemplate } =
|
||||
await import('../../src/main/report-history-service')
|
||||
const saved = await saveGeneratedReport({
|
||||
contactId: 'legacy-group',
|
||||
contactName: '旧报告',
|
||||
dateRange: '今天',
|
||||
messageCount: 5,
|
||||
generatedAt: '2026-08-12T11:00:00.000Z',
|
||||
generatedImage: `data:image/png;base64,${Buffer.from('legacy').toString('base64')}`,
|
||||
htmlPath: legacyHtml
|
||||
})
|
||||
|
||||
const updated = await updateGeneratedReportTemplate({
|
||||
reportId: saved.record!.id,
|
||||
templateId: 'desktop-editorial',
|
||||
generatedImage: `data:image/png;base64,${Buffer.from('new').toString('base64')}`,
|
||||
htmlPath: legacyHtml
|
||||
})
|
||||
|
||||
expect(updated).toEqual({
|
||||
success: false,
|
||||
error: '旧报告未保存结构化数据,无法无损切换模板'
|
||||
})
|
||||
})
|
||||
|
||||
it('extracts and persists a render snapshot once for legacy template switching', async () => {
|
||||
const legacyHtml = join(root, 'legacy-with-html.html')
|
||||
writeFileSync(legacyHtml, '<h1>Legacy source</h1>')
|
||||
const { saveGeneratedReport, prepareGeneratedReportTemplateSwitch } =
|
||||
await import('../../src/main/report-history-service')
|
||||
const saved = await saveGeneratedReport({
|
||||
contactId: 'legacy-snapshot-group',
|
||||
contactName: '旧报告快照',
|
||||
dateRange: '今天',
|
||||
messageCount: 8,
|
||||
generatedAt: '2026-08-12T12:00:00.000Z',
|
||||
generatedImage: `data:image/png;base64,${Buffer.from('legacy-snapshot').toString('base64')}`,
|
||||
htmlPath: legacyHtml
|
||||
})
|
||||
const extractSnapshot = vi.fn(async () => ({
|
||||
groupName: '旧报告快照',
|
||||
reportDate: '2026-08-12',
|
||||
values: { REPORT_TITLE: '旧报告快照日报', TOPIC_CARDS: '<div>已有主题</div>' }
|
||||
}))
|
||||
|
||||
const first = await prepareGeneratedReportTemplateSwitch(saved.record!.id, extractSnapshot)
|
||||
const second = await prepareGeneratedReportTemplateSwitch(saved.record!.id, extractSnapshot)
|
||||
|
||||
expect(first).toEqual(second)
|
||||
expect(extractSnapshot).toHaveBeenCalledTimes(1)
|
||||
expect(JSON.parse(readFileSync(saved.record!.jsonPath!, 'utf8'))).toMatchObject({
|
||||
id: saved.record!.id,
|
||||
reportRenderSnapshot: first.snapshot
|
||||
})
|
||||
})
|
||||
|
||||
it('persists the classic v1 template as a selectable history template', async () => {
|
||||
const classicHtml = join(root, 'classic.html')
|
||||
writeFileSync(classicHtml, '<h1>经典日报</h1>')
|
||||
const { saveGeneratedReport } = await import('../../src/main/report-history-service')
|
||||
const saved = await saveGeneratedReport({
|
||||
contactId: 'classic-group',
|
||||
contactName: '经典日报群',
|
||||
dateRange: '今天',
|
||||
messageCount: 6,
|
||||
generatedAt: '2026-08-12T13:00:00.000Z',
|
||||
generatedImage: `data:image/png;base64,${Buffer.from('classic').toString('base64')}`,
|
||||
htmlPath: classicHtml,
|
||||
reportSnapshot,
|
||||
reportMetadata,
|
||||
templateId: 'v1'
|
||||
})
|
||||
|
||||
expect(saved.success).toBe(true)
|
||||
expect(saved.record?.templateId).toBe('v1')
|
||||
expect(JSON.parse(readFileSync(saved.record!.jsonPath!, 'utf8')).templateId).toBe('v1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { GeneratedReportRecord } from '../../src/shared/report-history'
|
||||
import { switchGeneratedReportTemplate } from '../../src/renderer/src/utils/report-template-switch'
|
||||
|
||||
const structuredReport = {
|
||||
id: 'report-1',
|
||||
contactId: 'group-1',
|
||||
contactName: '测试群',
|
||||
dateRange: '今天',
|
||||
messageCount: 10,
|
||||
generatedAt: '2026-08-12T10:00:00.000Z',
|
||||
reportDate: '2026-08-12',
|
||||
htmlStatus: 'ready',
|
||||
pngStatus: 'ready',
|
||||
templateId: 'mobile-feed',
|
||||
reportSnapshot: { overview: '已有内容' },
|
||||
reportMetadata: { groupName: '测试群' }
|
||||
} as GeneratedReportRecord
|
||||
|
||||
describe('report template switching pipeline', () => {
|
||||
it('only exports the saved snapshot and updates the same history record', async () => {
|
||||
const api = {
|
||||
exportGroupReport: vi.fn(async () => ({
|
||||
success: true,
|
||||
imageDataUrl: 'data:image/png;base64,new',
|
||||
htmlPath: '/tmp/new.html',
|
||||
pngPath: '/tmp/new.png'
|
||||
})),
|
||||
exportGroupReportSnapshot: vi.fn(),
|
||||
prepareGeneratedReportTemplateSwitch: vi.fn(),
|
||||
updateGeneratedReportTemplate: vi.fn(async () => ({
|
||||
success: true,
|
||||
record: { ...structuredReport, templateId: 'mobile-dashboard' as const }
|
||||
}))
|
||||
}
|
||||
|
||||
const result = await switchGeneratedReportTemplate(structuredReport, 'mobile-dashboard', api)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(api.exportGroupReport).toHaveBeenCalledTimes(1)
|
||||
expect(api.exportGroupReport).toHaveBeenCalledWith({
|
||||
report: structuredReport.reportSnapshot,
|
||||
metadata: structuredReport.reportMetadata,
|
||||
templateId: 'mobile-dashboard'
|
||||
})
|
||||
expect(api.updateGeneratedReportTemplate).toHaveBeenCalledWith({
|
||||
reportId: structuredReport.id,
|
||||
templateId: 'mobile-dashboard',
|
||||
generatedImage: 'data:image/png;base64,new',
|
||||
htmlPath: '/tmp/new.html',
|
||||
pngPath: '/tmp/new.png'
|
||||
})
|
||||
})
|
||||
|
||||
it('switches an existing report back to the classic default without rerunning AI', async () => {
|
||||
const api = {
|
||||
exportGroupReport: vi.fn(async () => ({
|
||||
success: true,
|
||||
imageDataUrl: 'data:image/png;base64,classic',
|
||||
htmlPath: '/tmp/classic.html',
|
||||
pngPath: '/tmp/classic.png'
|
||||
})),
|
||||
exportGroupReportSnapshot: vi.fn(),
|
||||
prepareGeneratedReportTemplateSwitch: vi.fn(),
|
||||
updateGeneratedReportTemplate: vi.fn(async () => ({
|
||||
success: true,
|
||||
record: { ...structuredReport, templateId: 'v1' as const }
|
||||
}))
|
||||
}
|
||||
|
||||
const result = await switchGeneratedReportTemplate(structuredReport, 'v1', api)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(api.exportGroupReport).toHaveBeenCalledWith({
|
||||
report: structuredReport.reportSnapshot,
|
||||
metadata: structuredReport.reportMetadata,
|
||||
templateId: 'v1'
|
||||
})
|
||||
expect(api.prepareGeneratedReportTemplateSwitch).not.toHaveBeenCalled()
|
||||
expect(api.updateGeneratedReportTemplate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ reportId: structuredReport.id, templateId: 'v1' })
|
||||
)
|
||||
})
|
||||
|
||||
it('migrates a legacy record from its saved HTML before switching', async () => {
|
||||
const api = {
|
||||
exportGroupReport: vi.fn(),
|
||||
exportGroupReportSnapshot: vi.fn(async () => ({
|
||||
success: true,
|
||||
imageDataUrl: 'data:image/png;base64,legacy-new',
|
||||
htmlPath: '/tmp/legacy-new.html',
|
||||
pngPath: '/tmp/legacy-new.png'
|
||||
})),
|
||||
prepareGeneratedReportTemplateSwitch: vi.fn(async () => ({
|
||||
success: true,
|
||||
snapshot: {
|
||||
groupName: '测试群',
|
||||
reportDate: '2026-08-12',
|
||||
values: { REPORT_TITLE: '测试群日报' }
|
||||
}
|
||||
})),
|
||||
updateGeneratedReportTemplate: vi.fn(async () => ({ success: true }))
|
||||
}
|
||||
const result = await switchGeneratedReportTemplate(
|
||||
{ ...structuredReport, reportSnapshot: undefined, reportMetadata: undefined },
|
||||
'desktop-editorial',
|
||||
api
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(api.exportGroupReport).not.toHaveBeenCalled()
|
||||
expect(api.prepareGeneratedReportTemplateSwitch).toHaveBeenCalledWith('report-1')
|
||||
expect(api.exportGroupReportSnapshot).toHaveBeenCalledWith({
|
||||
snapshot: expect.objectContaining({ groupName: '测试群' }),
|
||||
templateId: 'desktop-editorial'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not call any pipeline step when a legacy record cannot be migrated', async () => {
|
||||
const api = {
|
||||
exportGroupReport: vi.fn(),
|
||||
exportGroupReportSnapshot: vi.fn(),
|
||||
prepareGeneratedReportTemplateSwitch: vi.fn(async () => ({
|
||||
success: false,
|
||||
error: '当前日报缺少 HTML 文件,无法迁移旧模板数据'
|
||||
})),
|
||||
updateGeneratedReportTemplate: vi.fn()
|
||||
}
|
||||
const result = await switchGeneratedReportTemplate(
|
||||
{ ...structuredReport, reportSnapshot: undefined, reportMetadata: undefined },
|
||||
'desktop-editorial',
|
||||
api
|
||||
)
|
||||
|
||||
expect(result).toEqual({ success: false, error: '当前日报缺少 HTML 文件,无法迁移旧模板数据' })
|
||||
expect(api.exportGroupReport).not.toHaveBeenCalled()
|
||||
expect(api.updateGeneratedReportTemplate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_REPORT_TEMPLATE,
|
||||
getReportTemplate,
|
||||
isReportTemplateId,
|
||||
isSelectableReportTemplateId,
|
||||
REPORT_TEMPLATES,
|
||||
SELECTABLE_REPORT_TEMPLATES
|
||||
} from '../../src/shared/report-templates'
|
||||
|
||||
describe('daily report templates', () => {
|
||||
it('exposes the classic default plus exactly three mobile and two desktop product templates', () => {
|
||||
expect(REPORT_TEMPLATES).toHaveLength(5)
|
||||
expect(SELECTABLE_REPORT_TEMPLATES).toHaveLength(6)
|
||||
expect(SELECTABLE_REPORT_TEMPLATES[0]).toEqual(DEFAULT_REPORT_TEMPLATE)
|
||||
expect(DEFAULT_REPORT_TEMPLATE).toMatchObject({
|
||||
id: 'v1',
|
||||
label: '默认模板',
|
||||
name: '经典日报',
|
||||
resourceFile: 'mobile_daily_report_v1.html'
|
||||
})
|
||||
expect(REPORT_TEMPLATES.filter((template) => template.platform === 'mobile')).toHaveLength(3)
|
||||
expect(REPORT_TEMPLATES.filter((template) => template.platform === 'desktop')).toHaveLength(2)
|
||||
expect(new Set(REPORT_TEMPLATES.map((template) => template.cssClass)).size).toBe(5)
|
||||
})
|
||||
|
||||
it('uses mobile and desktop capture widths appropriate to their layouts', () => {
|
||||
expect(DEFAULT_REPORT_TEMPLATE.captureWidth).toBe(430)
|
||||
expect(DEFAULT_REPORT_TEMPLATE.maxCaptureWidth).toBeGreaterThanOrEqual(
|
||||
DEFAULT_REPORT_TEMPLATE.captureWidth
|
||||
)
|
||||
for (const template of REPORT_TEMPLATES) {
|
||||
expect(
|
||||
template.platform === 'mobile' ? template.captureWidth : template.captureWidth >= 1280
|
||||
).toBeTruthy()
|
||||
expect(template.maxCaptureWidth).toBeGreaterThanOrEqual(template.captureWidth)
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves v1 and unknown ids to the classic default template', () => {
|
||||
expect(isReportTemplateId('desktop-editorial')).toBe(true)
|
||||
expect(isReportTemplateId('v1')).toBe(false)
|
||||
expect(isSelectableReportTemplateId('v1')).toBe(true)
|
||||
expect(isSelectableReportTemplateId('v2')).toBe(false)
|
||||
expect(getReportTemplate('unknown').id).toBe('v1')
|
||||
expect(getReportTemplate('v1')).toEqual(DEFAULT_REPORT_TEMPLATE)
|
||||
})
|
||||
|
||||
it('ships the classic resource and shared semantic resource with five distinct layout classes', () => {
|
||||
expect(existsSync(resolve('resources', DEFAULT_REPORT_TEMPLATE.resourceFile))).toBe(true)
|
||||
const resourcePath = resolve('resources', 'daily_report_templates.html')
|
||||
expect(existsSync(resourcePath)).toBe(true)
|
||||
const html = readFileSync(resourcePath, 'utf8')
|
||||
for (const template of REPORT_TEMPLATES) {
|
||||
expect(html).toContain(`.${template.cssClass}`)
|
||||
}
|
||||
expect(html).toContain('{{TOPIC_CARDS}}')
|
||||
expect(html).toContain('{{IMPORTANT_MESSAGES}}')
|
||||
expect(html).toContain('{{QA_CARDS}}')
|
||||
expect(html).toContain('{{RANK_ITEMS}}')
|
||||
expect(html).toContain('{{HERO_AVATARS}}')
|
||||
expect(html).not.toContain('群聊相册')
|
||||
expect(html).not.toContain('今日群相册')
|
||||
expect(html).toContain('grid-template-columns: minmax(78px, 104px) minmax(72px, 1fr) 34px')
|
||||
expect(html).toContain('.template-mobile-dashboard .heat-name {')
|
||||
expect(html).toContain('white-space: normal')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user