Skip to content

Commit 04ecf93

Browse files
committed
test: 新增导出、MCP 工具、动态时间线与报表服务的单元测试
实现导出功能测试,覆盖 CSV、JSON、Markdown 格式 新增 MCP 工具注册与执行测试,确保元数据与异常处理正常 创建动态时间线获取测试,验证数据标准化与异常场景 新增报表生成测试,覆盖多维度范围、消息聚合与关键词提取
1 parent 82a2948 commit 04ecf93

21 files changed

Lines changed: 4564 additions & 141 deletions

CipherTalk-CLI/package-lock.json

Lines changed: 1100 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CipherTalk-CLI/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"sync:upstream": "tsx scripts/sync-from-upstream.ts"
3737
},
3838
"dependencies": {
39+
"@modelcontextprotocol/sdk": "^1.27.1",
3940
"chalk": "^5.6.2",
4041
"cli-table3": "^0.6.5",
4142
"commander": "^14.0.2",

CipherTalk-CLI/src/commands/moments.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,27 @@ export function registerMomentsCommand(program: Command, context: CommandContext
55
const moments = program
66
.command('moments')
77
.description('朋友圈数据')
8-
.option('--from <date>', '开始日期')
9-
.option('--to <date>', '结束日期')
8+
.option('--limit <n>', '返回条数(默认 20,上限 200)', '20')
9+
.option('--user <wxid>', '只查指定 wxid 的朋友圈')
10+
.option('--from <date>', '开始时间(ISO 字符串或 Unix 秒)')
11+
.option('--to <date>', '结束时间(ISO 字符串或 Unix 秒)')
1012
.action(async () => {
11-
await runCommand(moments, context, async () => context.services.advanced.moments())
13+
await runCommand(moments, context, async (config, options) => {
14+
const limit = typeof options.limit === 'string' ? Number(options.limit) : undefined
15+
const result = await context.services.advanced.moments(config, {
16+
limit: Number.isFinite(limit) ? limit : undefined,
17+
user: typeof options.user === 'string' ? options.user : undefined,
18+
from: typeof options.from === 'string' ? options.from : undefined,
19+
to: typeof options.to === 'string' ? options.to : undefined
20+
})
21+
return {
22+
data: { entries: result.entries },
23+
meta: {
24+
total: result.total,
25+
limit: result.limit,
26+
...(result.meta || {})
27+
}
28+
}
29+
})
1230
})
1331
}

CipherTalk-CLI/src/commands/report.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,32 @@ export function registerReportCommand(program: Command, context: CommandContext)
55
const report = program
66
.command('report')
77
.description('年度报告数据')
8-
.option('--year <year>', '年份')
8+
.option('--year <year>', '年份(默认当前年)')
99
.option('--all-time', '全时间范围')
10+
.option('--session <id>', '只统计指定会话')
11+
.option('--top-contacts <n>', '联系人榜数量')
12+
.option('--top-keywords <n>', '关键词榜数量')
1013
.action(async () => {
11-
await runCommand(report, context, async () => context.services.advanced.report())
14+
await runCommand(report, context, async (config, options) => {
15+
const year = typeof options.year === 'string' ? Number(options.year) : undefined
16+
const topContacts = typeof options.topContacts === 'string' ? Number(options.topContacts) : undefined
17+
const topKeywords = typeof options.topKeywords === 'string' ? Number(options.topKeywords) : undefined
18+
const result = await context.services.advanced.report(config, {
19+
year: Number.isFinite(year) ? year : undefined,
20+
allTime: options.allTime === true,
21+
session: typeof options.session === 'string' ? options.session : undefined,
22+
topContacts: Number.isFinite(topContacts) ? topContacts : undefined,
23+
topKeywords: Number.isFinite(topKeywords) ? topKeywords : undefined
24+
})
25+
return {
26+
data: result,
27+
meta: {
28+
scope: result.scope,
29+
year: result.year,
30+
sessionId: result.sessionId,
31+
...(result.meta || {})
32+
}
33+
}
34+
})
1235
})
1336
}

CipherTalk-CLI/src/interactiveShell.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,41 @@ async function runShellCommand(line: string, context: CommandContext, globals: G
414414
writeEnvelope(context.output, successEnvelope(result), format)
415415
return true
416416
}
417-
case '/moments':
418-
context.output.stderr('朋友圈功能暂不支持。请使用桌面版密语查看朋友圈。')
417+
case '/moments': {
418+
const limit = commandLimit(options, 20)
419+
const result = await context.services.advanced.moments(config, {
420+
limit,
421+
user: asString(options.user),
422+
from: asString(options.from),
423+
to: asString(options.to)
424+
})
425+
writeEnvelope(context.output, successEnvelope({ entries: result.entries }, {
426+
total: result.total,
427+
limit: result.limit,
428+
...(result.meta || {})
429+
}), format)
419430
return true
420-
case '/report':
421-
context.output.stderr('年度报告功能暂不支持。请使用桌面版密语生成年度报告。')
431+
}
432+
case '/report': {
433+
const yearRaw = asString(options.year)
434+
const yearNum = yearRaw !== undefined ? Number(yearRaw) : undefined
435+
const topContactsRaw = asString(options['top-contacts'])
436+
const topKeywordsRaw = asString(options['top-keywords'])
437+
const result = await context.services.advanced.report(config, {
438+
year: Number.isFinite(yearNum) ? yearNum : undefined,
439+
allTime: options['all-time'] === true,
440+
session: asString(options.session),
441+
topContacts: topContactsRaw !== undefined ? Number(topContactsRaw) : undefined,
442+
topKeywords: topKeywordsRaw !== undefined ? Number(topKeywordsRaw) : undefined
443+
})
444+
writeEnvelope(context.output, successEnvelope(result, {
445+
scope: result.scope,
446+
year: result.year,
447+
sessionId: result.sessionId,
448+
...(result.meta || {})
449+
}), format)
422450
return true
451+
}
423452
case '/mcp':
424453
if (positional[0] === 'serve') {
425454
await context.services.advanced.mcpServe()

CipherTalk-CLI/src/services/advancedService.ts

Lines changed: 22 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
import { notImplemented, dbError, MiyuError } from '../errors.js'
2-
import { wcdbService } from './db/wcdbService.js'
3-
import { dbAdapter } from './db/dbAdapter.js'
41
import { searchMessages } from './searchService.js'
5-
import type { AdvancedService, SearchResult, StatsOptions, ExportOptions } from './types.js'
2+
import { exportChat as runExportChat } from './export/exportService.js'
3+
import { analyticsService } from './analytics/analyticsService.js'
4+
import { runMcpServe } from './mcp/runtime.js'
5+
import { getMomentsTimeline } from './sns/snsService.js'
6+
import { generateReport } from './report/reportService.js'
7+
import type {
8+
AdvancedService,
9+
SearchResult,
10+
StatsOptions,
11+
ExportOptions,
12+
MomentsOptions,
13+
MomentsResult,
14+
ReportOptions,
15+
ReportResult
16+
} from './types.js'
617
import type { RuntimeConfig } from '../types.js'
718

8-
async function connect(config: RuntimeConfig): Promise<void> {
9-
const ok = await wcdbService.open(config.dbPath!, config.keyHex!, config.wxid || '')
10-
if (!ok) throw dbError('数据库连接失败,请检查 dbPath / keyHex / wxid')
11-
}
12-
1319
export class RealAdvancedService implements AdvancedService {
14-
15-
// ── 搜索 ──
1620
async search(
1721
config: RuntimeConfig,
1822
keyword: string,
@@ -21,73 +25,24 @@ export class RealAdvancedService implements AdvancedService {
2125
return searchMessages(config, keyword, options || {})
2226
}
2327

24-
// ── 统计 ──
2528
async stats(config: RuntimeConfig, opts: StatsOptions): Promise<any> {
26-
await connect(config)
27-
28-
switch (opts.type) {
29-
case 'global': {
30-
// 会话总数
31-
let totalSessions = 0
32-
try {
33-
const r = await dbAdapter.get<{ cnt: number }>('session', '', 'SELECT COUNT(*) as cnt FROM SessionTable')
34-
totalSessions = r?.cnt || 0
35-
} catch { /**/ }
36-
return { totalSessions }
37-
}
38-
39-
case 'contacts': {
40-
const top = opts.top || 20
41-
// 从 session 表列出私聊
42-
const sessions = await dbAdapter.all<{ username: string }>(
43-
'session', '',
44-
`SELECT username FROM SessionTable WHERE username NOT LIKE '%@chatroom%' AND username NOT LIKE 'gh_%' ORDER BY sort_timestamp DESC LIMIT ?`,
45-
[top]
46-
)
47-
return { contacts: sessions.map(s => ({ wxid: s.username, displayName: s.username, messageCount: 0 })) }
48-
}
49-
50-
case 'session': {
51-
if (!opts.session) throw new Error('stats session 需要 --session 参数')
52-
// 尝试查询该会话的 msg 表
53-
return { totalMessages: 0, textMessages: 0, mediaMessages: 0, sentMessages: 0, receivedMessages: 0, activeDays: 0, firstMessageTime: null, lastMessageTime: null }
54-
}
55-
56-
case 'time': {
57-
return { distribution: {} }
58-
}
59-
60-
case 'keywords': {
61-
return { keywords: [] }
62-
}
63-
64-
case 'group': {
65-
return { totalMessages: 0, activeMembers: 0 }
66-
}
67-
68-
default:
69-
throw new Error(`未知统计类型: ${(opts as any).type}`)
70-
}
29+
return analyticsService.run(config, opts)
7130
}
7231

73-
// ── 导出 ──
7432
async exportChat(config: RuntimeConfig, opts: ExportOptions): Promise<{ path: string; count: number }> {
75-
// 简单实现:查询消息后写入文件
76-
await connect(config)
77-
throw notImplemented('export')
33+
return runExportChat(config, opts)
7834
}
7935

80-
// ── 未移植的功能 ──
81-
async moments(): Promise<never> {
82-
throw new MiyuError('NOT_IMPLEMENTED', '朋友圈数据:暂不支持。请使用桌面版密语查看朋友圈。')
36+
async moments(config: RuntimeConfig, options: MomentsOptions = {}): Promise<MomentsResult> {
37+
return getMomentsTimeline(config, options)
8338
}
8439

85-
async report(): Promise<never> {
86-
throw new MiyuError('NOT_IMPLEMENTED', '年度报告:暂不支持。请使用桌面版密语生成年度报告。')
40+
async report(config: RuntimeConfig, options: ReportOptions = {}): Promise<ReportResult> {
41+
return generateReport(config, options)
8742
}
8843

8944
async mcpServe(): Promise<never> {
90-
throw new MiyuError('NOT_IMPLEMENTED', 'MCP Server:暂不支持。请使用桌面版密语的 MCP 功能。')
45+
return runMcpServe()
9146
}
9247
}
9348

0 commit comments

Comments
 (0)