weflow-cli 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/weflow-cli-electron.cjs +59 -0
- package/bin/weflow-cli.ts +2311 -0
- package/cli.cjs +3 -0
- package/dist/bin/weflow-cli.d.ts +3 -0
- package/dist/bin/weflow-cli.d.ts.map +1 -0
- package/dist/bin/weflow-cli.js +2270 -0
- package/dist/bin/weflow-cli.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +690 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/core/dbPathService.d.ts +40 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -0
- package/dist/src/core/dbPathService.js +324 -0
- package/dist/src/core/dbPathService.js.map +1 -0
- package/dist/src/core/keyService.d.ts +59 -0
- package/dist/src/core/keyService.d.ts.map +1 -0
- package/dist/src/core/keyService.js +368 -0
- package/dist/src/core/keyService.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +62 -0
- package/dist/src/core/ntCore.d.ts.map +1 -0
- package/dist/src/core/ntCore.js +191 -0
- package/dist/src/core/ntCore.js.map +1 -0
- package/dist/src/core/sqlcipherCore.d.ts +119 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
- package/dist/src/core/sqlcipherCore.js +693 -0
- package/dist/src/core/sqlcipherCore.js.map +1 -0
- package/dist/src/core/wcdbCore.d.ts +738 -0
- package/dist/src/core/wcdbCore.d.ts.map +1 -0
- package/dist/src/core/wcdbCore.js +4545 -0
- package/dist/src/core/wcdbCore.js.map +1 -0
- package/dist/src/core/wechatClient.d.ts +23 -0
- package/dist/src/core/wechatClient.d.ts.map +1 -0
- package/dist/src/core/wechatClient.js +166 -0
- package/dist/src/core/wechatClient.js.map +1 -0
- package/dist/src/services/chatService.d.ts +25 -0
- package/dist/src/services/chatService.d.ts.map +1 -0
- package/dist/src/services/chatService.js +291 -0
- package/dist/src/services/chatService.js.map +1 -0
- package/dist/src/services/configService.d.ts +43 -0
- package/dist/src/services/configService.d.ts.map +1 -0
- package/dist/src/services/configService.js +156 -0
- package/dist/src/services/configService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +30 -0
- package/dist/src/services/exportService.d.ts.map +1 -0
- package/dist/src/services/exportService.js +273 -0
- package/dist/src/services/exportService.js.map +1 -0
- package/dist/src/services/wechat-formatter.d.ts +69 -0
- package/dist/src/services/wechat-formatter.d.ts.map +1 -0
- package/dist/src/services/wechat-formatter.js +248 -0
- package/dist/src/services/wechat-formatter.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +28 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -0
- package/dist/src/services/wechatMessageService.js +341 -0
- package/dist/src/services/wechatMessageService.js.map +1 -0
- package/dist/src/services/wereadService.d.ts +206 -0
- package/dist/src/services/wereadService.d.ts.map +1 -0
- package/dist/src/services/wereadService.js +145 -0
- package/dist/src/services/wereadService.js.map +1 -0
- package/dist/src/services/whitelistService.d.ts +20 -0
- package/dist/src/services/whitelistService.d.ts.map +1 -0
- package/dist/src/services/whitelistService.js +60 -0
- package/dist/src/services/whitelistService.js.map +1 -0
- package/dist/src/types.d.ts +140 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/errors.d.ts +25 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +43 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/pathUtils.d.ts +5 -0
- package/dist/src/utils/pathUtils.d.ts.map +1 -0
- package/dist/src/utils/pathUtils.js +16 -0
- package/dist/src/utils/pathUtils.js.map +1 -0
- package/dist/src/utils/pythonRunner.d.ts +27 -0
- package/dist/src/utils/pythonRunner.d.ts.map +1 -0
- package/dist/src/utils/pythonRunner.js +62 -0
- package/dist/src/utils/pythonRunner.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +14 -0
- package/dist/src/utils/talkerUtils.d.ts.map +1 -0
- package/dist/src/utils/talkerUtils.js +73 -0
- package/dist/src/utils/talkerUtils.js.map +1 -0
- package/mcp-server/index.ts +757 -0
- package/package.json +72 -0
- package/resources/key/win32/x64/wx_key.dll +0 -0
- package/resources/wcdb/win32/x64/SDL2.dll +0 -0
- package/resources/wcdb/win32/x64/WCDB.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
- package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
- package/scripts/_batch_link_sources.py +73 -0
- package/scripts/_utils.py +531 -0
- package/scripts/annual_report.py +796 -0
- package/scripts/auto_tag.py +128 -0
- package/scripts/biz_daily.py +896 -0
- package/scripts/chat_report.py +397 -0
- package/scripts/chat_stats.py +702 -0
- package/scripts/classify_daily.py +411 -0
- package/scripts/compile_wiki.py +258 -0
- package/scripts/create_reading_notes.py +370 -0
- package/scripts/enrich_backlinks.py +163 -0
- package/scripts/export_chat_html.py +766 -0
- package/scripts/extract_3x_key.py +86 -0
- package/scripts/extract_todos.py +404 -0
- package/scripts/fav_server.py +392 -0
- package/scripts/fix_topics.py +111 -0
- package/scripts/generate_ai_report.py +367 -0
- package/scripts/generate_html.py +1372 -0
- package/scripts/generate_review.py +170 -0
- package/scripts/mcp_bridge.py +398 -0
- package/scripts/nt_decrypt.py +562 -0
- package/scripts/pipeline.py +145 -0
- package/scripts/promote_all.py +310 -0
- package/scripts/promote_ideas.py +297 -0
- package/scripts/rag_chat.py +191 -0
- package/scripts/scan_decrypt_4x.py +329 -0
- package/scripts/semantic_search.py +431 -0
- package/scripts/sync_fav.py +146 -0
- package/scripts/sync_weread.py +192 -0
- package/scripts/vault_rag.py +139 -0
- package/scripts/vault_search.py +141 -0
- package/src/core/dbPathService.ts +348 -0
- package/src/core/keyService.ts +409 -0
- package/src/core/ntCore.ts +224 -0
- package/src/core/sqlcipherCore.ts +764 -0
- package/src/core/wcdbCore.ts +4568 -0
- package/src/core/wechatClient.ts +204 -0
- package/src/lz4.d.ts +10 -0
- package/src/services/chatService.ts +296 -0
- package/src/services/configService.ts +197 -0
- package/src/services/exportService.ts +305 -0
- package/src/services/wechat-formatter.ts +348 -0
- package/src/services/wechatMessageService.ts +383 -0
- package/src/services/wereadService.ts +308 -0
- package/src/services/whitelistService.ts +72 -0
- package/src/types.ts +141 -0
- package/src/utils/errors.ts +42 -0
- package/src/utils/pathUtils.ts +16 -0
- package/src/utils/pythonRunner.ts +81 -0
- package/src/utils/talkerUtils.ts +84 -0
|
@@ -0,0 +1,2311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import inquirer from 'inquirer'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { existsSync, readFileSync } from 'fs'
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import { dbPathService } from '../src/core/dbPathService.js'
|
|
9
|
+
import { keyService } from '../src/core/keyService.js'
|
|
10
|
+
import { NtCore } from '../src/core/ntCore.js'
|
|
11
|
+
import { configService } from '../src/services/configService.js'
|
|
12
|
+
import { chatService } from '../src/services/chatService.js'
|
|
13
|
+
import { exportService } from '../src/services/exportService.js'
|
|
14
|
+
import { resolveTalker as resolveTalkerCore } from '../src/utils/talkerUtils.js'
|
|
15
|
+
import { WechatMessageService } from '../src/services/wechatMessageService.js'
|
|
16
|
+
import { whitelistService } from '../src/services/whitelistService.js'
|
|
17
|
+
import type { ChatSession } from '../src/types.js'
|
|
18
|
+
|
|
19
|
+
const program = new Command()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 尝试从 WeFlow 桌面版配置中读取已保存的密钥
|
|
23
|
+
*/
|
|
24
|
+
function tryReadWeFlowKey(): string | null {
|
|
25
|
+
const candidates = [
|
|
26
|
+
join(homedir(), 'AppData', 'Roaming', 'WeFlow', 'WeFlow-config.json'),
|
|
27
|
+
join(homedir(), 'AppData', 'Roaming', 'weflow', 'WeFlow-config.json'),
|
|
28
|
+
join(homedir(), '.weflow', 'config.json'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
for (const configPath of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(configPath)) continue
|
|
34
|
+
const raw = readFileSync(configPath, 'utf8')
|
|
35
|
+
const config = JSON.parse(raw)
|
|
36
|
+
|
|
37
|
+
// WeFlow 使用 electron-store,decryptKey 可能是明文或加密的
|
|
38
|
+
let key = config.decryptKey || ''
|
|
39
|
+
// 如果是 lock: 前缀,说明是加密的,无法直接使用
|
|
40
|
+
if (typeof key === 'string' && key.startsWith('lock:')) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
// 如果是 safe: 前缀,说明是 safeStorage 加密的,也无法直接使用
|
|
44
|
+
if (typeof key === 'string' && key.startsWith('safe:')) {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
// 明文 key
|
|
48
|
+
if (typeof key === 'string' && key.length === 64) {
|
|
49
|
+
return key
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 检查 wxidConfigs 中的 key
|
|
53
|
+
const wxidConfigs = config.wxidConfigs || {}
|
|
54
|
+
for (const [_wxid, cfg] of Object.entries(wxidConfigs) as [string, any][]) {
|
|
55
|
+
const cfgKey = cfg.decryptKey || ''
|
|
56
|
+
if (typeof cfgKey === 'string' && cfgKey.length === 64 && !cfgKey.startsWith('lock:') && !cfgKey.startsWith('safe:')) {
|
|
57
|
+
return cfgKey
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* CLI 包装的 talker 解析 (wxid / 昵称 / 备注名 / 序号)。
|
|
69
|
+
* 委托给 src/utils/talkerUtils 的核心实现,外加 chalk 友好输出。
|
|
70
|
+
*/
|
|
71
|
+
async function resolveTalker(input: string): Promise<string> {
|
|
72
|
+
try {
|
|
73
|
+
const result = await resolveTalkerCore(input)
|
|
74
|
+
// 显示解析结果(已知格式跳过打印)
|
|
75
|
+
if (!input.startsWith('wxid_') && !input.includes('@chatroom') && !input.includes('@openim')) {
|
|
76
|
+
const isNum = /^\[?\d+\]?$/.test(input)
|
|
77
|
+
const sessions = await chatService.listSessions(undefined, 50)
|
|
78
|
+
const match = sessions.find(s => s.username === result)
|
|
79
|
+
if (match) {
|
|
80
|
+
const prefix = isNum ? `序号[${input.replace(/[\[\]]/g, '')}] ` : ''
|
|
81
|
+
console.log(chalk.gray(` → ${prefix}${match.displayName} (${match.username})`))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
} catch (e: any) {
|
|
86
|
+
console.log(chalk.red(`\n❌ ${e.message}\n`))
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.name('weflow-cli')
|
|
93
|
+
.description('WeFlow CLI - 微信聊天记录命令行查询与导出工具')
|
|
94
|
+
.version('1.5.0')
|
|
95
|
+
|
|
96
|
+
// ==================== init ====================
|
|
97
|
+
program
|
|
98
|
+
.command('init')
|
|
99
|
+
.description('自动检测微信数据目录并提取解密密钥')
|
|
100
|
+
.action(async () => {
|
|
101
|
+
console.log(chalk.cyan('🔧 WeFlow CLI 初始化\n'))
|
|
102
|
+
|
|
103
|
+
// Step 0: 检测微信版本
|
|
104
|
+
console.log(chalk.yellow('步骤 0/3: 检测微信版本...'))
|
|
105
|
+
const version = await keyService.detectWeChatVersion()
|
|
106
|
+
|
|
107
|
+
if (version) {
|
|
108
|
+
console.log(chalk.green(`✓ 检测到微信 ${version} 版本`))
|
|
109
|
+
} else {
|
|
110
|
+
// 没有运行中的微信,尝试已安装的版本
|
|
111
|
+
console.log(chalk.gray(' 未检测到运行中的微信进程'))
|
|
112
|
+
// 检查已有配置或数据目录
|
|
113
|
+
const existingPath = configService.get('dbPath')
|
|
114
|
+
if (existingPath && existsSync(existingPath)) {
|
|
115
|
+
console.log(chalk.green(` 使用已配置的数据目录: ${existingPath}`))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (version === '3.x') {
|
|
120
|
+
// ====== 3.x 初始化 ======
|
|
121
|
+
console.log(chalk.yellow('\n步骤 1/3: 提取 3.x 数据库密钥...'))
|
|
122
|
+
const result = await keyService.extract3xKey((msg) => {
|
|
123
|
+
console.log(chalk.gray(` ${msg}`))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
if (!result.success || !result.key) {
|
|
127
|
+
console.log(chalk.red(`\n✗ 密钥提取失败: ${result.error}`))
|
|
128
|
+
console.log(chalk.gray('\n解决方案:'))
|
|
129
|
+
console.log(chalk.gray(' 1. 确保 WeChat 3.x (WeChat.exe) 已登录'))
|
|
130
|
+
console.log(chalk.gray(' 2. 确保 Python + pywxdump 已安装'))
|
|
131
|
+
console.log(chalk.gray(' 3. 或手动指定密钥: weflow-cli config set decryptKey3x <64位密钥>'))
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const msgDir = result.msgDir || ''
|
|
136
|
+
const wxDir = result.wxDir || ''
|
|
137
|
+
const wxid = result.wxid || ''
|
|
138
|
+
|
|
139
|
+
configService.set('decryptKey3x', result.key)
|
|
140
|
+
configService.set('dataVersion', '3.x')
|
|
141
|
+
|
|
142
|
+
// 查找 MSG0.db
|
|
143
|
+
const msg0Path = join(msgDir, 'Multi', 'MSG0.db')
|
|
144
|
+
if (existsSync(msg0Path)) {
|
|
145
|
+
configService.set('dbPath3x', msg0Path)
|
|
146
|
+
console.log(chalk.green(`✓ 数据库: ${msg0Path}`))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (wxid) configService.set('wxid', wxid)
|
|
150
|
+
if (wxDir) configService.set('dbPath', wxDir)
|
|
151
|
+
|
|
152
|
+
console.log(chalk.green('\n✓ 密钥获取成功!'))
|
|
153
|
+
console.log(chalk.cyan('\n=============================='))
|
|
154
|
+
console.log(chalk.cyan('初始化完成! (3.x 模式)'))
|
|
155
|
+
console.log(chalk.cyan('=============================='))
|
|
156
|
+
console.log(chalk.white(`数据目录: ${wxDir}`))
|
|
157
|
+
console.log(chalk.white(`消息数据库: ${msg0Path}`))
|
|
158
|
+
console.log(chalk.white(`账号: ${wxid}`))
|
|
159
|
+
console.log(chalk.white(`密钥: ${result.key.slice(0, 8)}...${result.key.slice(-8)}`))
|
|
160
|
+
|
|
161
|
+
} else {
|
|
162
|
+
// ====== 4.x 初始化 (原有逻辑) ======
|
|
163
|
+
console.log(chalk.yellow('\n步骤 1/3: 检测微信数据目录...'))
|
|
164
|
+
const detected = await dbPathService.autoDetect()
|
|
165
|
+
if (!detected.success || !detected.path) {
|
|
166
|
+
console.log(chalk.red('✗ 未检测到微信数据目录'))
|
|
167
|
+
console.log(chalk.gray(' 请确保微信 4.x 已安装并登录过'))
|
|
168
|
+
console.log(chalk.gray(' 或手动指定: weflow-cli config set dbPath <path>'))
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
console.log(chalk.green(`✓ 检测到数据目录: ${detected.path}`))
|
|
172
|
+
configService.set('dbPath', detected.path)
|
|
173
|
+
configService.set('dataVersion', '4.x')
|
|
174
|
+
|
|
175
|
+
// Step 2: 扫描账号
|
|
176
|
+
console.log(chalk.yellow('\n步骤 2/3: 扫描微信账号...'))
|
|
177
|
+
const wxids = dbPathService.scanWxidCandidates(detected.path)
|
|
178
|
+
if (wxids.length === 0) {
|
|
179
|
+
console.log(chalk.red('✗ 未找到微信账号目录'))
|
|
180
|
+
process.exit(1)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const selectedWxid = wxids[0]
|
|
184
|
+
console.log(chalk.green(`✓ 找到 ${wxids.length} 个账号`))
|
|
185
|
+
for (const w of wxids) {
|
|
186
|
+
const marker = w === selectedWxid ? chalk.green(' → ') : ' '
|
|
187
|
+
const nickname = w.nickname ? ` (${w.nickname})` : ''
|
|
188
|
+
console.log(`${marker}${w.wxid}${nickname}`)
|
|
189
|
+
}
|
|
190
|
+
configService.set('wxid', selectedWxid.wxid)
|
|
191
|
+
|
|
192
|
+
// Step 3: 提取密钥
|
|
193
|
+
console.log(chalk.yellow('\n步骤 3/3: 提取数据库解密密钥...'))
|
|
194
|
+
|
|
195
|
+
// 优先尝试从 WeFlow 配置读取
|
|
196
|
+
console.log(chalk.gray(' 尝试从 WeFlow 桌面版配置读取...'))
|
|
197
|
+
let extractedKey = tryReadWeFlowKey()
|
|
198
|
+
|
|
199
|
+
if (extractedKey) {
|
|
200
|
+
console.log(chalk.green(' ✓ 从 WeFlow 配置读取到密钥'))
|
|
201
|
+
} else {
|
|
202
|
+
// 如果微信未运行,提示启动
|
|
203
|
+
if (!version) {
|
|
204
|
+
console.log(chalk.cyan('\n ══════════════════════════════════════'))
|
|
205
|
+
console.log(chalk.cyan(' 请现在启动微信 4.x 并登录!'))
|
|
206
|
+
console.log(chalk.cyan(' ══════════════════════════════════════\n'))
|
|
207
|
+
}
|
|
208
|
+
console.log(chalk.gray(' 等待微信进程出现(启动后会自动 hook)...'))
|
|
209
|
+
|
|
210
|
+
const keyResult = await keyService.waitForKey(120000, (msg) => {
|
|
211
|
+
console.log(chalk.gray(` ${msg}`))
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (keyResult.success && keyResult.key) {
|
|
215
|
+
extractedKey = keyResult.key
|
|
216
|
+
} else {
|
|
217
|
+
console.log(chalk.red(`\n✗ 密钥提取失败: ${keyResult.error}`))
|
|
218
|
+
console.log(chalk.gray('\n解决方案:'))
|
|
219
|
+
console.log(chalk.gray(' 1. 确保微信已登录'))
|
|
220
|
+
console.log(chalk.gray(' 2. 尝试在微信中发送一条消息'))
|
|
221
|
+
console.log(chalk.gray(' 3. 或手动指定密钥: weflow-cli config set decryptKey <64位密钥>'))
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
configService.set('decryptKey', extractedKey)
|
|
227
|
+
console.log(chalk.green('\n✓ 密钥获取成功!'))
|
|
228
|
+
|
|
229
|
+
// Step 4: 尝试扫描 NT 格式数据库 (xwechat_files)
|
|
230
|
+
// 即使检测到传统路径,也可能存在 NT 格式数据库
|
|
231
|
+
if (version) {
|
|
232
|
+
console.log(chalk.yellow('\n步骤 4/4: 扫描 NT 格式数据库...'))
|
|
233
|
+
console.log(chalk.gray(' 正在从微信内存中匹配数据库密钥...'))
|
|
234
|
+
|
|
235
|
+
const ntResult = await NtCore.scan()
|
|
236
|
+
if (ntResult.success && ntResult.matched && ntResult.matched.length > 0) {
|
|
237
|
+
console.log(chalk.green(` ✓ 找到 ${ntResult.matched.length} 个 NT 数据库`))
|
|
238
|
+
|
|
239
|
+
// 优先选择 message_0.db (主聊天数据库)
|
|
240
|
+
let primaryDb = ntResult.matched.find((db: any) => db.name === 'message_0.db')
|
|
241
|
+
if (!primaryDb) {
|
|
242
|
+
// 按大小降序排序,选择最大的数据库
|
|
243
|
+
primaryDb = ntResult.matched.sort((a: any, b: any) => b.size - a.size)[0]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (primaryDb) {
|
|
247
|
+
configService.set('ntDbPath', primaryDb.path)
|
|
248
|
+
configService.set('ntKey', primaryDb.key)
|
|
249
|
+
configService.set('ntSalt', primaryDb.salt)
|
|
250
|
+
console.log(chalk.green(` ✓ 主数据库: ${primaryDb.name} (${(primaryDb.size / 1024 / 1024).toFixed(1)}MB)`))
|
|
251
|
+
|
|
252
|
+
// 检查 contact.db 是否已匹配
|
|
253
|
+
const contactDb = ntResult.matched.find((db: any) => db.name === 'contact/contact.db')
|
|
254
|
+
if (contactDb) {
|
|
255
|
+
configService.set('contactDbPath', contactDb.path)
|
|
256
|
+
configService.set('contactKey', contactDb.key)
|
|
257
|
+
configService.set('contactSalt', contactDb.salt)
|
|
258
|
+
console.log(chalk.green(` ✓ 联系人数据库: ${contactDb.name} (${(contactDb.size / 1024 / 1024).toFixed(1)}MB)`))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 显示所有匹配的数据库
|
|
262
|
+
for (const db of ntResult.matched) {
|
|
263
|
+
const marker = db === primaryDb || db === contactDb ? chalk.green(' → ') : ' '
|
|
264
|
+
console.log(chalk.gray(`${marker}${db.name} (${(db.size / 1024 / 1024).toFixed(1)}MB)`))
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
console.log(chalk.gray(` NT 数据库扫描: ${ntResult.error || '未找到匹配的数据库'}`))
|
|
269
|
+
console.log(chalk.gray(' 提示: 可以稍后运行 weflow-cli init 重新扫描'))
|
|
270
|
+
}
|
|
271
|
+
} else if (detected.path.toLowerCase().includes('xwechat_files')) {
|
|
272
|
+
console.log(chalk.yellow('\n步骤 4/4: NT 格式数据库'))
|
|
273
|
+
console.log(chalk.gray(' 微信未运行,无法从内存中提取 NT 密钥'))
|
|
274
|
+
console.log(chalk.gray(' 请启动微信后运行: weflow-cli init'))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(chalk.cyan('\n=============================='))
|
|
278
|
+
console.log(chalk.cyan('初始化完成! (4.x 模式)'))
|
|
279
|
+
console.log(chalk.cyan('=============================='))
|
|
280
|
+
console.log(chalk.white(`数据目录: ${detected.path}`))
|
|
281
|
+
console.log(chalk.white(`账号: ${selectedWxid.wxid}${selectedWxid.nickname ? ` (${selectedWxid.nickname})` : ''}`))
|
|
282
|
+
console.log(chalk.white(`密钥: ${extractedKey.slice(0, 8)}...${extractedKey.slice(-8)}`))
|
|
283
|
+
const ntDbPath = configService.get('ntDbPath')
|
|
284
|
+
if (ntDbPath) {
|
|
285
|
+
console.log(chalk.white(`NT 数据库: ${ntDbPath}`))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log(chalk.gray('\n现在可以使用以下命令:'))
|
|
290
|
+
console.log(chalk.gray(' weflow-cli sessions 查看会话列表'))
|
|
291
|
+
console.log(chalk.gray(' weflow-cli messages <talker> 查看消息'))
|
|
292
|
+
console.log(chalk.gray(' weflow-cli contacts 查看联系人'))
|
|
293
|
+
console.log(chalk.gray(' weflow-cli export <talker> <format> 导出聊天记录'))
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// ==================== config ====================
|
|
297
|
+
const configCmd = program
|
|
298
|
+
.command('config')
|
|
299
|
+
.description('查看或修改配置')
|
|
300
|
+
|
|
301
|
+
configCmd
|
|
302
|
+
.command('show')
|
|
303
|
+
.description('显示当前配置')
|
|
304
|
+
.action(() => {
|
|
305
|
+
const config = configService.getAll()
|
|
306
|
+
console.log(chalk.cyan('当前配置:\n'))
|
|
307
|
+
console.log(`数据版本: ${config.dataVersion || chalk.gray('(自动检测)')}`)
|
|
308
|
+
console.log(`4.x 数据目录: ${config.dbPath || chalk.gray('(未设置)')}`)
|
|
309
|
+
console.log(`4.x 密钥: ${config.decryptKey ? '已设置' : chalk.gray('(未设置)')}`)
|
|
310
|
+
console.log(`3.x 数据库: ${config.dbPath3x || chalk.gray('(未设置)')}`)
|
|
311
|
+
console.log(`3.x 密钥: ${config.decryptKey3x ? '已设置' : chalk.gray('(未设置)')}`)
|
|
312
|
+
console.log(`NT 数据库: ${config.ntDbPath || chalk.gray('(未设置)')}`)
|
|
313
|
+
console.log(`NT 密钥: ${config.ntKey ? '已设置' : chalk.gray('(未设置)')}`)
|
|
314
|
+
console.log(`联系人DB: ${config.contactDbPath || chalk.gray('(未设置)')}`)
|
|
315
|
+
console.log(`联系人DB密钥: ${config.contactKey ? '已设置' : chalk.gray('(未设置)')}`)
|
|
316
|
+
console.log(`微信账号: ${config.wxid || chalk.gray('(未设置)')}`)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
configCmd
|
|
320
|
+
.command('set <key> <value>')
|
|
321
|
+
.description('设置配置项 (dbPath, decryptKey, dbPath3x, decryptKey3x, dataVersion, wxid)')
|
|
322
|
+
.action((key: string, value: string) => {
|
|
323
|
+
const validKeys = ['dbPath', 'decryptKey', 'dbPath3x', 'decryptKey3x', 'dataVersion', 'wxid', 'ntDbPath', 'ntKey', 'ntSalt', 'contactDbPath', 'contactKey', 'contactSalt', 'vaultRepo', 'aiEngine']
|
|
324
|
+
if (!validKeys.includes(key)) {
|
|
325
|
+
console.log(chalk.red(`无效的配置项: ${key}`))
|
|
326
|
+
console.log(chalk.gray(`可用: ${validKeys.join(', ')}`))
|
|
327
|
+
process.exit(1)
|
|
328
|
+
}
|
|
329
|
+
configService.set(key as any, value)
|
|
330
|
+
console.log(chalk.green(`✓ 已设置 ${key}`))
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
configCmd
|
|
334
|
+
.command('clear')
|
|
335
|
+
.description('清除所有配置')
|
|
336
|
+
.action(() => {
|
|
337
|
+
configService.clear()
|
|
338
|
+
console.log(chalk.green('✓ 配置已清除'))
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
// ==================== sessions ====================
|
|
342
|
+
program
|
|
343
|
+
.command('sessions')
|
|
344
|
+
.description('查看会话列表')
|
|
345
|
+
.option('-k, --keyword <keyword>', '搜索关键词')
|
|
346
|
+
.option('-n, --limit <number>', '最大数量', '30')
|
|
347
|
+
.action(async (opts) => {
|
|
348
|
+
if (!configService.isConfigured()) {
|
|
349
|
+
console.log(chalk.red('请先运行 weflow-cli init'))
|
|
350
|
+
process.exit(1)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const sessions = await chatService.listSessions(opts.keyword, parseInt(opts.limit))
|
|
354
|
+
if (sessions.length === 0) {
|
|
355
|
+
console.log(chalk.gray('未找到会话'))
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log(chalk.cyan(`会话列表 (${sessions.length} 条):\n`))
|
|
360
|
+
console.log(chalk.gray('序号 会话ID 昵称 最后消息'))
|
|
361
|
+
console.log(chalk.gray('─'.repeat(70)))
|
|
362
|
+
|
|
363
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
364
|
+
const s = sessions[i]
|
|
365
|
+
const num = String(i + 1).padStart(3)
|
|
366
|
+
const id = (s.username || '').padEnd(20)
|
|
367
|
+
const name = (s.displayName || '').slice(0, 12).padEnd(12)
|
|
368
|
+
const summary = (s.summary || '').slice(0, 30)
|
|
369
|
+
console.log(`${num} ${id} ${name} ${summary}`)
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// ==================== messages ====================
|
|
374
|
+
program
|
|
375
|
+
.command('messages <talker>')
|
|
376
|
+
.description('查看指定会话的消息 (支持 wxid / 昵称 / 备注 / 序号)')
|
|
377
|
+
.option('-n, --limit <number>', '最大数量', '50')
|
|
378
|
+
.option('-o, --offset <number>', '偏移量', '0')
|
|
379
|
+
.option('-s, --start <timestamp>', '开始时间戳')
|
|
380
|
+
.option('-e, --end <timestamp>', '结束时间戳')
|
|
381
|
+
.action(async (talkerInput: string, opts) => {
|
|
382
|
+
if (!configService.isConfigured()) {
|
|
383
|
+
console.log(chalk.red('\n❌ 还没配置'))
|
|
384
|
+
console.log(chalk.gray(' 运行: weflow-cli init\n'))
|
|
385
|
+
process.exit(1)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const talker = await resolveTalker(talkerInput)
|
|
389
|
+
|
|
390
|
+
const messages = await chatService.getMessages(
|
|
391
|
+
talker,
|
|
392
|
+
parseInt(opts.limit),
|
|
393
|
+
parseInt(opts.offset)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
if (messages.length === 0) {
|
|
397
|
+
console.log(chalk.gray('未找到消息'))
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(chalk.cyan(`消息记录 - ${talker} (${messages.length} 条):\n`))
|
|
402
|
+
|
|
403
|
+
for (const m of messages) {
|
|
404
|
+
const time = new Date(m.createTime * 1000).toLocaleString('zh-CN')
|
|
405
|
+
const senderName = m.isSend ? chalk.green('我') : chalk.blue((m as any).senderDisplay || m.senderUsername || talker)
|
|
406
|
+
const content = (m.parsedContent || m.rawContent || '').replace(/\n/g, ' ').slice(0, 80)
|
|
407
|
+
console.log(chalk.gray(`[${time}]`) + ` ${senderName}: ${content}`)
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// ==================== contacts ====================
|
|
412
|
+
program
|
|
413
|
+
.command('contacts')
|
|
414
|
+
.description('查看联系人列表')
|
|
415
|
+
.option('-k, --keyword <keyword>', '搜索关键词 (自动扩大搜索范围)')
|
|
416
|
+
.option('-n, --limit <number>', '最大数量', '500')
|
|
417
|
+
.action(async (opts) => {
|
|
418
|
+
if (!configService.isConfigured()) {
|
|
419
|
+
console.log(chalk.red('请先运行 weflow-cli init'))
|
|
420
|
+
process.exit(1)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const contacts = await chatService.listContacts(opts.keyword, parseInt(opts.limit))
|
|
424
|
+
if (contacts.length === 0) {
|
|
425
|
+
console.log(chalk.gray('未找到联系人'))
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
console.log(chalk.cyan(`联系人列表 (${contacts.length} 条):\n`))
|
|
430
|
+
console.log(chalk.gray('序号 用户ID 昵称/备注'))
|
|
431
|
+
console.log(chalk.gray('─'.repeat(60)))
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < contacts.length; i++) {
|
|
434
|
+
const c = contacts[i]
|
|
435
|
+
const num = String(i + 1).padStart(3)
|
|
436
|
+
const id = (c.username || '').padEnd(20)
|
|
437
|
+
const name = c.remark || c.displayName || c.nickname || ''
|
|
438
|
+
console.log(`${num} ${id} ${name}`)
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// ==================== export ====================
|
|
443
|
+
program
|
|
444
|
+
.command('export <talker> <format>')
|
|
445
|
+
.description('导出聊天记录 (支持 wxid / 昵称 / 备注 / 序号)')
|
|
446
|
+
.option('-o, --output <dir>', '输出目录', './output')
|
|
447
|
+
.option('-n, --limit <number>', '最大数量', '10000')
|
|
448
|
+
.action(async (talkerInput: string, format: string, opts) => {
|
|
449
|
+
if (!configService.isConfigured()) {
|
|
450
|
+
console.log(chalk.red('\n❌ 还没配置'))
|
|
451
|
+
console.log(chalk.gray(' 运行: weflow-cli init\n'))
|
|
452
|
+
process.exit(1)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const talker = await resolveTalker(talkerInput)
|
|
456
|
+
|
|
457
|
+
const validFormats = ['json', 'txt', 'html', 'excel']
|
|
458
|
+
if (!validFormats.includes(format)) {
|
|
459
|
+
console.log(chalk.red(`无效格式: ${format}`))
|
|
460
|
+
console.log(chalk.gray(`可用: ${validFormats.join(', ')}`))
|
|
461
|
+
process.exit(1)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(chalk.cyan(`正在导出 ${talker} 的聊天记录 (${format})...\n`))
|
|
465
|
+
|
|
466
|
+
let result
|
|
467
|
+
const limit = parseInt(opts.limit)
|
|
468
|
+
|
|
469
|
+
switch (format) {
|
|
470
|
+
case 'json':
|
|
471
|
+
result = await exportService.exportJson(talker, opts.output, limit)
|
|
472
|
+
break
|
|
473
|
+
case 'txt':
|
|
474
|
+
result = await exportService.exportTxt(talker, opts.output, limit)
|
|
475
|
+
break
|
|
476
|
+
case 'html':
|
|
477
|
+
result = await exportService.exportHtml(talker, opts.output, limit)
|
|
478
|
+
break
|
|
479
|
+
case 'excel':
|
|
480
|
+
result = await exportService.exportExcel(talker, opts.output, limit)
|
|
481
|
+
break
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (result?.success) {
|
|
485
|
+
console.log(chalk.green(`✓ 导出成功: ${result.path}`))
|
|
486
|
+
} else {
|
|
487
|
+
console.log(chalk.red(`✗ 导出失败: ${result?.error}`))
|
|
488
|
+
process.exit(1)
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// ==================== dbkey ====================
|
|
493
|
+
program
|
|
494
|
+
.command('dbkey')
|
|
495
|
+
.description('从运行中的微信进程提取数据库解密密钥 (自动检测版本)')
|
|
496
|
+
.option('-t, --timeout <ms>', '超时时间(毫秒)', '60000')
|
|
497
|
+
.action(async (opts) => {
|
|
498
|
+
console.log(chalk.cyan('🔑 提取微信数据库密钥\n'))
|
|
499
|
+
console.log(chalk.gray('请确保微信已登录且正在运行...\n'))
|
|
500
|
+
|
|
501
|
+
const version = await keyService.detectWeChatVersion()
|
|
502
|
+
if (!version) {
|
|
503
|
+
console.log(chalk.red('未检测到微信进程 (WeChat.exe 或 Weixin.exe)'))
|
|
504
|
+
process.exit(1)
|
|
505
|
+
}
|
|
506
|
+
console.log(chalk.gray(`检测到微信 ${version} 版本\n`))
|
|
507
|
+
|
|
508
|
+
if (version === '3.x') {
|
|
509
|
+
const result = await keyService.extract3xKey((msg) => {
|
|
510
|
+
console.log(chalk.gray(` ${msg}`))
|
|
511
|
+
})
|
|
512
|
+
if (result.success && result.key) {
|
|
513
|
+
console.log(chalk.green(`\n✓ 密钥: ${result.key}`))
|
|
514
|
+
console.log(chalk.green(` wxid: ${result.wxid}`))
|
|
515
|
+
console.log(chalk.green(` 消息目录: ${result.msgDir}`))
|
|
516
|
+
} else {
|
|
517
|
+
console.log(chalk.red(`\n✗ 失败: ${result.error}`))
|
|
518
|
+
process.exit(1)
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
const result = await keyService.autoGetDbKey(parseInt(opts.timeout), (msg) => {
|
|
522
|
+
console.log(chalk.gray(` ${msg}`))
|
|
523
|
+
})
|
|
524
|
+
if (result.success && result.key) {
|
|
525
|
+
console.log(chalk.green(`\n✓ 密钥: ${result.key}`))
|
|
526
|
+
} else {
|
|
527
|
+
console.log(chalk.red(`\n✗ 失败: ${result.error}`))
|
|
528
|
+
process.exit(1)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
// ==================== scan ====================
|
|
534
|
+
program
|
|
535
|
+
.command('scan')
|
|
536
|
+
.description('扫描微信数据目录中的账号')
|
|
537
|
+
.option('-p, --path <path>', '数据目录路径')
|
|
538
|
+
.action(async (opts) => {
|
|
539
|
+
const path = opts.path || configService.get('dbPath') || dbPathService.getDefaultPath()
|
|
540
|
+
console.log(chalk.cyan(`扫描目录: ${path}\n`))
|
|
541
|
+
|
|
542
|
+
const wxids = dbPathService.scanWxidCandidates(path)
|
|
543
|
+
if (wxids.length === 0) {
|
|
544
|
+
console.log(chalk.gray('未找到账号'))
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
console.log(chalk.cyan(`找到 ${wxids.length} 个账号:\n`))
|
|
549
|
+
for (const w of wxids) {
|
|
550
|
+
const time = new Date(w.modifiedTime).toLocaleString('zh-CN')
|
|
551
|
+
const nickname = w.nickname ? ` (${w.nickname})` : ''
|
|
552
|
+
console.log(` ${w.wxid}${nickname} ${chalk.gray(time)}`)
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
// ==================== login-wechat ====================
|
|
557
|
+
program
|
|
558
|
+
.command('login-wechat')
|
|
559
|
+
.description('微信扫码登录,获取消息收发权限')
|
|
560
|
+
.option('--base-url <url>', 'ilink 服务地址')
|
|
561
|
+
.action(async (opts) => {
|
|
562
|
+
const token = configService.get('wechatOcToken')
|
|
563
|
+
if (token) {
|
|
564
|
+
const { reconfirm } = await inquirer.prompt([{
|
|
565
|
+
type: 'confirm',
|
|
566
|
+
name: 'reconfirm',
|
|
567
|
+
message: '已有登录状态,要重新登录吗?',
|
|
568
|
+
default: false,
|
|
569
|
+
}])
|
|
570
|
+
if (!reconfirm) {
|
|
571
|
+
console.log(chalk.gray('保持当前登录状态'))
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
console.log(chalk.cyan('正在获取登录二维码...\n'))
|
|
577
|
+
const service = new WechatMessageService({ baseUrl: opts.baseUrl })
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const { qrcodeContent } = await service.startLogin()
|
|
581
|
+
console.log(chalk.green('请用微信扫描以下二维码:\n'))
|
|
582
|
+
|
|
583
|
+
// Print QR in terminal
|
|
584
|
+
try {
|
|
585
|
+
const QRCode = (await import('qrcode-terminal')).default
|
|
586
|
+
QRCode.generate(qrcodeContent, { small: true })
|
|
587
|
+
} catch {
|
|
588
|
+
console.log(chalk.yellow('(终端二维码显示失败,请安装: npm install qrcode-terminal)'))
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
console.log(chalk.cyan('\n等待扫码 (约2分钟有效)...'))
|
|
592
|
+
const session = await service.waitForLogin()
|
|
593
|
+
|
|
594
|
+
if (session.status === 'confirmed') {
|
|
595
|
+
console.log(chalk.green('\n✓ 登录成功!'))
|
|
596
|
+
console.log(chalk.gray(` account_id: ${session.accountId || '未知'}`))
|
|
597
|
+
} else {
|
|
598
|
+
console.log(chalk.red(`\n✗ 登录失败: ${session.error || '超时'}`))
|
|
599
|
+
}
|
|
600
|
+
} catch (e: any) {
|
|
601
|
+
console.log(chalk.red(`\n✗ 登录失败: ${e.message}`))
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
// ==================== logout-wechat ====================
|
|
606
|
+
program
|
|
607
|
+
.command('logout-wechat')
|
|
608
|
+
.description('退出微信消息通道登录')
|
|
609
|
+
.action(() => {
|
|
610
|
+
configService.set('wechatOcToken', '')
|
|
611
|
+
configService.set('wechatOcAccountId', '')
|
|
612
|
+
configService.set('wechatOcSyncBuf', '')
|
|
613
|
+
console.log(chalk.green('✓ 已退出登录'))
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
// ==================== whitelist ====================
|
|
617
|
+
const whitelistCmd = program
|
|
618
|
+
.command('whitelist')
|
|
619
|
+
.description('白名单管理 (不传参数显示列表)')
|
|
620
|
+
|
|
621
|
+
whitelistCmd
|
|
622
|
+
.action(() => {
|
|
623
|
+
const list = whitelistService.getList()
|
|
624
|
+
if (list.length === 0) {
|
|
625
|
+
console.log(chalk.gray('白名单为空'))
|
|
626
|
+
console.log(chalk.gray('使用 weflow-cli whitelist add <昵称> 添加'))
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
console.log(chalk.cyan(`白名单 (${list.length}):\n`))
|
|
630
|
+
for (let i = 0; i < list.length; i++) {
|
|
631
|
+
console.log(` ${String(i + 1).padStart(2)}. ${list[i]}`)
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
whitelistCmd
|
|
636
|
+
.command('add <target>')
|
|
637
|
+
.description('添加白名单 (支持 wxid/昵称/备注名/序号)')
|
|
638
|
+
.action(async (target: string) => {
|
|
639
|
+
// 解析目标
|
|
640
|
+
let wxid: string
|
|
641
|
+
let displayName = target
|
|
642
|
+
if (target.startsWith('wxid_') || target.includes('@chatroom') || target.includes('@openim')) {
|
|
643
|
+
wxid = target
|
|
644
|
+
} else {
|
|
645
|
+
try {
|
|
646
|
+
wxid = await resolveTalker(target)
|
|
647
|
+
const sessions = await chatService.listSessions(undefined, 50)
|
|
648
|
+
const match = sessions.find(s => s.username === wxid)
|
|
649
|
+
if (match) displayName = match.displayName || target
|
|
650
|
+
} catch (e: any) {
|
|
651
|
+
console.log(chalk.red(`✗ ${e.message}`))
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 检查是否已在名单中
|
|
657
|
+
if (whitelistService.isAllowed(wxid)) {
|
|
658
|
+
console.log(chalk.yellow(`"${displayName}" (${wxid}) 已在白名单中`))
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// 三重确认
|
|
663
|
+
console.log(chalk.cyan(`\n⚠️ 即将添加以下联系人到白名单:`))
|
|
664
|
+
console.log(chalk.white(` 昵称: ${displayName}`))
|
|
665
|
+
console.log(chalk.gray(` wxid: ${wxid}`))
|
|
666
|
+
console.log(chalk.yellow(` 添加后,该联系人可向你收发消息\n`))
|
|
667
|
+
|
|
668
|
+
const { q1 } = await inquirer.prompt([{
|
|
669
|
+
type: 'confirm',
|
|
670
|
+
name: 'q1',
|
|
671
|
+
message: `确认添加 "${displayName}" 到白名单?`,
|
|
672
|
+
default: false,
|
|
673
|
+
}])
|
|
674
|
+
if (!q1) { console.log(chalk.gray('已取消')); return }
|
|
675
|
+
|
|
676
|
+
// 再确认一次(防止误操作)
|
|
677
|
+
const { q2 } = await inquirer.prompt([{
|
|
678
|
+
type: 'input',
|
|
679
|
+
name: 'q2',
|
|
680
|
+
message: `请输入 "确认" 以继续:`,
|
|
681
|
+
}])
|
|
682
|
+
if (q2 !== '确认') { console.log(chalk.gray('已取消')); return }
|
|
683
|
+
|
|
684
|
+
whitelistService.addDirect(wxid)
|
|
685
|
+
console.log(chalk.green(`\n✓ 已添加 "${displayName}" 到白名单`))
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
whitelistCmd
|
|
689
|
+
.command('rm <wxid>')
|
|
690
|
+
.description('移除白名单中的 wxid')
|
|
691
|
+
.action((wxid: string) => {
|
|
692
|
+
if (whitelistService.remove(wxid)) {
|
|
693
|
+
console.log(chalk.green(`✓ 已移除: ${wxid}`))
|
|
694
|
+
} else {
|
|
695
|
+
console.log(chalk.yellow(`未找到: ${wxid}`))
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
whitelistCmd
|
|
700
|
+
.command('clear')
|
|
701
|
+
.description('清空白名单')
|
|
702
|
+
.action(async () => {
|
|
703
|
+
const { confirm } = await inquirer.prompt([{
|
|
704
|
+
type: 'confirm',
|
|
705
|
+
name: 'confirm',
|
|
706
|
+
message: '确定要清空所有白名单吗?',
|
|
707
|
+
default: false,
|
|
708
|
+
}])
|
|
709
|
+
if (confirm) {
|
|
710
|
+
whitelistService.clear()
|
|
711
|
+
console.log(chalk.green('✓ 白名单已清空'))
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
// ==================== send ====================
|
|
716
|
+
program
|
|
717
|
+
.command('send <target> <message>')
|
|
718
|
+
.description('发送文本消息给指定联系人')
|
|
719
|
+
.option('--image <path>', '发图片')
|
|
720
|
+
.option('--file <path>', '发文件')
|
|
721
|
+
.action(async (target: string, message: string, opts) => {
|
|
722
|
+
// Resolve target
|
|
723
|
+
let wxid: string
|
|
724
|
+
try {
|
|
725
|
+
wxid = await resolveTalker(target)
|
|
726
|
+
} catch (e: any) {
|
|
727
|
+
console.log(chalk.red(`${e.message}`))
|
|
728
|
+
process.exit(1)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Whitelist check
|
|
732
|
+
if (!whitelistService.isAllowed(wxid)) {
|
|
733
|
+
console.log(chalk.red('\n❌ 目标不在白名单中,拒绝发送'))
|
|
734
|
+
console.log(chalk.gray(` 先运行: weflow-cli whitelist add ${target}`))
|
|
735
|
+
console.log(chalk.gray(` 查看名单: weflow-cli whitelist\n`))
|
|
736
|
+
process.exit(1)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// 二次确认
|
|
740
|
+
const { confirm } = await inquirer.prompt([{
|
|
741
|
+
type: 'confirm',
|
|
742
|
+
name: 'confirm',
|
|
743
|
+
message: `确认发送给 ${chalk.cyan(target)}?`,
|
|
744
|
+
default: false,
|
|
745
|
+
}])
|
|
746
|
+
if (!confirm) {
|
|
747
|
+
console.log(chalk.gray('已取消'))
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Login check
|
|
752
|
+
const token = configService.get('wechatOcToken')
|
|
753
|
+
if (!token) {
|
|
754
|
+
console.log(chalk.red('\n❌ 未登录消息通道'))
|
|
755
|
+
console.log(chalk.gray(' 先运行: weflow-cli login-wechat\n'))
|
|
756
|
+
process.exit(1)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const service = new WechatMessageService({ token })
|
|
760
|
+
|
|
761
|
+
let success = false
|
|
762
|
+
if (opts.image) {
|
|
763
|
+
success = await service.sendImage(wxid, opts.image)
|
|
764
|
+
} else if (opts.file) {
|
|
765
|
+
success = await service.sendFile(wxid, opts.file)
|
|
766
|
+
} else {
|
|
767
|
+
success = await service.sendText(wxid, message)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (success) {
|
|
771
|
+
console.log(chalk.green('✓ 发送成功'))
|
|
772
|
+
} else {
|
|
773
|
+
console.log(chalk.red('✗ 发送失败 — 可能需要先收到对方一条消息 (获取 context_token)'))
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
// ==================== listen ====================
|
|
778
|
+
program
|
|
779
|
+
.command('listen')
|
|
780
|
+
.description('监听微信消息 (Ctrl+C 退出)')
|
|
781
|
+
.option('--target <wxid>', '只显示指定用户的消息')
|
|
782
|
+
.action(async (opts) => {
|
|
783
|
+
const token = configService.get('wechatOcToken')
|
|
784
|
+
if (!token) {
|
|
785
|
+
console.log(chalk.red('\n❌ 未登录消息通道'))
|
|
786
|
+
console.log(chalk.gray(' 先运行: weflow-cli login-wechat\n'))
|
|
787
|
+
process.exit(1)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Whitelist must be non-empty before listening
|
|
791
|
+
const wl = whitelistService.getList()
|
|
792
|
+
if (wl.length === 0) {
|
|
793
|
+
console.log(chalk.red('\n❌ 白名单为空,拒绝监听'))
|
|
794
|
+
console.log(chalk.gray(' 为了防止收到非预期消息,必须先配置白名单'))
|
|
795
|
+
console.log(chalk.gray(' 运行: weflow-cli whitelist add <昵称>\n'))
|
|
796
|
+
process.exit(1)
|
|
797
|
+
}
|
|
798
|
+
console.log(chalk.gray(`当前白名单: ${wl.join(', ')}`))
|
|
799
|
+
|
|
800
|
+
const service = new WechatMessageService({ token })
|
|
801
|
+
console.log(chalk.cyan('\n开始监听消息... (Ctrl+C 退出)\n'))
|
|
802
|
+
|
|
803
|
+
service.onMessage((msg) => {
|
|
804
|
+
// Whitelist filter (strict)
|
|
805
|
+
if (!whitelistService.isAllowed(msg.fromUserId)) {
|
|
806
|
+
return
|
|
807
|
+
}
|
|
808
|
+
// Target filter
|
|
809
|
+
if (opts.target && msg.fromUserId !== opts.target) {
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const time = new Date(msg.timestampMs).toLocaleString('zh-CN')
|
|
814
|
+
const kind = msg.messageKind !== 'text' ? ` [${msg.messageKind}]` : ''
|
|
815
|
+
console.log(chalk.gray(`[${time}]`) + ` ${chalk.blue(msg.senderNickname || msg.fromUserId)}:${kind} ${msg.messageStr}`)
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
// Graceful shutdown
|
|
819
|
+
process.on('SIGINT', async () => {
|
|
820
|
+
console.log(chalk.gray('\n正在停止监听...'))
|
|
821
|
+
await service.stop()
|
|
822
|
+
process.exit(0)
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
await service.startPolling()
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
// ==================== report ====================
|
|
829
|
+
program
|
|
830
|
+
.command('report')
|
|
831
|
+
.description('生成聊天月报(AI 分析任务和回复)')
|
|
832
|
+
.option('--month <YYYY-MM>', '指定月份')
|
|
833
|
+
.option('--talker <昵称>', '指定联系人(可多次使用)')
|
|
834
|
+
.option('--from-whitelist', '使用白名单中的联系人')
|
|
835
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
836
|
+
.option('--no-ai', '仅统计,不调用 AI')
|
|
837
|
+
.option('-o, --output <dir>', '输出目录', './output')
|
|
838
|
+
.action(async (opts) => {
|
|
839
|
+
if (!configService.isConfigured()) {
|
|
840
|
+
console.log(chalk.red('\n❌ 还没配置\n 运行: weflow-cli init\n'))
|
|
841
|
+
process.exit(1)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const { execFile } = await import('child_process')
|
|
845
|
+
const { promisify } = await import('util')
|
|
846
|
+
const execFileAsync = promisify(execFile)
|
|
847
|
+
const { fileURLToPath } = await import('url')
|
|
848
|
+
const { dirname } = await import('path')
|
|
849
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
850
|
+
const __dirname = dirname(__filename)
|
|
851
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
852
|
+
const script = join(pkgRoot, 'scripts', 'chat_report.py')
|
|
853
|
+
|
|
854
|
+
const args: string[] = [script]
|
|
855
|
+
if (opts.month) args.push('--month', opts.month)
|
|
856
|
+
if (opts.talker) {
|
|
857
|
+
// commander collects repeated --talker into array, single value is string
|
|
858
|
+
const talkers = Array.isArray(opts.talker) ? opts.talker : [opts.talker]
|
|
859
|
+
for (const t of talkers) args.push('--talker', t)
|
|
860
|
+
}
|
|
861
|
+
if (opts.fromWhitelist) args.push('--from-whitelist')
|
|
862
|
+
if (opts.apiKey) args.push('--api-key', opts.apiKey)
|
|
863
|
+
if (opts.ai === false) args.push('--no-ai')
|
|
864
|
+
if (opts.output) args.push('--output', opts.output)
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
console.log(chalk.cyan('正在生成月报...\n'))
|
|
868
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
869
|
+
timeout: 600_000,
|
|
870
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
871
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
872
|
+
})
|
|
873
|
+
console.log(stdout)
|
|
874
|
+
} catch (e: any) {
|
|
875
|
+
console.error(chalk.red(`\n✗ 生成失败: ${e.message}`))
|
|
876
|
+
process.exit(1)
|
|
877
|
+
}
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
// vault init
|
|
881
|
+
program
|
|
882
|
+
.command('vault')
|
|
883
|
+
.description('Obsidian Vault 管理')
|
|
884
|
+
.addCommand(
|
|
885
|
+
new Command('init')
|
|
886
|
+
.description('初始化 Obsidian Vault 目录结构')
|
|
887
|
+
.option('-p, --path <dir>', 'Vault 路径', './output/wechat-vault')
|
|
888
|
+
.action(async (opts) => {
|
|
889
|
+
const { mkdirSync, writeFileSync } = await import('fs')
|
|
890
|
+
const { join } = await import('path')
|
|
891
|
+
|
|
892
|
+
const vaultPath = opts.path
|
|
893
|
+
console.log(chalk.cyan(`\n🔧 初始化 Obsidian Vault: ${vaultPath}\n`))
|
|
894
|
+
|
|
895
|
+
// Directory structure
|
|
896
|
+
const dirs = [
|
|
897
|
+
'.obsidian',
|
|
898
|
+
'Templates',
|
|
899
|
+
'Sources/WeChat',
|
|
900
|
+
'Wiki/Concepts',
|
|
901
|
+
'Wiki/Entities',
|
|
902
|
+
'Wiki/Topics',
|
|
903
|
+
]
|
|
904
|
+
for (const d of dirs) {
|
|
905
|
+
mkdirSync(join(vaultPath, d), { recursive: true })
|
|
906
|
+
console.log(` ✓ ${d}/`)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// .obsidian/app.json
|
|
910
|
+
writeFileSync(join(vaultPath, '.obsidian', 'app.json'), JSON.stringify({
|
|
911
|
+
"newFileLocation": "folder",
|
|
912
|
+
"newFileFolderPath": "Sources",
|
|
913
|
+
"attachmentFolderPath": "Assets",
|
|
914
|
+
"showInlineTitle": false,
|
|
915
|
+
}, null, 2), 'utf-8')
|
|
916
|
+
|
|
917
|
+
// Templates/article.md
|
|
918
|
+
writeFileSync(join(vaultPath, 'Templates', 'article.md'), [
|
|
919
|
+
'---',
|
|
920
|
+
'title: "{{title}}"',
|
|
921
|
+
'source: ""',
|
|
922
|
+
'date: {{date}}',
|
|
923
|
+
'topic: AI',
|
|
924
|
+
'tags: []',
|
|
925
|
+
'created: {{date}}',
|
|
926
|
+
'---',
|
|
927
|
+
'',
|
|
928
|
+
'# {{title}}',
|
|
929
|
+
'',
|
|
930
|
+
'> 来源: ',
|
|
931
|
+
'> 时间:{{date}} ',
|
|
932
|
+
'',
|
|
933
|
+
'---',
|
|
934
|
+
'',
|
|
935
|
+
'## AI 摘要',
|
|
936
|
+
'',
|
|
937
|
+
'',
|
|
938
|
+
'## 相关概念',
|
|
939
|
+
'',
|
|
940
|
+
'',
|
|
941
|
+
'---',
|
|
942
|
+
'',
|
|
943
|
+
'## 正文',
|
|
944
|
+
'',
|
|
945
|
+
].join('\n'), 'utf-8')
|
|
946
|
+
|
|
947
|
+
// README.md
|
|
948
|
+
writeFileSync(join(vaultPath, 'README.md'), [
|
|
949
|
+
'# WeChat Knowledge Vault',
|
|
950
|
+
'',
|
|
951
|
+
'> 由 weflow-cli 自动生成,兼容 Obsidian。',
|
|
952
|
+
'',
|
|
953
|
+
'## 目录结构',
|
|
954
|
+
'',
|
|
955
|
+
'| 目录 | 说明 |',
|
|
956
|
+
'|------|------|',
|
|
957
|
+
'| `Sources/WeChat/` | 公众号文章(按日期+主题分类) |',
|
|
958
|
+
'| `Wiki/Concepts/` | 概念页(手动或 AI 生成) |',
|
|
959
|
+
'| `Wiki/Entities/` | 实体页(公众号、作者等) |',
|
|
960
|
+
'| `Wiki/Topics/` | 主题总览页 |',
|
|
961
|
+
'| `Templates/` | 模板文件 |',
|
|
962
|
+
'',
|
|
963
|
+
'## 快速查询',
|
|
964
|
+
'',
|
|
965
|
+
'使用 Obsidian Dataview 插件:',
|
|
966
|
+
'',
|
|
967
|
+
'```dataview',
|
|
968
|
+
'TABLE date, topic, tags',
|
|
969
|
+
'FROM "Sources/WeChat"',
|
|
970
|
+
'WHERE topic = "AI"',
|
|
971
|
+
'SORT date DESC',
|
|
972
|
+
'```',
|
|
973
|
+
'',
|
|
974
|
+
'```dataview',
|
|
975
|
+
'TABLE length(rows) as "篇数"',
|
|
976
|
+
'FROM "Sources/WeChat"',
|
|
977
|
+
'GROUP BY topic',
|
|
978
|
+
'SORT rows.length DESC',
|
|
979
|
+
'```',
|
|
980
|
+
'',
|
|
981
|
+
'## 每日更新',
|
|
982
|
+
'',
|
|
983
|
+
'```bash',
|
|
984
|
+
'# 生成今日日报',
|
|
985
|
+
'python scripts/biz_daily.py --api-key <key>',
|
|
986
|
+
'',
|
|
987
|
+
'# 后处理(广告清洗+深度摘要)',
|
|
988
|
+
'python scripts/classify_daily.py --api-key <key> --interest AI',
|
|
989
|
+
'',
|
|
990
|
+
'# 同步到 GitHub',
|
|
991
|
+
'# (见 OPERATIONS.md)',
|
|
992
|
+
'```',
|
|
993
|
+
'',
|
|
994
|
+
'---',
|
|
995
|
+
'',
|
|
996
|
+
'*由 weflow-cli vault init 生成*',
|
|
997
|
+
].join('\n'), 'utf-8')
|
|
998
|
+
|
|
999
|
+
// .gitignore
|
|
1000
|
+
writeFileSync(join(vaultPath, '.gitignore'), [
|
|
1001
|
+
'.obsidian/workspace*.json',
|
|
1002
|
+
'.obsidian/hotkeys.json',
|
|
1003
|
+
'.trash/',
|
|
1004
|
+
'.DS_Store',
|
|
1005
|
+
].join('\n'), 'utf-8')
|
|
1006
|
+
|
|
1007
|
+
console.log(chalk.green(`\n✓ Vault 创建完成!`))
|
|
1008
|
+
console.log(` 用 Obsidian 打开: File → Open Vault → ${vaultPath}`)
|
|
1009
|
+
})
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
// wiki compile
|
|
1013
|
+
program
|
|
1014
|
+
.command('wiki')
|
|
1015
|
+
.description('概念图谱管理')
|
|
1016
|
+
.addCommand(
|
|
1017
|
+
new Command('compile')
|
|
1018
|
+
.description('扫描文章 [[Wikilinks]] 聚合生成概念页')
|
|
1019
|
+
.option('-l, --limit <n>', '最多生成概念数', '20')
|
|
1020
|
+
.option('--source <dir>', '文章目录', './output/biz-daily')
|
|
1021
|
+
.option('-o, --output <dir>', '概念页输出目录', './output/wechat-vault/Wiki/Concepts')
|
|
1022
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
1023
|
+
.action(async (opts) => {
|
|
1024
|
+
const { execFile } = await import('child_process')
|
|
1025
|
+
const { promisify } = await import('util')
|
|
1026
|
+
const execFileAsync = promisify(execFile)
|
|
1027
|
+
const { fileURLToPath } = await import('url')
|
|
1028
|
+
const { dirname } = await import('path')
|
|
1029
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1030
|
+
const __dirname = dirname(__filename)
|
|
1031
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1032
|
+
const script = join(pkgRoot, 'scripts', 'compile_wiki.py')
|
|
1033
|
+
|
|
1034
|
+
const args: string[] = [script, '--limit', opts.limit, '--source', opts.source, '--output', opts.output]
|
|
1035
|
+
if (opts.apiKey) args.push('--api-key', opts.apiKey)
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
console.log(chalk.cyan('正在编译概念图谱...\n'))
|
|
1039
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
1040
|
+
timeout: 300_000,
|
|
1041
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1042
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1043
|
+
})
|
|
1044
|
+
console.log(stdout)
|
|
1045
|
+
} catch (e: any) {
|
|
1046
|
+
console.error(chalk.red(`\n✗ 编译失败: ${e.message}`))
|
|
1047
|
+
process.exit(1)
|
|
1048
|
+
}
|
|
1049
|
+
})
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
// pipeline run
|
|
1053
|
+
program
|
|
1054
|
+
.command('pipeline')
|
|
1055
|
+
.description('端到端自动化流水线')
|
|
1056
|
+
.addCommand(
|
|
1057
|
+
new Command('run')
|
|
1058
|
+
.description('一键运行 biz_daily → classify → wiki compile')
|
|
1059
|
+
.option('--date <YYYY-MM-DD>', '日期')
|
|
1060
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
1061
|
+
.option('--interest <topic>', '兴趣主题', 'AI')
|
|
1062
|
+
.option('--wiki-limit <n>', '概念编译数', '20')
|
|
1063
|
+
.action(async (opts) => {
|
|
1064
|
+
const { execFile } = await import('child_process')
|
|
1065
|
+
const { promisify } = await import('util')
|
|
1066
|
+
const execFileAsync = promisify(execFile)
|
|
1067
|
+
const { fileURLToPath } = await import('url')
|
|
1068
|
+
const { dirname } = await import('path')
|
|
1069
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1070
|
+
const __dirname = dirname(__filename)
|
|
1071
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1072
|
+
const script = join(pkgRoot, 'scripts', 'pipeline.py')
|
|
1073
|
+
|
|
1074
|
+
const args: string[] = [script, '--api-key', opts.apiKey, '--interest', opts.interest, '--wiki-limit', opts.wikiLimit]
|
|
1075
|
+
if (opts.date) args.push('--date', opts.date)
|
|
1076
|
+
|
|
1077
|
+
try {
|
|
1078
|
+
console.log(chalk.cyan('\n启动端到端流水线...\n'))
|
|
1079
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
1080
|
+
timeout: 600_000,
|
|
1081
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
1082
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1083
|
+
})
|
|
1084
|
+
console.log(stdout)
|
|
1085
|
+
} catch (e: any) {
|
|
1086
|
+
console.error(chalk.red(`\n✗ 流水线失败: ${e.message}`))
|
|
1087
|
+
process.exit(1)
|
|
1088
|
+
}
|
|
1089
|
+
})
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
// vault sync — add to existing vault command
|
|
1093
|
+
program.commands
|
|
1094
|
+
.find(c => c.name() === 'vault')
|
|
1095
|
+
?.addCommand(
|
|
1096
|
+
new Command('sync')
|
|
1097
|
+
.description('增量提交 Vault 并推送到远端仓库')
|
|
1098
|
+
.option('-r, --repo <url>', '远端 Git 仓库地址')
|
|
1099
|
+
.option('-b, --branch <name>', '分支名', 'main')
|
|
1100
|
+
.option('--include-chat', '包含聊天记录(默认不同步)')
|
|
1101
|
+
.action(async (opts) => {
|
|
1102
|
+
const { execFile } = await import('child_process')
|
|
1103
|
+
const { promisify } = await import('util')
|
|
1104
|
+
const execFileAsync = promisify(execFile)
|
|
1105
|
+
const vaultPath = join(process.cwd(), 'output', 'wechat-vault')
|
|
1106
|
+
|
|
1107
|
+
const repo = opts.repo || configService.get('vaultRepo')
|
|
1108
|
+
if (!repo) {
|
|
1109
|
+
console.error(chalk.red('\n❌ 未指定远端仓库。运行: weflow-cli config set vaultRepo <url> 或使用 --repo 参数\n'))
|
|
1110
|
+
process.exit(1)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
// Ensure vault is a git repo
|
|
1115
|
+
const gitDir = join(vaultPath, '.git')
|
|
1116
|
+
const { existsSync } = await import('fs')
|
|
1117
|
+
if (!existsSync(gitDir)) {
|
|
1118
|
+
console.log(chalk.cyan('初始化 Vault Git 仓库...'))
|
|
1119
|
+
await execFileAsync('git', ['init'], { cwd: vaultPath })
|
|
1120
|
+
await execFileAsync('git', ['remote', 'add', 'origin', repo], { cwd: vaultPath })
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Check for changes
|
|
1124
|
+
try {
|
|
1125
|
+
await execFileAsync('git', ['diff', '--cached', '--quiet'], { cwd: vaultPath })
|
|
1126
|
+
await execFileAsync('git', ['diff', '--quiet'], { cwd: vaultPath })
|
|
1127
|
+
console.log(chalk.yellow('没有变更,跳过同步'))
|
|
1128
|
+
return
|
|
1129
|
+
} catch {
|
|
1130
|
+
// Has changes, proceed
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const dateStr = new Date().toISOString().slice(0, 10)
|
|
1134
|
+
const { stdout: statOut } = await execFileAsync('git', ['diff', '--stat'], { cwd: vaultPath })
|
|
1135
|
+
|
|
1136
|
+
console.log(chalk.cyan('提交变更...'))
|
|
1137
|
+
await execFileAsync('git', ['add', '-A'], { cwd: vaultPath })
|
|
1138
|
+
await execFileAsync('git', ['commit', '-m', `vault sync: ${dateStr} | ${statOut.split('\n').length} files`], { cwd: vaultPath })
|
|
1139
|
+
|
|
1140
|
+
console.log(chalk.cyan('推送到远端...'))
|
|
1141
|
+
await execFileAsync('git', ['push', '-u', 'origin', opts.branch], { cwd: vaultPath, timeout: 60_000 })
|
|
1142
|
+
|
|
1143
|
+
console.log(chalk.green(`\n✓ Vault 已同步到 ${repo} (${opts.branch})`))
|
|
1144
|
+
} catch (e: any) {
|
|
1145
|
+
console.error(chalk.red(`\n✗ 同步失败: ${e.message}`))
|
|
1146
|
+
process.exit(1)
|
|
1147
|
+
}
|
|
1148
|
+
})
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
// vault enrich — bidirectional backlinks
|
|
1152
|
+
program.commands
|
|
1153
|
+
.find(c => c.name() === 'vault')
|
|
1154
|
+
?.addCommand(
|
|
1155
|
+
new Command('enrich')
|
|
1156
|
+
.description('增强双向链接 — 为文章添加相关阅读段落')
|
|
1157
|
+
.option('--date <YYYY-MM-DD>', '日期(默认今天)')
|
|
1158
|
+
.action(async (opts) => {
|
|
1159
|
+
const { fileURLToPath } = await import('url')
|
|
1160
|
+
const { dirname } = await import('path')
|
|
1161
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1162
|
+
const __dirname = dirname(__filename)
|
|
1163
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1164
|
+
const script = join(pkgRoot, 'scripts', 'enrich_backlinks.py')
|
|
1165
|
+
if (!opts.date) {
|
|
1166
|
+
const now = new Date()
|
|
1167
|
+
opts.date = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`
|
|
1168
|
+
}
|
|
1169
|
+
await runPythonCmd(script, ['--date', opts.date])
|
|
1170
|
+
})
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
// vault notes — create reading notes
|
|
1174
|
+
program.commands
|
|
1175
|
+
.find(c => c.name() === 'vault')
|
|
1176
|
+
?.addCommand(
|
|
1177
|
+
new Command('notes')
|
|
1178
|
+
.description('创建阅读笔记 — 为文章生成 Vault 笔记页')
|
|
1179
|
+
.option('--date <YYYY-MM-DD>', '日期(默认今天)')
|
|
1180
|
+
.option('--vault <path>', 'Vault 路径', './output/wechat-vault')
|
|
1181
|
+
.action(async (opts) => {
|
|
1182
|
+
const { fileURLToPath } = await import('url')
|
|
1183
|
+
const { dirname } = await import('path')
|
|
1184
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1185
|
+
const __dirname = dirname(__filename)
|
|
1186
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1187
|
+
const script = join(pkgRoot, 'scripts', 'create_reading_notes.py')
|
|
1188
|
+
if (!opts.date) {
|
|
1189
|
+
const now = new Date()
|
|
1190
|
+
opts.date = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`
|
|
1191
|
+
}
|
|
1192
|
+
await runPythonCmd(script, ['--date', opts.date, '--vault', opts.vault])
|
|
1193
|
+
})
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
// vault tag — auto tag
|
|
1197
|
+
program.commands
|
|
1198
|
+
.find(c => c.name() === 'vault')
|
|
1199
|
+
?.addCommand(
|
|
1200
|
+
new Command('tag')
|
|
1201
|
+
.description('自动标签 — AI 为文章补充标签')
|
|
1202
|
+
.option('--date <YYYY-MM-DD>', '日期(默认今天)')
|
|
1203
|
+
.action(async (opts) => {
|
|
1204
|
+
const { fileURLToPath } = await import('url')
|
|
1205
|
+
const { dirname } = await import('path')
|
|
1206
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1207
|
+
const __dirname = dirname(__filename)
|
|
1208
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1209
|
+
const script = join(pkgRoot, 'scripts', 'auto_tag.py')
|
|
1210
|
+
if (!opts.date) { const n=new Date(); opts.date=`${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,'0')}-${String(n.getDate()).padStart(2,'0')}` }
|
|
1211
|
+
await runPythonCmd(script, ['--date', opts.date])
|
|
1212
|
+
})
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
// vault search — search across Vault
|
|
1216
|
+
program.commands
|
|
1217
|
+
.find(c => c.name() === 'vault')
|
|
1218
|
+
?.addCommand(
|
|
1219
|
+
new Command('search')
|
|
1220
|
+
.description('Vault 搜索 — 文章+概念+笔记')
|
|
1221
|
+
.argument('<query>', '搜索关键词')
|
|
1222
|
+
.option('--type <type>', 'all / article / concept / note', 'all')
|
|
1223
|
+
.option('--top-k <n>', '返回数量', '10')
|
|
1224
|
+
.action(async (query, opts) => {
|
|
1225
|
+
const { fileURLToPath } = await import('url')
|
|
1226
|
+
const { dirname } = await import('path')
|
|
1227
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1228
|
+
const __dirname = dirname(__filename)
|
|
1229
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1230
|
+
const script = join(pkgRoot, 'scripts', 'vault_search.py')
|
|
1231
|
+
await runPythonCmd(script, [query, '--type', opts.type, '--top-k', opts.topK, '--json'])
|
|
1232
|
+
})
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
// vault rag — RAG dialog over Vault
|
|
1236
|
+
program.commands
|
|
1237
|
+
.find(c => c.name() === 'vault')
|
|
1238
|
+
?.addCommand(
|
|
1239
|
+
new Command('rag')
|
|
1240
|
+
.description('Vault 问答 — 基于知识库的 AI 对话')
|
|
1241
|
+
.argument('<question>', '问题')
|
|
1242
|
+
.option('--top-k <n>', '检索条数', '8')
|
|
1243
|
+
.action(async (question, opts) => {
|
|
1244
|
+
const { fileURLToPath } = await import('url')
|
|
1245
|
+
const { dirname } = await import('path')
|
|
1246
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1247
|
+
const __dirname = dirname(__filename)
|
|
1248
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1249
|
+
const script = join(pkgRoot, 'scripts', 'vault_rag.py')
|
|
1250
|
+
await runPythonCmd(script, [question, '--top-k', opts.topK])
|
|
1251
|
+
})
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
// vault sync-weread — sync WeRead to Vault
|
|
1255
|
+
program.commands
|
|
1256
|
+
.find(c => c.name() === 'vault')
|
|
1257
|
+
?.addCommand(
|
|
1258
|
+
new Command('sync-weread')
|
|
1259
|
+
.description('微信读书同步到 Vault')
|
|
1260
|
+
.option('--type <type>', 'all / shelf / notes', 'all')
|
|
1261
|
+
.action(async (opts) => {
|
|
1262
|
+
const { fileURLToPath } = await import('url')
|
|
1263
|
+
const { dirname } = await import('path')
|
|
1264
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1265
|
+
const __dirname = dirname(__filename)
|
|
1266
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1267
|
+
const script = join(pkgRoot, 'scripts', 'sync_weread.py')
|
|
1268
|
+
await runPythonCmd(script, ['--type', opts.type])
|
|
1269
|
+
})
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
// chat-stats
|
|
1273
|
+
program
|
|
1274
|
+
.command('chat-stats')
|
|
1275
|
+
.description('微信个人信息消费报告(支付 + 转账)')
|
|
1276
|
+
.option('--period <week|month>', '报告周期', 'week')
|
|
1277
|
+
.option('--month <YYYY-MM>', '指定月份(覆盖 --period)')
|
|
1278
|
+
.option('-o, --output <path>', '输出路径')
|
|
1279
|
+
.action(async (opts) => {
|
|
1280
|
+
const { execFile } = await import('child_process')
|
|
1281
|
+
const { promisify } = await import('util')
|
|
1282
|
+
const execFileAsync = promisify(execFile)
|
|
1283
|
+
const { fileURLToPath } = await import('url')
|
|
1284
|
+
const { dirname } = await import('path')
|
|
1285
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1286
|
+
const __dirname = dirname(__filename)
|
|
1287
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1288
|
+
const script = join(pkgRoot, 'scripts', 'chat_stats.py')
|
|
1289
|
+
const args: string[] = [script, '--period', opts.period]
|
|
1290
|
+
if (opts.month) { args.push('--month', opts.month) }
|
|
1291
|
+
if (opts.output) { args.push('--output', opts.output) }
|
|
1292
|
+
try {
|
|
1293
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
1294
|
+
timeout: 600_000, maxBuffer: 50 * 1024 * 1024,
|
|
1295
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1296
|
+
})
|
|
1297
|
+
console.log(stdout)
|
|
1298
|
+
} catch (e: any) {
|
|
1299
|
+
console.error(chalk.red(`\n✗ 失败: ${e.message}`))
|
|
1300
|
+
process.exit(1)
|
|
1301
|
+
}
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
// generate-review
|
|
1305
|
+
program
|
|
1306
|
+
.command('review')
|
|
1307
|
+
.description('生成 AI 学习日报')
|
|
1308
|
+
.option('--date <YYYY-MM-DD>', '日期')
|
|
1309
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
1310
|
+
.action(async (opts) => {
|
|
1311
|
+
const { execFile } = await import('child_process')
|
|
1312
|
+
const { promisify } = await import('util')
|
|
1313
|
+
const execFileAsync = promisify(execFile)
|
|
1314
|
+
const { fileURLToPath } = await import('url')
|
|
1315
|
+
const { dirname } = await import('path')
|
|
1316
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1317
|
+
const __dirname = dirname(__filename)
|
|
1318
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1319
|
+
const script = join(pkgRoot, 'scripts', 'generate_review.py')
|
|
1320
|
+
const args: string[] = [script]
|
|
1321
|
+
if (opts.date) args.push('--date', opts.date)
|
|
1322
|
+
if (opts.apiKey) args.push('--api-key', opts.apiKey)
|
|
1323
|
+
try {
|
|
1324
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
1325
|
+
timeout: 120_000, maxBuffer: 10 * 1024 * 1024,
|
|
1326
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1327
|
+
})
|
|
1328
|
+
console.log(stdout)
|
|
1329
|
+
} catch (e: any) {
|
|
1330
|
+
console.error(chalk.red(`\n✗ 失败: ${e.message}`))
|
|
1331
|
+
process.exit(1)
|
|
1332
|
+
}
|
|
1333
|
+
})
|
|
1334
|
+
|
|
1335
|
+
// fav-server
|
|
1336
|
+
program
|
|
1337
|
+
.command('fav-server')
|
|
1338
|
+
.description('启动收藏服务器 — 浏览器访问 localhost 实时同步收藏夹')
|
|
1339
|
+
.option('--date <YYYY-MM-DD>', '日期(默认今天)')
|
|
1340
|
+
.option('--port <number>', '端口', '8765')
|
|
1341
|
+
.option('--open', '自动打开浏览器')
|
|
1342
|
+
.action(async (opts) => {
|
|
1343
|
+
const { spawn } = await import('child_process')
|
|
1344
|
+
const { fileURLToPath } = await import('url')
|
|
1345
|
+
const { dirname } = await import('path')
|
|
1346
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1347
|
+
const __dirname = dirname(__filename)
|
|
1348
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
1349
|
+
const script = join(pkgRoot, 'scripts', 'fav_server.py')
|
|
1350
|
+
|
|
1351
|
+
if (!opts.date) {
|
|
1352
|
+
const now = new Date()
|
|
1353
|
+
const yyyy = now.getFullYear()
|
|
1354
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0')
|
|
1355
|
+
const dd = String(now.getDate()).padStart(2, '0')
|
|
1356
|
+
opts.date = `${yyyy}-${mm}-${dd}`
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const args = [script, '--date', opts.date, '--port', opts.port]
|
|
1360
|
+
console.log(chalk.cyan(`⭐ 启动收藏服务器 (${opts.date})\n`))
|
|
1361
|
+
console.log(chalk.gray(` 按住 Ctrl 并点击: http://localhost:${opts.port}`))
|
|
1362
|
+
|
|
1363
|
+
if (opts.open) {
|
|
1364
|
+
const { exec } = await import('child_process')
|
|
1365
|
+
exec(`start http://localhost:${opts.port}`)
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const child = spawn('python', args, {
|
|
1369
|
+
stdio: 'inherit',
|
|
1370
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1371
|
+
})
|
|
1372
|
+
await new Promise<void>((resolve) => child.on('exit', (code) => {
|
|
1373
|
+
if (code !== 0 && code !== null) process.exit(code)
|
|
1374
|
+
resolve()
|
|
1375
|
+
}))
|
|
1376
|
+
})
|
|
1377
|
+
|
|
1378
|
+
// ==================== weread ====================
|
|
1379
|
+
const wereadCmd = program
|
|
1380
|
+
.command('weread')
|
|
1381
|
+
.description('微信读书助手 — 书架、统计、笔记、搜索、书评、推荐')
|
|
1382
|
+
|
|
1383
|
+
wereadCmd
|
|
1384
|
+
.command('shelf')
|
|
1385
|
+
.description('查看书架')
|
|
1386
|
+
.option('--json', 'JSON 输出')
|
|
1387
|
+
.action(async (opts) => {
|
|
1388
|
+
const weread = await getWereadService()
|
|
1389
|
+
const res = await weread.shelf()
|
|
1390
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1391
|
+
const d = res.data!
|
|
1392
|
+
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
1393
|
+
|
|
1394
|
+
const total = d.books.length + d.albums.length + (d.mp ? 1 : 0)
|
|
1395
|
+
console.log(chalk.cyan(`📚 书架 (${total} 条目):\n`))
|
|
1396
|
+
console.log(chalk.gray(` 电子书: ${d.books.length} 本 | 有声书: ${d.albums.length} 个 | 文章收藏: ${d.mp ? '有' : '无'}`))
|
|
1397
|
+
if (d.archive.length) {
|
|
1398
|
+
console.log(chalk.gray(` 书单: ${d.archive.map((a: any) => a.name).join(', ')}`))
|
|
1399
|
+
}
|
|
1400
|
+
console.log()
|
|
1401
|
+
const books = d.books.sort((a: any, b: any) => b.readUpdateTime - a.readUpdateTime)
|
|
1402
|
+
for (let i = 0; i < Math.min(books.length, 30); i++) {
|
|
1403
|
+
const b = books[i]
|
|
1404
|
+
const num = String(i + 1).padStart(3)
|
|
1405
|
+
const done = b.finishReading === 1 ? chalk.green('✓') : ' '
|
|
1406
|
+
console.log(`${num}. ${done} ${b.title} ${chalk.gray(b.author)}`)
|
|
1407
|
+
}
|
|
1408
|
+
if (d.books.length > 30) console.log(chalk.gray(` ... 共 ${d.books.length} 本`))
|
|
1409
|
+
})
|
|
1410
|
+
|
|
1411
|
+
wereadCmd
|
|
1412
|
+
.command('stats')
|
|
1413
|
+
.description('阅读统计')
|
|
1414
|
+
.option('--mode <mode>', 'weekly | monthly | annually | overall', 'monthly')
|
|
1415
|
+
.option('--json', 'JSON 输出')
|
|
1416
|
+
.action(async (opts) => {
|
|
1417
|
+
const weread = await getWereadService()
|
|
1418
|
+
const res = await weread.readData(opts.mode)
|
|
1419
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1420
|
+
const d = res.data!
|
|
1421
|
+
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
1422
|
+
|
|
1423
|
+
const h = (d.totalReadTime || 0) / 3600
|
|
1424
|
+
console.log(chalk.cyan(`📊 阅读统计 (${opts.mode}):\n`))
|
|
1425
|
+
console.log(` 总时长: ${chalk.white(h.toFixed(1) + ' 小时')}`)
|
|
1426
|
+
console.log(` 有效天数: ${chalk.white(String(d.readDays))}`)
|
|
1427
|
+
if (d.dayAverageReadTime) {
|
|
1428
|
+
console.log(` 日均: ${chalk.white(Math.round(d.dayAverageReadTime / 60) + ' 分钟')}`)
|
|
1429
|
+
}
|
|
1430
|
+
if (d.readStat?.length) {
|
|
1431
|
+
console.log(` ${d.readStat.map((s: any) => `${s.stat}: ${chalk.white(s.counts)}`).join(' | ')}`)
|
|
1432
|
+
}
|
|
1433
|
+
if (d.preferCategoryWord) console.log(` 偏好: ${chalk.white(d.preferCategoryWord)}`)
|
|
1434
|
+
if (d.preferTimeWord) console.log(` 时段: ${chalk.white(d.preferTimeWord)}`)
|
|
1435
|
+
|
|
1436
|
+
// 偏好分类
|
|
1437
|
+
if (d.preferCategory?.length) {
|
|
1438
|
+
console.log(chalk.cyan('\n 偏好分类:'))
|
|
1439
|
+
for (const c of d.preferCategory.slice(0, 5)) {
|
|
1440
|
+
const hh = (c.readingTime / 3600).toFixed(1)
|
|
1441
|
+
console.log(` ${c.categoryTitle}: ${chalk.white(hh + 'h')} (${c.readingCount}本)`)
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// Read longest
|
|
1446
|
+
if (d.readLongest?.length) {
|
|
1447
|
+
console.log(chalk.cyan('\n 读最久的书:'))
|
|
1448
|
+
for (const item of d.readLongest.slice(0, 5)) {
|
|
1449
|
+
const b = item.book || item.albumInfo || {}
|
|
1450
|
+
const hh = (item.readTime / 3600).toFixed(1)
|
|
1451
|
+
console.log(` ${b.title || '(未知)'}: ${chalk.white(hh + 'h')} ${chalk.gray(b.author || '')}`)
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
})
|
|
1455
|
+
|
|
1456
|
+
wereadCmd
|
|
1457
|
+
.command('notes')
|
|
1458
|
+
.description('笔记划线')
|
|
1459
|
+
.option('--book-id <id>', '指定书籍 ID')
|
|
1460
|
+
.option('-n, --limit <n>', '数量', '20')
|
|
1461
|
+
.option('--json', 'JSON 输出')
|
|
1462
|
+
.action(async (opts) => {
|
|
1463
|
+
const weread = await getWereadService()
|
|
1464
|
+
if (opts.bookId) {
|
|
1465
|
+
const res = await weread.bookmarks(opts.bookId, parseInt(opts.limit))
|
|
1466
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1467
|
+
const d = res.data!
|
|
1468
|
+
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
1469
|
+
console.log(chalk.cyan(`📝 划线笔记 (${d.updated?.length || 0} 条):\n`))
|
|
1470
|
+
for (let i = 0; i < Math.min((d.updated || []).length, parseInt(opts.limit)); i++) {
|
|
1471
|
+
const n = d.updated[i]
|
|
1472
|
+
const icon = n.type === 1 ? '💬' : '📌'
|
|
1473
|
+
console.log(`${chalk.gray(` [${i + 1}]`)} ${icon} ${n.markText?.slice(0, 60) || ''}`)
|
|
1474
|
+
if (n.content) console.log(`${chalk.cyan(' 想法:')} ${n.content.slice(0, 100)}`)
|
|
1475
|
+
console.log()
|
|
1476
|
+
}
|
|
1477
|
+
} else {
|
|
1478
|
+
const res = await weread.notebooks(50)
|
|
1479
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1480
|
+
const d = res.data!
|
|
1481
|
+
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
1482
|
+
console.log(chalk.cyan(`📝 笔记本 (${d.books?.length || 0} 本有笔记):\n`))
|
|
1483
|
+
for (let i = 0; i < Math.min((d.books || []).length, 30); i++) {
|
|
1484
|
+
const b: any = d.books[i]
|
|
1485
|
+
const bookInfo = b.book || b
|
|
1486
|
+
const num = String(i + 1).padStart(3)
|
|
1487
|
+
console.log(`${num}. ${bookInfo.title || '(未知)'} ${chalk.gray(`划线:${b.highlightCount || b.noteCount || 0} 想法:${b.reviewCount || 0} 书签:${b.bookmarkCount || 0}`)}`)
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
})
|
|
1491
|
+
|
|
1492
|
+
wereadCmd
|
|
1493
|
+
.command('search')
|
|
1494
|
+
.description('搜索书籍')
|
|
1495
|
+
.argument('<keyword>', '搜索关键词')
|
|
1496
|
+
.option('-n, --limit <n>', '数量', '10')
|
|
1497
|
+
.option('--json', 'JSON 输出')
|
|
1498
|
+
.action(async (keyword, opts) => {
|
|
1499
|
+
const weread = await getWereadService()
|
|
1500
|
+
const res = await weread.search(keyword, parseInt(opts.limit))
|
|
1501
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1502
|
+
const d = res.data!
|
|
1503
|
+
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
1504
|
+
console.log(chalk.cyan(`🔍 搜索: "${keyword}" (${d.books?.length || 0} 条):\n`))
|
|
1505
|
+
for (let i = 0; i < (d.books || []).length; i++) {
|
|
1506
|
+
const b = d.books[i]
|
|
1507
|
+
const num = String(i + 1).padStart(2)
|
|
1508
|
+
const star = b.rating ? ` ⭐${b.rating}` : ''
|
|
1509
|
+
console.log(` ${num}. ${b.title} ${chalk.gray(b.author)}${star}`)
|
|
1510
|
+
if (b.intro) console.log(` ${chalk.gray(b.intro.slice(0, 80))}`)
|
|
1511
|
+
}
|
|
1512
|
+
})
|
|
1513
|
+
|
|
1514
|
+
wereadCmd
|
|
1515
|
+
.command('book')
|
|
1516
|
+
.description('书籍详情')
|
|
1517
|
+
.argument('<bookId>', '书籍 ID')
|
|
1518
|
+
.option('--json', 'JSON 输出')
|
|
1519
|
+
.action(async (bookId, opts) => {
|
|
1520
|
+
const weread = await getWereadService()
|
|
1521
|
+
const [infoRes, progressRes] = await Promise.all([
|
|
1522
|
+
weread.bookInfo(bookId),
|
|
1523
|
+
weread.getProgress(bookId),
|
|
1524
|
+
])
|
|
1525
|
+
if (!infoRes.ok) { console.log(chalk.red(`✗ ${infoRes.error}`)); process.exit(1) }
|
|
1526
|
+
const b = infoRes.data!
|
|
1527
|
+
if (opts.json) { console.log(JSON.stringify(b, null, 2)); return }
|
|
1528
|
+
|
|
1529
|
+
console.log(chalk.cyan(`📖 ${b.title}`))
|
|
1530
|
+
if (b.author) console.log(` 作者: ${chalk.white(b.author)}`)
|
|
1531
|
+
if (b.rating) console.log(` 评分: ${chalk.white('⭐' + b.rating + ' (' + (b as any).ratingCount + '评)')}`)
|
|
1532
|
+
if (b.category) console.log(` 分类: ${chalk.gray(b.category)}`)
|
|
1533
|
+
if (b.wordCount) console.log(` 字数: ${chalk.gray(b.wordCount)}`)
|
|
1534
|
+
if (b.publisher) console.log(` 出版: ${chalk.gray(b.publisher)}`)
|
|
1535
|
+
if (b.intro) console.log(`\n ${b.intro.slice(0, 200)}`)
|
|
1536
|
+
if (progressRes.ok && progressRes.data) {
|
|
1537
|
+
console.log(`\n 📍 进度: ${chalk.white(progressRes.data.chapterTitle || '未开始')}`)
|
|
1538
|
+
}
|
|
1539
|
+
})
|
|
1540
|
+
|
|
1541
|
+
wereadCmd
|
|
1542
|
+
.command('review')
|
|
1543
|
+
.description('书籍点评')
|
|
1544
|
+
.argument('<bookId>', '书籍 ID')
|
|
1545
|
+
.option('-n, --limit <n>', '数量', '10')
|
|
1546
|
+
.option('--json', 'JSON 输出')
|
|
1547
|
+
.action(async (bookId, opts) => {
|
|
1548
|
+
const weread = await getWereadService()
|
|
1549
|
+
const res = await weread.reviews(bookId, parseInt(opts.limit))
|
|
1550
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1551
|
+
const d = res.data!
|
|
1552
|
+
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
1553
|
+
console.log(chalk.cyan(`💬 书评 (${d.reviews?.length || 0} 条):\n`))
|
|
1554
|
+
for (let i = 0; i < (d.reviews || []).length; i++) {
|
|
1555
|
+
const r = d.reviews[i]
|
|
1556
|
+
const num = String(i + 1).padStart(2)
|
|
1557
|
+
const star = r.rating ? ` ⭐${r.rating}` : ''
|
|
1558
|
+
console.log(` ${num}. ${r.user?.name || '匿名'}${star} ${chalk.gray(`👍${r.likeCount || 0}`)}`)
|
|
1559
|
+
if (r.content) console.log(` ${r.content.slice(0, 120)}`)
|
|
1560
|
+
console.log()
|
|
1561
|
+
}
|
|
1562
|
+
})
|
|
1563
|
+
|
|
1564
|
+
wereadCmd
|
|
1565
|
+
.command('discover')
|
|
1566
|
+
.description('推荐好书')
|
|
1567
|
+
.option('--book-id <id>', '基于某本书的相似推荐')
|
|
1568
|
+
.option('-n, --limit <n>', '数量', '10')
|
|
1569
|
+
.option('--json', 'JSON 输出')
|
|
1570
|
+
.action(async (opts) => {
|
|
1571
|
+
const weread = await getWereadService()
|
|
1572
|
+
const res = opts.bookId
|
|
1573
|
+
? await weread.similar(opts.bookId, parseInt(opts.limit))
|
|
1574
|
+
: await weread.recommend(parseInt(opts.limit))
|
|
1575
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1576
|
+
const d = res.data!
|
|
1577
|
+
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
1578
|
+
const label = opts.bookId ? '相似推荐' : '个性化推荐'
|
|
1579
|
+
console.log(chalk.cyan(`🎯 ${label}:\n`))
|
|
1580
|
+
for (let i = 0; i < (d.books || []).length; i++) {
|
|
1581
|
+
const b = d.books[i]
|
|
1582
|
+
const num = String(i + 1).padStart(2)
|
|
1583
|
+
const star = b.rating ? ` ⭐${b.rating}` : ''
|
|
1584
|
+
console.log(` ${num}. ${b.title} ${chalk.gray(b.author)}${star}`)
|
|
1585
|
+
if (b.intro) console.log(` ${chalk.gray(b.intro.slice(0, 80))}`)
|
|
1586
|
+
}
|
|
1587
|
+
})
|
|
1588
|
+
|
|
1589
|
+
wereadCmd
|
|
1590
|
+
.command('profile')
|
|
1591
|
+
.description('个人阅读概览')
|
|
1592
|
+
.option('--json', 'JSON 输出')
|
|
1593
|
+
.action(async (opts) => {
|
|
1594
|
+
const weread = await getWereadService()
|
|
1595
|
+
const res = await weread.profile()
|
|
1596
|
+
if (!res.ok) { console.log(chalk.red(`✗ ${res.error}`)); process.exit(1) }
|
|
1597
|
+
const p = res.data!
|
|
1598
|
+
if (opts.json) { console.log(JSON.stringify(p, null, 2)); return }
|
|
1599
|
+
|
|
1600
|
+
const totalH = (p.totalReadTime / 3600).toFixed(1)
|
|
1601
|
+
console.log(chalk.cyan('📖 个人阅读概览:\n'))
|
|
1602
|
+
console.log(` 总时长: ${chalk.white(totalH + ' 小时')}`)
|
|
1603
|
+
console.log(` 阅读天数: ${chalk.white(String(p.totalReadDays) + ' 天')}`)
|
|
1604
|
+
console.log(` 书架: ${chalk.white(String(p.totalBooks) + ' 本')}`)
|
|
1605
|
+
console.log(` 读完: ${chalk.white(String(p.totalFinished) + ' 本')}`)
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
// 辅助函数
|
|
1609
|
+
async function runPythonCmd(script: string, args: string[]) {
|
|
1610
|
+
const { execFile } = await import('child_process')
|
|
1611
|
+
const { promisify } = await import('util')
|
|
1612
|
+
const execFileAsync = promisify(execFile)
|
|
1613
|
+
try {
|
|
1614
|
+
const { stdout } = await execFileAsync('python', [script, ...args], {
|
|
1615
|
+
timeout: 120_000, maxBuffer: 5 * 1024 * 1024,
|
|
1616
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1617
|
+
})
|
|
1618
|
+
console.log(stdout)
|
|
1619
|
+
} catch (e: any) {
|
|
1620
|
+
console.error(chalk.red(`\n✗ 失败: ${e.message}`))
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
async function getWereadService() {
|
|
1625
|
+
const key = process.env.WEREAD_API_KEY || ''
|
|
1626
|
+
const mod = await import('../src/services/wereadService.js')
|
|
1627
|
+
return new mod.WereadService(key)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// semantic-search
|
|
1631
|
+
program
|
|
1632
|
+
.command('search')
|
|
1633
|
+
.description('语义搜索知识库(聊天记录 + 文章)')
|
|
1634
|
+
.argument('<query>', '搜索关键词')
|
|
1635
|
+
.option('--top-k <n>', '返回数量', '10')
|
|
1636
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
1637
|
+
.action(async (query, opts) => {
|
|
1638
|
+
const { execFile } = await import('child_process')
|
|
1639
|
+
const { promisify } = await import('util')
|
|
1640
|
+
const execFileAsync = promisify(execFile)
|
|
1641
|
+
const { fileURLToPath } = await import('url')
|
|
1642
|
+
const { dirname } = await import('path')
|
|
1643
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1644
|
+
const __dirname = dirname(__filename)
|
|
1645
|
+
const pkgRoot = join(__dirname, '..')
|
|
1646
|
+
const script = join(pkgRoot, 'scripts', 'semantic_search.py')
|
|
1647
|
+
const args: string[] = [script, 'search', query, '--top-k', opts.topK]
|
|
1648
|
+
if (opts.apiKey) args.push('--api-key', opts.apiKey)
|
|
1649
|
+
try {
|
|
1650
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
1651
|
+
timeout: 60_000, maxBuffer: 10 * 1024 * 1024,
|
|
1652
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1653
|
+
})
|
|
1654
|
+
console.log(stdout)
|
|
1655
|
+
} catch (e: any) {
|
|
1656
|
+
console.error(chalk.red(`\n✗ 搜索失败: ${e.message}`))
|
|
1657
|
+
process.exit(1)
|
|
1658
|
+
}
|
|
1659
|
+
})
|
|
1660
|
+
|
|
1661
|
+
program
|
|
1662
|
+
.command('search-index')
|
|
1663
|
+
.description('构建语义搜索索引')
|
|
1664
|
+
.option('--full', '全量重建')
|
|
1665
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
1666
|
+
.action(async (opts) => {
|
|
1667
|
+
const { execFile } = await import('child_process')
|
|
1668
|
+
const { promisify } = await import('util')
|
|
1669
|
+
const execFileAsync = promisify(execFile)
|
|
1670
|
+
const { fileURLToPath } = await import('url')
|
|
1671
|
+
const { dirname } = await import('path')
|
|
1672
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1673
|
+
const __dirname = dirname(__filename)
|
|
1674
|
+
const pkgRoot = join(__dirname, '..')
|
|
1675
|
+
const script = join(pkgRoot, 'scripts', 'semantic_search.py')
|
|
1676
|
+
const args: string[] = [script, 'build']
|
|
1677
|
+
if (opts.full) args.push('--full')
|
|
1678
|
+
if (opts.apiKey) args.push('--api-key', opts.apiKey)
|
|
1679
|
+
try {
|
|
1680
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
1681
|
+
timeout: 300_000, maxBuffer: 10 * 1024 * 1024,
|
|
1682
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1683
|
+
})
|
|
1684
|
+
console.log(stdout)
|
|
1685
|
+
} catch (e: any) {
|
|
1686
|
+
console.error(chalk.red(`\n✗ 失败: ${e.message}`))
|
|
1687
|
+
process.exit(1)
|
|
1688
|
+
}
|
|
1689
|
+
})
|
|
1690
|
+
|
|
1691
|
+
// chat
|
|
1692
|
+
program
|
|
1693
|
+
.command('chat')
|
|
1694
|
+
.description('RAG 智能助手 — 基于聊天记录和公众号的对话式问答')
|
|
1695
|
+
.argument('[question]', '要问的问题(不传则进入交互模式)')
|
|
1696
|
+
.option('--top-k <n>', '检索数量', '10')
|
|
1697
|
+
.option('--talker <name>', '限定联系人/群聊')
|
|
1698
|
+
.option('--api-key <key>', 'AI API key')
|
|
1699
|
+
.option('--json', 'JSON 输出')
|
|
1700
|
+
.action(async (question, opts) => {
|
|
1701
|
+
const { execFile, spawn } = await import('child_process')
|
|
1702
|
+
const { promisify } = await import('util')
|
|
1703
|
+
const execFileAsync = promisify(execFile)
|
|
1704
|
+
const { fileURLToPath } = await import('url')
|
|
1705
|
+
const { dirname } = await import('path')
|
|
1706
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1707
|
+
const __dirname = dirname(__filename)
|
|
1708
|
+
const pkgRoot = join(__dirname, '..')
|
|
1709
|
+
const script = join(pkgRoot, 'scripts', 'rag_chat.py')
|
|
1710
|
+
const args: string[] = [script]
|
|
1711
|
+
if (question) {
|
|
1712
|
+
args.push(question, '--top-k', opts.topK)
|
|
1713
|
+
if (opts.talker) args.push('--talker', opts.talker)
|
|
1714
|
+
if (opts.apiKey) args.push('--api-key', opts.apiKey)
|
|
1715
|
+
if (opts.json) args.push('--json')
|
|
1716
|
+
try {
|
|
1717
|
+
const { stdout } = await execFileAsync('python', args, {
|
|
1718
|
+
timeout: 120_000, maxBuffer: 10 * 1024 * 1024,
|
|
1719
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1720
|
+
})
|
|
1721
|
+
console.log(stdout)
|
|
1722
|
+
} catch (e: any) {
|
|
1723
|
+
console.error(chalk.red(`\n✗ 对话失败: ${e.message}`))
|
|
1724
|
+
process.exit(1)
|
|
1725
|
+
}
|
|
1726
|
+
} else {
|
|
1727
|
+
// 交互模式:使用 spawn 保持终端交互
|
|
1728
|
+
args.push('--interactive', '--top-k', opts.topK)
|
|
1729
|
+
if (opts.talker) args.push('--talker', opts.talker)
|
|
1730
|
+
if (opts.apiKey) args.push('--api-key', opts.apiKey)
|
|
1731
|
+
const child = spawn('python', args, {
|
|
1732
|
+
stdio: 'inherit',
|
|
1733
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1734
|
+
})
|
|
1735
|
+
await new Promise<void>((resolve) => child.on('exit', (code) => {
|
|
1736
|
+
if (code !== 0) process.exit(code || 1);
|
|
1737
|
+
resolve();
|
|
1738
|
+
}))
|
|
1739
|
+
}
|
|
1740
|
+
})
|
|
1741
|
+
|
|
1742
|
+
// Helper: run a Python script
|
|
1743
|
+
async function runPython(script: string, args: string[]): Promise<void> {
|
|
1744
|
+
const { execFile } = await import('child_process')
|
|
1745
|
+
const { promisify } = await import('util')
|
|
1746
|
+
const execFileAsync = promisify(execFile)
|
|
1747
|
+
const { fileURLToPath } = await import('url')
|
|
1748
|
+
const { dirname } = await import('path')
|
|
1749
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1750
|
+
const __dirname = dirname(__filename)
|
|
1751
|
+
const pkgRoot = join(__dirname, '..')
|
|
1752
|
+
const pyArgs = [join(pkgRoot, script), ...args]
|
|
1753
|
+
try {
|
|
1754
|
+
const { stdout } = await execFileAsync('python', pyArgs, {
|
|
1755
|
+
timeout: 120_000, maxBuffer: 5 * 1024 * 1024,
|
|
1756
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1757
|
+
})
|
|
1758
|
+
console.log(stdout)
|
|
1759
|
+
} catch (e: any) {
|
|
1760
|
+
console.error(chalk.red(`\n✗ 失败: ${e.message}`))
|
|
1761
|
+
process.exit(1)
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// annual-report
|
|
1766
|
+
program
|
|
1767
|
+
.command('annual-report')
|
|
1768
|
+
.description('生成年度数字生活报告')
|
|
1769
|
+
.argument('[year]', '年份(默认今年)')
|
|
1770
|
+
.option('--share', '分享模式(紧凑卡片)')
|
|
1771
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
1772
|
+
.option('--skip-ai', '跳过 AI 总结')
|
|
1773
|
+
.action(async (year, opts) => {
|
|
1774
|
+
const a = [year || String(new Date().getFullYear())]
|
|
1775
|
+
if (opts.share) a.push('--share')
|
|
1776
|
+
if (opts.apiKey) a.push('--api-key', opts.apiKey)
|
|
1777
|
+
if (opts.skipAi) a.push('--skip-ai')
|
|
1778
|
+
await runPython('scripts/annual_report.py', a)
|
|
1779
|
+
})
|
|
1780
|
+
|
|
1781
|
+
// todos
|
|
1782
|
+
const todosCmd = program
|
|
1783
|
+
.command('todos')
|
|
1784
|
+
.description('待办提取与任务追踪')
|
|
1785
|
+
|
|
1786
|
+
todosCmd
|
|
1787
|
+
.command('extract')
|
|
1788
|
+
.description('AI 扫描聊天记录提取待办')
|
|
1789
|
+
.option('--days <n>', '扫描天数', '7')
|
|
1790
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
1791
|
+
.action(async (opts) => {
|
|
1792
|
+
await runPython('scripts/extract_todos.py', ['extract', '--days', opts.days, ...(opts.apiKey ? ['--api-key', opts.apiKey] : [])])
|
|
1793
|
+
})
|
|
1794
|
+
|
|
1795
|
+
todosCmd
|
|
1796
|
+
.command('list')
|
|
1797
|
+
.description('列出待办')
|
|
1798
|
+
.option('--status <s>', 'pending / done')
|
|
1799
|
+
.option('--urgency <u>', '高 / 中 / 低')
|
|
1800
|
+
.option('--json', 'JSON 输出')
|
|
1801
|
+
.action(async (opts) => {
|
|
1802
|
+
const a = ['list']
|
|
1803
|
+
if (opts.status) a.push('--status', opts.status)
|
|
1804
|
+
if (opts.urgency) a.push('--urgency', opts.urgency)
|
|
1805
|
+
if (opts.json) a.push('--json')
|
|
1806
|
+
await runPython('scripts/extract_todos.py', a)
|
|
1807
|
+
})
|
|
1808
|
+
|
|
1809
|
+
todosCmd
|
|
1810
|
+
.command('done')
|
|
1811
|
+
.description('标记待办为已完成')
|
|
1812
|
+
.argument('<id>', '待办 ID')
|
|
1813
|
+
.action(async (id) => {
|
|
1814
|
+
await runPython('scripts/extract_todos.py', ['done', id])
|
|
1815
|
+
})
|
|
1816
|
+
|
|
1817
|
+
todosCmd
|
|
1818
|
+
.command('undone')
|
|
1819
|
+
.description('取消已完成标记')
|
|
1820
|
+
.argument('<id>', '待办 ID')
|
|
1821
|
+
.action(async (id) => {
|
|
1822
|
+
await runPython('scripts/extract_todos.py', ['undone', id])
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
todosCmd
|
|
1826
|
+
.command('rm')
|
|
1827
|
+
.description('删除待办')
|
|
1828
|
+
.argument('<id>', '待办 ID')
|
|
1829
|
+
.action(async (id) => {
|
|
1830
|
+
await runPython('scripts/extract_todos.py', ['rm', id])
|
|
1831
|
+
})
|
|
1832
|
+
|
|
1833
|
+
todosCmd
|
|
1834
|
+
.command('remind')
|
|
1835
|
+
.description('查看待办提醒')
|
|
1836
|
+
.action(async () => {
|
|
1837
|
+
await runPython('scripts/extract_todos.py', ['remind'])
|
|
1838
|
+
})
|
|
1839
|
+
|
|
1840
|
+
// ==================== daily ====================
|
|
1841
|
+
program
|
|
1842
|
+
.command('daily')
|
|
1843
|
+
.description('一键生成公众号日报(抓取 + AI 摘要 + HTML 阅读器 + 行动建议)')
|
|
1844
|
+
.option('-d, --date <YYYY-MM-DD>', '日期')
|
|
1845
|
+
.option('--api-key <key>', 'DeepSeek API key(或设环境变量 DEEPSEEK_API_KEY)')
|
|
1846
|
+
.option('--skip-classify', '跳过后处理')
|
|
1847
|
+
.action(async (opts) => {
|
|
1848
|
+
const { execFile, spawn } = await import('child_process')
|
|
1849
|
+
const { promisify } = await import('util')
|
|
1850
|
+
const execFileAsync = promisify(execFile)
|
|
1851
|
+
const { fileURLToPath } = await import('url')
|
|
1852
|
+
const { dirname } = await import('path')
|
|
1853
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1854
|
+
const __dirname = dirname(__filename)
|
|
1855
|
+
const pkgRoot = join(__dirname, '..')
|
|
1856
|
+
const pipeline = join(pkgRoot, 'scripts', 'pipeline.py')
|
|
1857
|
+
|
|
1858
|
+
const date = opts.date || new Date().toISOString().slice(0, 10)
|
|
1859
|
+
const apiKey = opts.apiKey || process.env.DEEPSEEK_API_KEY || ''
|
|
1860
|
+
if (!apiKey) {
|
|
1861
|
+
console.log(chalk.red('\n❌ 缺少 DeepSeek API key'))
|
|
1862
|
+
console.log(chalk.gray(' 用法: weflow-cli daily --api-key <key>'))
|
|
1863
|
+
console.log(chalk.gray(' 或设环境变量: set DEEPSEEK_API_KEY=<key>\n'))
|
|
1864
|
+
process.exit(1)
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
console.log(chalk.cyan(`\n📰 正在生成 ${date} 公众号日报...\n`))
|
|
1868
|
+
const args = [pipeline, '--date', date, '--api-key', apiKey, '--interest', 'AI', '--skip-wiki']
|
|
1869
|
+
if (opts.skipClassify) args.push('--skip-classify')
|
|
1870
|
+
|
|
1871
|
+
const child = spawn('python', args, {
|
|
1872
|
+
stdio: 'inherit',
|
|
1873
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1874
|
+
})
|
|
1875
|
+
child.on('exit', (code) => {
|
|
1876
|
+
if (code === 0) {
|
|
1877
|
+
console.log(chalk.green(`\n✓ 日报已生成: output/biz-daily/${date}/`))
|
|
1878
|
+
console.log(chalk.gray(` HTML 阅读器: weflow-cli daily-server --date ${date}`))
|
|
1879
|
+
} else {
|
|
1880
|
+
process.exit(code || 1)
|
|
1881
|
+
}
|
|
1882
|
+
})
|
|
1883
|
+
})
|
|
1884
|
+
|
|
1885
|
+
// ==================== daily-server ====================
|
|
1886
|
+
program
|
|
1887
|
+
.command('daily-server')
|
|
1888
|
+
.description('启动本地日报阅读器(浏览器浏览 + 收藏 + 笔记)')
|
|
1889
|
+
.option('-d, --date <YYYY-MM-DD>', '日期')
|
|
1890
|
+
.option('-p, --port <n>', '端口', '8765')
|
|
1891
|
+
.option('--open', '自动打开浏览器')
|
|
1892
|
+
.action(async (opts) => {
|
|
1893
|
+
const { spawn, exec } = await import('child_process')
|
|
1894
|
+
const { fileURLToPath } = await import('url')
|
|
1895
|
+
const { dirname } = await import('path')
|
|
1896
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
1897
|
+
const __dirname = dirname(__filename)
|
|
1898
|
+
const pkgRoot = join(__dirname, '..')
|
|
1899
|
+
const favServer = join(pkgRoot, 'scripts', 'fav_server.py')
|
|
1900
|
+
|
|
1901
|
+
const date = opts.date || new Date().toISOString().slice(0, 10)
|
|
1902
|
+
console.log(chalk.cyan(`⭐ 启动日报阅读器\n`))
|
|
1903
|
+
console.log(chalk.gray(` 日期: ${date}`))
|
|
1904
|
+
console.log(chalk.gray(` 地址: http://localhost:${opts.port}`))
|
|
1905
|
+
console.log()
|
|
1906
|
+
|
|
1907
|
+
if (opts.open) {
|
|
1908
|
+
exec(`start http://localhost:${opts.port}`)
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const child = spawn('python', [favServer, '--date', date, '--port', String(opts.port)], {
|
|
1912
|
+
stdio: 'inherit',
|
|
1913
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
1914
|
+
})
|
|
1915
|
+
process.on('SIGINT', () => child.kill())
|
|
1916
|
+
})
|
|
1917
|
+
|
|
1918
|
+
// ==================== Interactive Menu (no arguments) ====================
|
|
1919
|
+
program
|
|
1920
|
+
.command('mcp-config')
|
|
1921
|
+
.description('输出 MCP Server 配置,粘贴到 .mcp.json 即可让 AI 操作本工具')
|
|
1922
|
+
.option('--port <n>', 'MCP Server 端口', '0')
|
|
1923
|
+
.option('-o, --output <file>', '写入文件', '')
|
|
1924
|
+
.action(async (opts) => {
|
|
1925
|
+
const { writeFileSync } = await import('fs')
|
|
1926
|
+
const config = {
|
|
1927
|
+
mcpServers: {
|
|
1928
|
+
weflow: {
|
|
1929
|
+
command: 'npx',
|
|
1930
|
+
args: ['tsx', 'mcp-server/index.ts'],
|
|
1931
|
+
cwd: '${workspaceFolder}',
|
|
1932
|
+
},
|
|
1933
|
+
},
|
|
1934
|
+
}
|
|
1935
|
+
const json = JSON.stringify(config, null, 2)
|
|
1936
|
+
if (opts.output) {
|
|
1937
|
+
writeFileSync(opts.output, json, 'utf-8')
|
|
1938
|
+
console.log(chalk.green(`✓ 已写入 ${opts.output}`))
|
|
1939
|
+
console.log(chalk.gray('AI 助手现在可以使用以下工具:'))
|
|
1940
|
+
} else {
|
|
1941
|
+
console.log(json)
|
|
1942
|
+
}
|
|
1943
|
+
console.log(chalk.cyan('\n可用的 MCP 工具:'))
|
|
1944
|
+
console.log(chalk.white(' wechat.search_articles ') + chalk.gray('搜索知识库文章'))
|
|
1945
|
+
console.log(chalk.white(' wechat.get_daily ') + chalk.gray('获取公众号日报'))
|
|
1946
|
+
console.log(chalk.white(' wechat.get_review ') + chalk.gray('获取 AI 学习日报'))
|
|
1947
|
+
console.log(chalk.white(' wechat.get_stats ') + chalk.gray('知识库统计'))
|
|
1948
|
+
console.log(chalk.white(' wechat.get_concepts ') + chalk.gray('概念图谱'))
|
|
1949
|
+
console.log()
|
|
1950
|
+
if (opts.output) {
|
|
1951
|
+
console.log(chalk.gray(`配置已写入 ${opts.output},重启 AI 编辑器即可生效`))
|
|
1952
|
+
} else {
|
|
1953
|
+
console.log(chalk.gray('复制以上 JSON 到你的 .mcp.json 文件即可'))
|
|
1954
|
+
}
|
|
1955
|
+
})
|
|
1956
|
+
|
|
1957
|
+
// ==================== check ====================
|
|
1958
|
+
program
|
|
1959
|
+
.command('check')
|
|
1960
|
+
.description('检查运行环境(Python、pip 依赖、数据库配置)')
|
|
1961
|
+
.action(async () => {
|
|
1962
|
+
console.log(chalk.cyan('🔍 WeFlow CLI 环境检查\n'))
|
|
1963
|
+
|
|
1964
|
+
// 1. Node.js
|
|
1965
|
+
console.log(chalk.white('Node.js:'), chalk.green(`v${process.versions.node}`))
|
|
1966
|
+
|
|
1967
|
+
// 2. Python
|
|
1968
|
+
const { execSync } = await import('child_process')
|
|
1969
|
+
let pythonOk = false
|
|
1970
|
+
try {
|
|
1971
|
+
const pyVer = execSync('python --version 2>&1 || python3 --version 2>&1', { encoding: 'utf-8', timeout: 5000 }).trim()
|
|
1972
|
+
console.log(chalk.white('Python: '), chalk.green(pyVer))
|
|
1973
|
+
pythonOk = true
|
|
1974
|
+
} catch {
|
|
1975
|
+
console.log(chalk.white('Python: '), chalk.red('✗ 未找到'))
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// 3. Pip dependencies
|
|
1979
|
+
if (pythonOk) {
|
|
1980
|
+
const deps = ['sqlcipher3', 'html2text', 'zstandard', 'cryptography']
|
|
1981
|
+
console.log(chalk.white('\nPython 依赖:'))
|
|
1982
|
+
let allOk = true
|
|
1983
|
+
for (const dep of deps) {
|
|
1984
|
+
try {
|
|
1985
|
+
execSync(`python -c "import ${dep}"`, { timeout: 5000 })
|
|
1986
|
+
console.log(` ${dep.padEnd(20)} ${chalk.green('✓')}`)
|
|
1987
|
+
} catch {
|
|
1988
|
+
console.log(` ${dep.padEnd(20)} ${chalk.red('✗ 缺失')}`)
|
|
1989
|
+
allOk = false
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (!allOk) {
|
|
1993
|
+
console.log(chalk.yellow('\n⚠️ 运行以下命令安装缺失依赖:'))
|
|
1994
|
+
console.log(chalk.gray(' pip install sqlcipher3 html2text zstandard cryptography'))
|
|
1995
|
+
}
|
|
1996
|
+
// 可选依赖
|
|
1997
|
+
const optDeps = ['scrapling']
|
|
1998
|
+
console.log(chalk.white('\n可选依赖(提升抓取成功率):'))
|
|
1999
|
+
for (const dep of optDeps) {
|
|
2000
|
+
try {
|
|
2001
|
+
execSync(`python -c "import ${dep}"`, { timeout: 5000 })
|
|
2002
|
+
console.log(` ${dep.padEnd(20)} ${chalk.green('✓')}`)
|
|
2003
|
+
} catch {
|
|
2004
|
+
console.log(` ${dep.padEnd(20)} ${chalk.gray('○ (npm install scrapling)')}`)
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// 4. Config
|
|
2010
|
+
console.log(chalk.white('\n配置状态:'))
|
|
2011
|
+
const configured = configService.isConfigured()
|
|
2012
|
+
const hasKey = !!configService.get('ntKey') || !!configService.get('decryptKey')
|
|
2013
|
+
if (configured && hasKey) {
|
|
2014
|
+
console.log(chalk.green(' ✓ 已初始化 — 可以运行 weflow-cli'))
|
|
2015
|
+
} else {
|
|
2016
|
+
console.log(chalk.yellow(' ○ 未初始化 — 运行 weflow-cli init'))
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// 5. Database
|
|
2020
|
+
const ntDb = configService.get('ntDbPath')
|
|
2021
|
+
if (ntDb && existsSync(ntDb)) {
|
|
2022
|
+
const stat = (await import('fs')).statSync(ntDb)
|
|
2023
|
+
const mb = (stat.size / 1024 / 1024).toFixed(0)
|
|
2024
|
+
console.log(chalk.white(` 消息数据库: ${ntDb} (${mb}MB)`))
|
|
2025
|
+
}
|
|
2026
|
+
const bizDb = (configService as any).get?.('bizKey')
|
|
2027
|
+
if (bizDb) {
|
|
2028
|
+
console.log(chalk.green(' ✓ 公众号数据库密钥已配置'))
|
|
2029
|
+
}
|
|
2030
|
+
console.log()
|
|
2031
|
+
})
|
|
2032
|
+
|
|
2033
|
+
// ==================== Interactive Menu (no arguments) ====================
|
|
2034
|
+
async function showInteractiveMenu() {
|
|
2035
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2036
|
+
const configured = configService.isConfigured()
|
|
2037
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2038
|
+
const hasNtKey = !!configService.get('ntKey')
|
|
2039
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2040
|
+
const hasLogin = !!configService.get('wechatOcToken')
|
|
2041
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2042
|
+
const hasVault = !!configService.get('vaultRepo')
|
|
2043
|
+
|
|
2044
|
+
// 首次运行:自动引导初始化
|
|
2045
|
+
if (!configured) {
|
|
2046
|
+
console.log(chalk.cyan('\n👋 欢迎使用 WeFlow CLI!'))
|
|
2047
|
+
console.log(chalk.gray('检测到尚未初始化,让我们开始设置...\n'))
|
|
2048
|
+
|
|
2049
|
+
const { startInit } = await inquirer.prompt([{
|
|
2050
|
+
type: 'confirm',
|
|
2051
|
+
name: 'startInit',
|
|
2052
|
+
message: '是否现在开始初始化(检测微信数据目录并提取密钥)?',
|
|
2053
|
+
default: true,
|
|
2054
|
+
}])
|
|
2055
|
+
if (startInit) {
|
|
2056
|
+
const initCmd = program.commands.find(c => c.name() === 'init')
|
|
2057
|
+
if (initCmd) { await (initCmd as any).action(); return }
|
|
2058
|
+
}
|
|
2059
|
+
console.log(chalk.gray('\n稍后可运行: weflow-cli init\n'))
|
|
2060
|
+
return
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// 构建菜单选项
|
|
2064
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2065
|
+
const choices: any[] = []
|
|
2066
|
+
|
|
2067
|
+
choices.push(new inquirer.Separator(' 📬 聊天记录'))
|
|
2068
|
+
choices.push({ name: ' 查看会话列表', value: 'sessions' })
|
|
2069
|
+
choices.push({ name: ' 查看聊天消息', value: 'messages' })
|
|
2070
|
+
choices.push({ name: ' 导出聊天记录', value: 'export' })
|
|
2071
|
+
choices.push({ name: ' 生成聊天月报', value: 'report' })
|
|
2072
|
+
|
|
2073
|
+
choices.push(new inquirer.Separator(' 📰 公众号日报'))
|
|
2074
|
+
choices.push({ name: ' 生成今日日报', value: 'daily' })
|
|
2075
|
+
choices.push({ name: ' 启动阅读器', value: 'daily-server' })
|
|
2076
|
+
|
|
2077
|
+
choices.push(new inquirer.Separator(' 💬 微信消息'))
|
|
2078
|
+
choices.push({
|
|
2079
|
+
name: ` 扫码登录${hasLogin ? chalk.green(' (已登录)') : ''}`,
|
|
2080
|
+
value: 'login-wechat',
|
|
2081
|
+
})
|
|
2082
|
+
choices.push({
|
|
2083
|
+
name: ` 监听消息${!hasLogin ? chalk.gray(' (需先登录)') : ''}`,
|
|
2084
|
+
value: 'listen',
|
|
2085
|
+
disabled: !hasLogin ? '请先扫码登录' : undefined,
|
|
2086
|
+
})
|
|
2087
|
+
choices.push({
|
|
2088
|
+
name: ` 发送消息${!hasLogin ? chalk.gray(' (需先登录)') : ''}`,
|
|
2089
|
+
value: 'send',
|
|
2090
|
+
disabled: !hasLogin ? '请先扫码登录' : undefined,
|
|
2091
|
+
})
|
|
2092
|
+
|
|
2093
|
+
choices.push(new inquirer.Separator(' 📚 知识库'))
|
|
2094
|
+
choices.push({ name: ' 初始化 Obsidian Vault', value: 'vault-init' })
|
|
2095
|
+
choices.push({ name: ' 编译概念图谱', value: 'wiki-compile' })
|
|
2096
|
+
choices.push({
|
|
2097
|
+
name: ` 同步到远端${hasVault ? '' : chalk.gray(' (未配置仓库)')}`,
|
|
2098
|
+
value: 'vault-sync',
|
|
2099
|
+
disabled: !hasVault ? '请先配置 vaultRepo' : undefined,
|
|
2100
|
+
})
|
|
2101
|
+
|
|
2102
|
+
choices.push(new inquirer.Separator(' 📊 统计 & 报告'))
|
|
2103
|
+
choices.push({ name: ' 📊 个人信息消费报告', value: 'chat-stats' })
|
|
2104
|
+
choices.push({ name: ' 📚 微信读书统计', value: 'weread' })
|
|
2105
|
+
choices.push({ name: ' 🧠 提取待办事项', value: 'extract-todos' })
|
|
2106
|
+
choices.push({ name: ' 📝 AI 学习日报', value: 'generate-review' })
|
|
2107
|
+
|
|
2108
|
+
choices.push(new inquirer.Separator(' ⚙️ 系统'))
|
|
2109
|
+
choices.push({ name: ' 查看配置', value: 'config-show' })
|
|
2110
|
+
choices.push({ name: ' 重新初始化', value: 'init' })
|
|
2111
|
+
|
|
2112
|
+
// 显示状态摘要
|
|
2113
|
+
console.log(chalk.cyan('\n🚀 WeFlow CLI v1.5.0\n'))
|
|
2114
|
+
const statusParts: string[] = []
|
|
2115
|
+
statusParts.push(hasNtKey ? chalk.green('✓ 数据库') : chalk.yellow('○ 数据库'))
|
|
2116
|
+
statusParts.push(hasLogin ? chalk.green('✓ 消息通道') : chalk.gray('○ 消息通道'))
|
|
2117
|
+
statusParts.push(hasVault ? chalk.green('✓ Vault') : chalk.gray('○ Vault'))
|
|
2118
|
+
console.log(chalk.gray(` 状态: ${statusParts.join(' ')}\n`))
|
|
2119
|
+
|
|
2120
|
+
const { action } = await inquirer.prompt([{
|
|
2121
|
+
type: 'select' as any,
|
|
2122
|
+
name: 'action',
|
|
2123
|
+
message: '请选择操作',
|
|
2124
|
+
choices,
|
|
2125
|
+
loop: false,
|
|
2126
|
+
}])
|
|
2127
|
+
|
|
2128
|
+
// 辅助函数:执行命令
|
|
2129
|
+
const runCmd = async (name: string, opts: Record<string, any> = {}) => {
|
|
2130
|
+
const cmd = program.commands.find(c => c.name() === name)
|
|
2131
|
+
if (cmd) await (cmd as any).action(opts)
|
|
2132
|
+
}
|
|
2133
|
+
const runSubCmd = async (parent: string, child: string, opts: Record<string, any> = {}) => {
|
|
2134
|
+
const cmd = program.commands.find(c => c.name() === parent) as any
|
|
2135
|
+
const sub = cmd?.commands?.find((c: any) => c.name() === child)
|
|
2136
|
+
if (sub) await sub.action(opts)
|
|
2137
|
+
}
|
|
2138
|
+
const runPython = async (scriptName: string, label: string) => {
|
|
2139
|
+
const { execFile } = await import('child_process')
|
|
2140
|
+
const { promisify } = await import('util')
|
|
2141
|
+
const execFileAsync = promisify(execFile)
|
|
2142
|
+
const { fileURLToPath } = await import('url')
|
|
2143
|
+
const { dirname } = await import('path')
|
|
2144
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
2145
|
+
const __dirname = dirname(__filename)
|
|
2146
|
+
const pkgRoot = join(__dirname, '..', '..')
|
|
2147
|
+
const script = join(pkgRoot, 'scripts', scriptName)
|
|
2148
|
+
console.log(chalk.cyan(`正在${label}...\n`))
|
|
2149
|
+
try {
|
|
2150
|
+
const { stdout } = await execFileAsync('python', [script], {
|
|
2151
|
+
timeout: 600_000, maxBuffer: 50 * 1024 * 1024,
|
|
2152
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
2153
|
+
})
|
|
2154
|
+
console.log(stdout)
|
|
2155
|
+
} catch (e: any) {
|
|
2156
|
+
console.error(chalk.red(`${label}失败: ${e.message}`))
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
switch (action) {
|
|
2161
|
+
case 'sessions':
|
|
2162
|
+
await runCmd('sessions')
|
|
2163
|
+
break
|
|
2164
|
+
case 'messages': {
|
|
2165
|
+
const sessions = await chatService.listSessions(undefined, 30)
|
|
2166
|
+
if (sessions.length === 0) { console.log(chalk.gray('无会话')); break }
|
|
2167
|
+
const { talker } = await inquirer.prompt([{
|
|
2168
|
+
type: 'select' as any, name: 'talker', message: '选择会话',
|
|
2169
|
+
choices: sessions.map((s, i) => ({
|
|
2170
|
+
name: `${String(i + 1).padStart(3)}. ${s.displayName || s.username} ${chalk.gray((s.summary || '').slice(0, 30))}`,
|
|
2171
|
+
value: s.username,
|
|
2172
|
+
})),
|
|
2173
|
+
loop: false,
|
|
2174
|
+
}])
|
|
2175
|
+
await runCmd('messages', { talker, limit: '50', offset: '0' })
|
|
2176
|
+
break
|
|
2177
|
+
}
|
|
2178
|
+
case 'export': {
|
|
2179
|
+
const sessions = await chatService.listSessions(undefined, 30)
|
|
2180
|
+
if (sessions.length === 0) { console.log(chalk.gray('无会话')); break }
|
|
2181
|
+
const { talker } = await inquirer.prompt([{
|
|
2182
|
+
type: 'select' as any, name: 'talker', message: '选择要导出的会话',
|
|
2183
|
+
choices: sessions.map((s, i) => ({
|
|
2184
|
+
name: `${String(i + 1).padStart(3)}. ${s.displayName || s.username}`,
|
|
2185
|
+
value: s.username,
|
|
2186
|
+
})),
|
|
2187
|
+
loop: false,
|
|
2188
|
+
}])
|
|
2189
|
+
const { format } = await inquirer.prompt([{
|
|
2190
|
+
type: 'select' as any, name: 'format', message: '选择导出格式',
|
|
2191
|
+
choices: ['html', 'json', 'txt', 'excel'], loop: false,
|
|
2192
|
+
}])
|
|
2193
|
+
await runCmd('export', { talker, format, output: './output' })
|
|
2194
|
+
break
|
|
2195
|
+
}
|
|
2196
|
+
case 'report':
|
|
2197
|
+
await runCmd('report', {})
|
|
2198
|
+
break
|
|
2199
|
+
case 'daily': {
|
|
2200
|
+
const apiKey = process.env.DEEPSEEK_API_KEY || ''
|
|
2201
|
+
if (!apiKey) {
|
|
2202
|
+
console.log(chalk.red('\n❌ 缺少 DeepSeek API key'))
|
|
2203
|
+
console.log(chalk.gray(' 设置环境变量: set DEEPSEEK_API_KEY=<key>'))
|
|
2204
|
+
break
|
|
2205
|
+
}
|
|
2206
|
+
const now = new Date()
|
|
2207
|
+
const yyyy = now.getFullYear()
|
|
2208
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0')
|
|
2209
|
+
const dd = String(now.getDate()).padStart(2, '0')
|
|
2210
|
+
const dateStr = `${yyyy}-${mm}-${dd}`
|
|
2211
|
+
const { spawn } = await import('child_process')
|
|
2212
|
+
const { fileURLToPath } = await import('url')
|
|
2213
|
+
const { dirname } = await import('path')
|
|
2214
|
+
const fn = fileURLToPath(import.meta.url)
|
|
2215
|
+
const pkgs = join(dirname(fn), '..')
|
|
2216
|
+
const pipeline = join(pkgs, 'scripts', 'pipeline.py')
|
|
2217
|
+
console.log(chalk.cyan(`\n📰 正在生成 ${dateStr} 公众号日报...\n`))
|
|
2218
|
+
const child = spawn('python', [pipeline, '--date', dateStr, '--api-key', apiKey, '--interest', 'AI', '--skip-wiki'], {
|
|
2219
|
+
stdio: 'inherit',
|
|
2220
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
2221
|
+
})
|
|
2222
|
+
await new Promise<void>((resolve) => child.on('exit', () => resolve()))
|
|
2223
|
+
break
|
|
2224
|
+
}
|
|
2225
|
+
case 'daily-server': {
|
|
2226
|
+
const now = new Date()
|
|
2227
|
+
const yyyy = now.getFullYear()
|
|
2228
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0')
|
|
2229
|
+
const dd = String(now.getDate()).padStart(2, '0')
|
|
2230
|
+
const dateStr = `${yyyy}-${mm}-${dd}`
|
|
2231
|
+
await runCmd('fav-server', { date: dateStr, port: '8765', open: true })
|
|
2232
|
+
break
|
|
2233
|
+
}
|
|
2234
|
+
case 'login-wechat':
|
|
2235
|
+
await runCmd('login-wechat', {})
|
|
2236
|
+
break
|
|
2237
|
+
case 'listen':
|
|
2238
|
+
await runCmd('listen', {})
|
|
2239
|
+
break
|
|
2240
|
+
case 'send': {
|
|
2241
|
+
const sessions = await chatService.listSessions(undefined, 30)
|
|
2242
|
+
if (sessions.length === 0) { console.log(chalk.gray('无会话')); break }
|
|
2243
|
+
const { talker } = await inquirer.prompt([{
|
|
2244
|
+
type: 'select' as any, name: 'talker', message: '选择接收人',
|
|
2245
|
+
choices: sessions.map((s, i) => ({
|
|
2246
|
+
name: `${String(i + 1).padStart(3)}. ${s.displayName || s.username}`,
|
|
2247
|
+
value: s.username,
|
|
2248
|
+
})),
|
|
2249
|
+
loop: false,
|
|
2250
|
+
}])
|
|
2251
|
+
const { message } = await inquirer.prompt([{
|
|
2252
|
+
type: 'input', name: 'message', message: '输入消息内容',
|
|
2253
|
+
}])
|
|
2254
|
+
if (!message.trim()) { console.log(chalk.gray('已取消')); break }
|
|
2255
|
+
await runCmd('send', { talker, message })
|
|
2256
|
+
break
|
|
2257
|
+
}
|
|
2258
|
+
case 'vault-init':
|
|
2259
|
+
await runSubCmd('vault', 'init', {})
|
|
2260
|
+
break
|
|
2261
|
+
case 'wiki-compile':
|
|
2262
|
+
await runSubCmd('wiki', 'compile', {})
|
|
2263
|
+
break
|
|
2264
|
+
case 'vault-sync':
|
|
2265
|
+
await runSubCmd('vault', 'sync', {})
|
|
2266
|
+
break
|
|
2267
|
+
case 'config-show':
|
|
2268
|
+
await runSubCmd('config', 'show')
|
|
2269
|
+
break
|
|
2270
|
+
case 'chat-stats':
|
|
2271
|
+
await runCmd('chat-stats', { period: 'week' })
|
|
2272
|
+
break
|
|
2273
|
+
case 'weread': {
|
|
2274
|
+
const wCmd = program.commands.find(c => c.name() === 'weread') as any
|
|
2275
|
+
const statsCmd = wCmd?.commands?.find((c: any) => c.name() === 'stats')
|
|
2276
|
+
if (statsCmd) await statsCmd.action({ mode: 'monthly' })
|
|
2277
|
+
break
|
|
2278
|
+
}
|
|
2279
|
+
case 'extract-todos': {
|
|
2280
|
+
const sessions = await chatService.listSessions(undefined, 30)
|
|
2281
|
+
if (sessions.length === 0) { console.log(chalk.gray('无会话')); break }
|
|
2282
|
+
const { talker } = await inquirer.prompt([{
|
|
2283
|
+
type: 'select' as any, name: 'talker', message: '选择联系人',
|
|
2284
|
+
choices: sessions.map((s, i) => ({
|
|
2285
|
+
name: `${String(i + 1).padStart(3)}. ${s.displayName || s.username}`,
|
|
2286
|
+
value: s.displayName || s.username,
|
|
2287
|
+
})),
|
|
2288
|
+
loop: false,
|
|
2289
|
+
}])
|
|
2290
|
+
await runCmd('todos', { talker, days: '7' })
|
|
2291
|
+
break
|
|
2292
|
+
}
|
|
2293
|
+
case 'generate-review':
|
|
2294
|
+
await runCmd('review', {})
|
|
2295
|
+
break
|
|
2296
|
+
case 'init':
|
|
2297
|
+
await runCmd('init')
|
|
2298
|
+
break
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// 检测是否无参数启动(显示交互式菜单)
|
|
2303
|
+
const cliArgs = process.argv.slice(2)
|
|
2304
|
+
if (cliArgs.length === 0) {
|
|
2305
|
+
showInteractiveMenu().catch((e) => {
|
|
2306
|
+
console.error(chalk.red(`\n错误: ${e.message}`))
|
|
2307
|
+
process.exit(1)
|
|
2308
|
+
})
|
|
2309
|
+
} else {
|
|
2310
|
+
program.parse()
|
|
2311
|
+
}
|