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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/bin/weflow-cli-electron.cjs +59 -0
  4. package/bin/weflow-cli.ts +2311 -0
  5. package/cli.cjs +3 -0
  6. package/dist/bin/weflow-cli.d.ts +3 -0
  7. package/dist/bin/weflow-cli.d.ts.map +1 -0
  8. package/dist/bin/weflow-cli.js +2270 -0
  9. package/dist/bin/weflow-cli.js.map +1 -0
  10. package/dist/mcp-server/index.d.ts +3 -0
  11. package/dist/mcp-server/index.d.ts.map +1 -0
  12. package/dist/mcp-server/index.js +690 -0
  13. package/dist/mcp-server/index.js.map +1 -0
  14. package/dist/src/core/dbPathService.d.ts +40 -0
  15. package/dist/src/core/dbPathService.d.ts.map +1 -0
  16. package/dist/src/core/dbPathService.js +324 -0
  17. package/dist/src/core/dbPathService.js.map +1 -0
  18. package/dist/src/core/keyService.d.ts +59 -0
  19. package/dist/src/core/keyService.d.ts.map +1 -0
  20. package/dist/src/core/keyService.js +368 -0
  21. package/dist/src/core/keyService.js.map +1 -0
  22. package/dist/src/core/ntCore.d.ts +62 -0
  23. package/dist/src/core/ntCore.d.ts.map +1 -0
  24. package/dist/src/core/ntCore.js +191 -0
  25. package/dist/src/core/ntCore.js.map +1 -0
  26. package/dist/src/core/sqlcipherCore.d.ts +119 -0
  27. package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
  28. package/dist/src/core/sqlcipherCore.js +693 -0
  29. package/dist/src/core/sqlcipherCore.js.map +1 -0
  30. package/dist/src/core/wcdbCore.d.ts +738 -0
  31. package/dist/src/core/wcdbCore.d.ts.map +1 -0
  32. package/dist/src/core/wcdbCore.js +4545 -0
  33. package/dist/src/core/wcdbCore.js.map +1 -0
  34. package/dist/src/core/wechatClient.d.ts +23 -0
  35. package/dist/src/core/wechatClient.d.ts.map +1 -0
  36. package/dist/src/core/wechatClient.js +166 -0
  37. package/dist/src/core/wechatClient.js.map +1 -0
  38. package/dist/src/services/chatService.d.ts +25 -0
  39. package/dist/src/services/chatService.d.ts.map +1 -0
  40. package/dist/src/services/chatService.js +291 -0
  41. package/dist/src/services/chatService.js.map +1 -0
  42. package/dist/src/services/configService.d.ts +43 -0
  43. package/dist/src/services/configService.d.ts.map +1 -0
  44. package/dist/src/services/configService.js +156 -0
  45. package/dist/src/services/configService.js.map +1 -0
  46. package/dist/src/services/exportService.d.ts +30 -0
  47. package/dist/src/services/exportService.d.ts.map +1 -0
  48. package/dist/src/services/exportService.js +273 -0
  49. package/dist/src/services/exportService.js.map +1 -0
  50. package/dist/src/services/wechat-formatter.d.ts +69 -0
  51. package/dist/src/services/wechat-formatter.d.ts.map +1 -0
  52. package/dist/src/services/wechat-formatter.js +248 -0
  53. package/dist/src/services/wechat-formatter.js.map +1 -0
  54. package/dist/src/services/wechatMessageService.d.ts +28 -0
  55. package/dist/src/services/wechatMessageService.d.ts.map +1 -0
  56. package/dist/src/services/wechatMessageService.js +341 -0
  57. package/dist/src/services/wechatMessageService.js.map +1 -0
  58. package/dist/src/services/wereadService.d.ts +206 -0
  59. package/dist/src/services/wereadService.d.ts.map +1 -0
  60. package/dist/src/services/wereadService.js +145 -0
  61. package/dist/src/services/wereadService.js.map +1 -0
  62. package/dist/src/services/whitelistService.d.ts +20 -0
  63. package/dist/src/services/whitelistService.d.ts.map +1 -0
  64. package/dist/src/services/whitelistService.js +60 -0
  65. package/dist/src/services/whitelistService.js.map +1 -0
  66. package/dist/src/types.d.ts +140 -0
  67. package/dist/src/types.d.ts.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils/errors.d.ts +25 -0
  71. package/dist/src/utils/errors.d.ts.map +1 -0
  72. package/dist/src/utils/errors.js +43 -0
  73. package/dist/src/utils/errors.js.map +1 -0
  74. package/dist/src/utils/pathUtils.d.ts +5 -0
  75. package/dist/src/utils/pathUtils.d.ts.map +1 -0
  76. package/dist/src/utils/pathUtils.js +16 -0
  77. package/dist/src/utils/pathUtils.js.map +1 -0
  78. package/dist/src/utils/pythonRunner.d.ts +27 -0
  79. package/dist/src/utils/pythonRunner.d.ts.map +1 -0
  80. package/dist/src/utils/pythonRunner.js +62 -0
  81. package/dist/src/utils/pythonRunner.js.map +1 -0
  82. package/dist/src/utils/talkerUtils.d.ts +14 -0
  83. package/dist/src/utils/talkerUtils.d.ts.map +1 -0
  84. package/dist/src/utils/talkerUtils.js +73 -0
  85. package/dist/src/utils/talkerUtils.js.map +1 -0
  86. package/mcp-server/index.ts +757 -0
  87. package/package.json +72 -0
  88. package/resources/key/win32/x64/wx_key.dll +0 -0
  89. package/resources/wcdb/win32/x64/SDL2.dll +0 -0
  90. package/resources/wcdb/win32/x64/WCDB.dll +0 -0
  91. package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
  92. package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
  93. package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
  94. package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
  95. package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
  96. package/scripts/_batch_link_sources.py +73 -0
  97. package/scripts/_utils.py +531 -0
  98. package/scripts/annual_report.py +796 -0
  99. package/scripts/auto_tag.py +128 -0
  100. package/scripts/biz_daily.py +896 -0
  101. package/scripts/chat_report.py +397 -0
  102. package/scripts/chat_stats.py +702 -0
  103. package/scripts/classify_daily.py +411 -0
  104. package/scripts/compile_wiki.py +258 -0
  105. package/scripts/create_reading_notes.py +370 -0
  106. package/scripts/enrich_backlinks.py +163 -0
  107. package/scripts/export_chat_html.py +766 -0
  108. package/scripts/extract_3x_key.py +86 -0
  109. package/scripts/extract_todos.py +404 -0
  110. package/scripts/fav_server.py +392 -0
  111. package/scripts/fix_topics.py +111 -0
  112. package/scripts/generate_ai_report.py +367 -0
  113. package/scripts/generate_html.py +1372 -0
  114. package/scripts/generate_review.py +170 -0
  115. package/scripts/mcp_bridge.py +398 -0
  116. package/scripts/nt_decrypt.py +562 -0
  117. package/scripts/pipeline.py +145 -0
  118. package/scripts/promote_all.py +310 -0
  119. package/scripts/promote_ideas.py +297 -0
  120. package/scripts/rag_chat.py +191 -0
  121. package/scripts/scan_decrypt_4x.py +329 -0
  122. package/scripts/semantic_search.py +431 -0
  123. package/scripts/sync_fav.py +146 -0
  124. package/scripts/sync_weread.py +192 -0
  125. package/scripts/vault_rag.py +139 -0
  126. package/scripts/vault_search.py +141 -0
  127. package/src/core/dbPathService.ts +348 -0
  128. package/src/core/keyService.ts +409 -0
  129. package/src/core/ntCore.ts +224 -0
  130. package/src/core/sqlcipherCore.ts +764 -0
  131. package/src/core/wcdbCore.ts +4568 -0
  132. package/src/core/wechatClient.ts +204 -0
  133. package/src/lz4.d.ts +10 -0
  134. package/src/services/chatService.ts +296 -0
  135. package/src/services/configService.ts +197 -0
  136. package/src/services/exportService.ts +305 -0
  137. package/src/services/wechat-formatter.ts +348 -0
  138. package/src/services/wechatMessageService.ts +383 -0
  139. package/src/services/wereadService.ts +308 -0
  140. package/src/services/whitelistService.ts +72 -0
  141. package/src/types.ts +141 -0
  142. package/src/utils/errors.ts +42 -0
  143. package/src/utils/pathUtils.ts +16 -0
  144. package/src/utils/pythonRunner.ts +81 -0
  145. package/src/utils/talkerUtils.ts +84 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * 微信消息收发服务 — 通过 ilink API 桥接登录、收消息、发消息。
3
+ *
4
+ * 协议参考: AstrBot weixin_oc_adapter.py
5
+ */
6
+ import crypto from 'crypto'
7
+ import path from 'path'
8
+ import { WechatClient } from '../core/wechatClient.js'
9
+ import { configService } from './configService.js'
10
+ import type { WechatOCConfig, WechatLoginSession, WechatInboundMessage, WechatMessageComponent } from '../types.js'
11
+
12
+ function sleep(ms: number): Promise<void> {
13
+ return new Promise(resolve => setTimeout(resolve, ms))
14
+ }
15
+
16
+ function uuidHex(): string {
17
+ return crypto.randomUUID().replace(/-/g, '')
18
+ }
19
+
20
+ export class WechatMessageService {
21
+ private client: WechatClient
22
+ private config: WechatOCConfig
23
+ private loginSession: WechatLoginSession | null = null
24
+ private shutdownFlag = false
25
+ private contextTokens: Map<string, string> = new Map()
26
+ private syncBuf: string
27
+ private messageCallbacks: Array<(msg: WechatInboundMessage) => void> = []
28
+
29
+ constructor(config: WechatOCConfig = {}) {
30
+ this.config = config
31
+ this.syncBuf = config.syncBuf || ''
32
+ this.client = new WechatClient({
33
+ baseUrl: config.baseUrl,
34
+ cdnBaseUrl: config.cdnBaseUrl,
35
+ apiTimeoutMs: config.apiTimeoutMs,
36
+ token: config.token,
37
+ })
38
+ }
39
+
40
+ // ====== Login ======
41
+
42
+ async startLogin(): Promise<{ qrcodeUrl: string; qrcodeContent: string }> {
43
+ const data = await this.client.requestJson('GET', 'ilink/bot/get_bot_qrcode', {
44
+ params: { bot_type: this.config.botType || '3' },
45
+ })
46
+
47
+ const qrcode = data.qrcode as string
48
+ const qrcodeImgContent = data.qrcode_img_content as string
49
+
50
+ this.loginSession = {
51
+ sessionKey: uuidHex(),
52
+ qrcode,
53
+ qrcodeImgContent,
54
+ startedAt: Date.now(),
55
+ status: 'wait',
56
+ }
57
+
58
+ return { qrcodeUrl: qrcode, qrcodeContent: qrcodeImgContent }
59
+ }
60
+
61
+ async pollQrStatus(): Promise<WechatLoginSession> {
62
+ if (!this.loginSession) {
63
+ throw new Error('未开始登录流程,请先调用 startLogin()')
64
+ }
65
+
66
+ const data = await this.client.requestJson('GET', 'ilink/bot/get_qrcode_status', {
67
+ params: { qrcode: this.loginSession.qrcode },
68
+ extraHeaders: { 'iLink-App-ClientVersion': '1' },
69
+ })
70
+
71
+ const status = data.status as string
72
+ this.loginSession.status = status as WechatLoginSession['status']
73
+
74
+ if (status === 'confirmed') {
75
+ this.loginSession.botToken = data.bot_token as string
76
+ this.loginSession.accountId = data.ilink_bot_id as string
77
+ this.loginSession.baseUrl = data.baseurl as string
78
+ this.loginSession.userId = data.ilink_user_id as string
79
+
80
+ // Persist token
81
+ if (this.loginSession.botToken) {
82
+ this.client.token = this.loginSession.botToken
83
+ configService.set('wechatOcToken', this.loginSession.botToken)
84
+ }
85
+ if (this.loginSession.accountId) {
86
+ configService.set('wechatOcAccountId', this.loginSession.accountId)
87
+ }
88
+ } else if (status === 'expired') {
89
+ // qrcode expired — caller should retry startLogin()
90
+ }
91
+
92
+ return this.loginSession
93
+ }
94
+
95
+ async waitForLogin(pollIntervalMs = 2000): Promise<WechatLoginSession> {
96
+ let expiredCount = 0
97
+
98
+ while (true) {
99
+ try {
100
+ const session = await this.pollQrStatus()
101
+
102
+ if (session.status === 'confirmed') {
103
+ return session
104
+ }
105
+ if (session.status === 'expired') {
106
+ expiredCount++
107
+ if (expiredCount >= 3) {
108
+ session.error = '二维码过期次数过多,请重新运行登录命令'
109
+ return session
110
+ }
111
+ await this.startLogin()
112
+ console.log('二维码已过期,已获取新二维码,请重新扫码')
113
+ }
114
+ } catch (e: any) {
115
+ // Timeout or network error — retry after interval
116
+ if (e.message?.includes('timeout')) {
117
+ // Expected: QR status poll times out, keep retrying
118
+ } else {
119
+ console.error(`轮询异常: ${e.message}`)
120
+ }
121
+ }
122
+
123
+ await sleep(pollIntervalMs)
124
+ }
125
+ }
126
+
127
+ // ====== Message Polling ======
128
+
129
+ async startPolling(): Promise<void> {
130
+ while (!this.shutdownFlag) {
131
+ try {
132
+ const data = await this.client.requestJson('POST', 'ilink/bot/getupdates', {
133
+ payload: {
134
+ base_info: { channel_version: 'astrbot' },
135
+ get_updates_buf: this.syncBuf,
136
+ },
137
+ tokenRequired: true,
138
+ timeoutMs: 40_000, // Long-poll: server will hold until message arrives
139
+ })
140
+
141
+ // ret/errcode may be absent on empty response (normal for long-poll timeout)
142
+ if (data.ret != null && data.ret !== 0 || data.errcode != null && data.errcode !== 0) {
143
+ console.error(`getupdates error: ret=${data.ret} errcode=${data.errcode} errmsg=${data.errmsg || 'unknown'}`)
144
+ if (data.errcode === -1 || data.errcode === 401) {
145
+ console.error('Token may have expired, please login again')
146
+ }
147
+ await sleep(5000)
148
+ continue
149
+ }
150
+
151
+ if (data.get_updates_buf) {
152
+ this.syncBuf = data.get_updates_buf
153
+ configService.set('wechatOcSyncBuf', this.syncBuf as any)
154
+ }
155
+
156
+ const msgs: any[] = data.msgs || []
157
+ for (const msg of msgs) {
158
+ if (this.shutdownFlag) return
159
+ const inbound = this.parseInboundMessage(msg)
160
+ if (inbound) {
161
+ for (const cb of this.messageCallbacks) {
162
+ try { cb(inbound) } catch {}
163
+ }
164
+ }
165
+ }
166
+ } catch (e: any) {
167
+ if (this.shutdownFlag) return
168
+ // Long-poll timeout is normal — server holds connection until message arrives
169
+ if (e.name === 'AbortError' || e.message?.includes('timeout')) {
170
+ // Restart poll immediately
171
+ continue
172
+ }
173
+ console.error(`Polling error: ${e.message}`)
174
+ await sleep(5000)
175
+ }
176
+ }
177
+ }
178
+
179
+ async stop(): Promise<void> {
180
+ this.shutdownFlag = true
181
+ }
182
+
183
+ // ====== Send ======
184
+
185
+ async sendText(userId: string, text: string): Promise<boolean> {
186
+ if (!this.client.token) return false
187
+
188
+ const contextToken = this.contextTokens.get(userId)
189
+ if (!contextToken) return false
190
+
191
+ const result = await this.client.requestJson('POST', 'ilink/bot/sendmessage', {
192
+ payload: {
193
+ base_info: { channel_version: 'astrbot' },
194
+ msg: {
195
+ from_user_id: '',
196
+ to_user_id: userId,
197
+ client_id: uuidHex(),
198
+ message_type: 2,
199
+ message_state: 2,
200
+ context_token: contextToken,
201
+ item_list: [{ type: 1, text_item: { text } }],
202
+ },
203
+ },
204
+ tokenRequired: true,
205
+ })
206
+
207
+ return result.ret === 0 && result.errcode === 0
208
+ }
209
+
210
+ async sendMedia(
211
+ userId: string,
212
+ filePath: string,
213
+ mediaType: 'image' | 'video' | 'file',
214
+ ): Promise<boolean> {
215
+ if (!this.client.token) return false
216
+
217
+ const contextToken = this.contextTokens.get(userId)
218
+ if (!contextToken) return false
219
+
220
+ const fs = await import('fs/promises')
221
+ const fileBuffer = await fs.readFile(filePath)
222
+ const fileName = path.basename(filePath)
223
+ const rawMD5 = crypto.createHash('md5').update(fileBuffer).digest('hex')
224
+ const rawSize = fileBuffer.length
225
+
226
+ const fileKey = uuidHex()
227
+ const aesKeyHex = crypto.randomBytes(16).toString('hex')
228
+ const cipherSize = WechatClient.aesPaddedSize(rawSize)
229
+
230
+ const typeMap = {
231
+ image: { uploadMediaType: 1, itemType: 2 },
232
+ video: { uploadMediaType: 2, itemType: 5 },
233
+ file: { uploadMediaType: 3, itemType: 4 },
234
+ }
235
+ const { uploadMediaType, itemType } = typeMap[mediaType]
236
+
237
+ // Get upload URL
238
+ const uploadResult = await this.client.requestJson('POST', 'ilink/bot/getuploadurl', {
239
+ payload: {
240
+ filekey: fileKey,
241
+ media_type: uploadMediaType,
242
+ to_user_id: userId,
243
+ rawsize: rawSize,
244
+ rawfilemd5: rawMD5,
245
+ filesize: cipherSize,
246
+ aeskey: aesKeyHex,
247
+ no_need_thumb: true,
248
+ base_info: { channel_version: 'astrbot' },
249
+ },
250
+ tokenRequired: true,
251
+ })
252
+
253
+ if (uploadResult.ret !== 0 || uploadResult.errcode !== 0) {
254
+ console.error(`getuploadurl error: ${uploadResult.errmsg}`)
255
+ return false
256
+ }
257
+
258
+ // Upload to CDN
259
+ const encryptedParam = await this.client.uploadToCdn(
260
+ uploadResult.upload_full_url as string,
261
+ uploadResult.upload_param as string,
262
+ fileKey,
263
+ aesKeyHex,
264
+ fileBuffer,
265
+ )
266
+
267
+ // Build media item and send
268
+ const mediaPayload = {
269
+ encrypt_query_param: encryptedParam,
270
+ aes_key: Buffer.from(aesKeyHex, 'hex').toString('base64'),
271
+ encrypt_type: 1,
272
+ }
273
+
274
+ let itemList: any[]
275
+ if (mediaType === 'image') {
276
+ itemList = [{ type: 2, image_item: { media: mediaPayload, mid_size: rawSize, aeskey: aesKeyHex } }]
277
+ } else if (mediaType === 'file') {
278
+ itemList = [{ type: 4, file_item: { media: mediaPayload, file_name: fileName, len: rawSize } }]
279
+ } else {
280
+ itemList = [{ type: 5, video_item: { media: mediaPayload, video_size: rawSize } }]
281
+ }
282
+
283
+ const result = await this.client.requestJson('POST', 'ilink/bot/sendmessage', {
284
+ payload: {
285
+ base_info: { channel_version: 'astrbot' },
286
+ msg: {
287
+ from_user_id: '',
288
+ to_user_id: userId,
289
+ client_id: uuidHex(),
290
+ message_type: 2,
291
+ message_state: 2,
292
+ context_token: contextToken,
293
+ item_list: itemList,
294
+ },
295
+ },
296
+ tokenRequired: true,
297
+ })
298
+
299
+ return result.ret === 0 && result.errcode === 0
300
+ }
301
+
302
+ async sendImage(userId: string, imagePath: string): Promise<boolean> {
303
+ return this.sendMedia(userId, imagePath, 'image')
304
+ }
305
+
306
+ async sendFile(userId: string, filePath: string, _fileName?: string): Promise<boolean> {
307
+ return this.sendMedia(userId, filePath, 'file')
308
+ }
309
+
310
+ // ====== Callbacks ======
311
+
312
+ onMessage(callback: (msg: WechatInboundMessage) => void): void {
313
+ this.messageCallbacks.push(callback)
314
+ }
315
+
316
+ // ====== Status ======
317
+
318
+ isLoggedIn(): boolean {
319
+ return !!(this.client.token || this.config.token)
320
+ }
321
+
322
+ getAccountId(): string | null {
323
+ return this.config.accountId || null
324
+ }
325
+
326
+ // ====== Private ======
327
+
328
+ private parseInboundMessage(msg: any): WechatInboundMessage | null {
329
+ const fromUserId: string = msg.from_user_id || ''
330
+ if (!fromUserId) return null
331
+
332
+ // Store context token for future replies
333
+ if (msg.context_token) {
334
+ this.contextTokens.set(fromUserId, msg.context_token)
335
+ }
336
+
337
+ const components: WechatMessageComponent[] = []
338
+ const itemList: any[] = msg.item_list || []
339
+
340
+ for (const item of itemList) {
341
+ const itemType = item.type as number
342
+ if (itemType === 1 && item.text_item) {
343
+ components.push({ type: 'plain', text: item.text_item.text || '' })
344
+ } else if (itemType === 2 && item.image_item) {
345
+ components.push({ type: 'image', filePath: '' }) // image download deferred
346
+ } else if (itemType === 3 && item.voice_item) {
347
+ components.push({ type: 'record', filePath: '' })
348
+ } else if (itemType === 4 && item.file_item) {
349
+ components.push({ type: 'file', name: item.file_item.file_name || '', filePath: '' })
350
+ } else if (itemType === 5 && item.video_item) {
351
+ components.push({ type: 'video', filePath: '' })
352
+ }
353
+ }
354
+
355
+ // Determine message kind
356
+ let messageKind: WechatInboundMessage['messageKind'] = 'unknown'
357
+ if (itemList.length === 1) {
358
+ const t = itemList[0].type
359
+ if (t === 1) messageKind = 'text'
360
+ else if (t === 2) messageKind = 'image'
361
+ else if (t === 3) messageKind = 'voice'
362
+ else if (t === 4) messageKind = 'file'
363
+ else if (t === 5) messageKind = 'video'
364
+ }
365
+
366
+ const textComponents = components.filter(c => c.type === 'plain')
367
+ const messageStr = textComponents.map(c => (c as { type: 'plain'; text: string }).text).join('')
368
+
369
+ return {
370
+ messageId: msg.client_id || uuidHex(),
371
+ fromUserId,
372
+ senderNickname: msg.from_user_id || '',
373
+ timestamp: Math.floor(Date.now() / 1000),
374
+ timestampMs: Date.now(),
375
+ components,
376
+ messageStr,
377
+ messageKind,
378
+ rawMessage: msg,
379
+ isReply: !!(msg.ref_msg),
380
+ quotedText: msg.ref_msg?.text || undefined,
381
+ }
382
+ }
383
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * 微信读书 Agent API Gateway 封装
3
+ *
4
+ * 接口文档: ~/.claude/skills/weread-skills/
5
+ * Gateway: https://i.weread.qq.com/api/agent/gateway
6
+ */
7
+
8
+ import https from 'node:https'
9
+
10
+ const GATEWAY = 'https://i.weread.qq.com/api/agent/gateway'
11
+ const SKILL_VERSION = '1.0.3'
12
+
13
+ // weread.qq.com 服务端不支持 TLS 1.3,需要强制使用 TLS 1.2
14
+ const httpsAgent = new https.Agent({ maxVersion: 'TLSv1.2' })
15
+
16
+ export interface WereadResult<T = any> {
17
+ ok: boolean
18
+ data?: T
19
+ error?: string
20
+ }
21
+
22
+ // ---- shelf ----
23
+
24
+ export interface ShelfBook {
25
+ bookId: string
26
+ title: string
27
+ author: string
28
+ cover: string
29
+ category: string
30
+ readUpdateTime: number
31
+ finishReading: number
32
+ updateTime: number
33
+ secret: number
34
+ isTop: boolean
35
+ payType: number
36
+ }
37
+
38
+ export interface ShelfData {
39
+ books: ShelfBook[]
40
+ albums: any[]
41
+ mp: any
42
+ archive: { name: string; bookIds: string[]; albumIds: string[] }[]
43
+ bookCount: number
44
+ }
45
+
46
+ // ---- readdata ----
47
+
48
+ export interface ReadDetail {
49
+ baseTime: number
50
+ readTimes: Record<string, number>
51
+ readDays: number
52
+ totalReadTime: number
53
+ dayAverageReadTime: number
54
+ compare?: number
55
+ readLongest: { book?: any; albumInfo?: any; readTime: number; tags?: string[] }[]
56
+ readStat: { stat: string; counts: string; scheme?: string }[]
57
+ preferCategory: { categoryId: number; categoryTitle: string; readingTime: number; readingCount: number; val: number }[]
58
+ preferCategoryWord?: string
59
+ preferTime: number[]
60
+ preferTimeWord?: string
61
+ }
62
+
63
+ // ---- notes ----
64
+
65
+ export interface NotebookItem {
66
+ bookId: string
67
+ title: string
68
+ author: string
69
+ cover: string
70
+ highlightCount: number
71
+ noteCount: number
72
+ bookmarkCount: number
73
+ updateTime: number
74
+ }
75
+
76
+ export interface NoteDetail {
77
+ bookId: string
78
+ chapterUid: number
79
+ chapterTitle: string
80
+ markText: string
81
+ content: string
82
+ range: string
83
+ createTime: number
84
+ type: number // 0=划线, 1=想法
85
+ }
86
+
87
+ // ---- search ----
88
+
89
+ export interface SearchResult {
90
+ bookId: string
91
+ title: string
92
+ author: string
93
+ cover: string
94
+ rating: number
95
+ ratingCount: number
96
+ category: string
97
+ wordCount: string
98
+ intro: string
99
+ }
100
+
101
+ // ---- book ----
102
+
103
+ export interface BookInfo {
104
+ bookId: string
105
+ title: string
106
+ author: string
107
+ cover: string
108
+ rating: number
109
+ ratingCount: number
110
+ category: string
111
+ wordCount: string
112
+ intro: string
113
+ publisher: string
114
+ isbn: string
115
+ totalWords: string
116
+ format: string
117
+ }
118
+
119
+ export interface ChapterInfo {
120
+ bookId: string
121
+ title: string
122
+ chapters: { chapterUid: number; title: string; level: number; wordCount: number }[]
123
+ }
124
+
125
+ // ---- review ----
126
+
127
+ export interface BookReview {
128
+ reviewId: string
129
+ content: string
130
+ rating: number
131
+ createTime: number
132
+ user: { name: string; avatar: string }
133
+ likeCount: number
134
+ }
135
+
136
+ // ---- discover ----
137
+
138
+ export interface RecommendBook {
139
+ bookId: string
140
+ title: string
141
+ author: string
142
+ cover: string
143
+ rating: number
144
+ intro: string
145
+ }
146
+
147
+ // ---- profile ----
148
+
149
+ export interface UserProfile {
150
+ vid: string
151
+ name: string
152
+ avatar: string
153
+ gender: number
154
+ totalReadTime: number
155
+ totalReadDays: number
156
+ totalBooks: number
157
+ totalFinished: number
158
+ totalNotes: number
159
+ totalHighlights: number
160
+ totalReviews: number
161
+ }
162
+
163
+
164
+ export class WereadService {
165
+ private apiKey: string
166
+
167
+ constructor(apiKey?: string) {
168
+ this.apiKey = apiKey || process.env.WEREAD_API_KEY || ''
169
+ }
170
+
171
+ private async call<T>(apiName: string, params: Record<string, any> = {}): Promise<WereadResult<T>> {
172
+ if (!this.apiKey) {
173
+ return { ok: false, error: '未设置 WEREAD_API_KEY,请设置环境变量或在 CLI 中配置' }
174
+ }
175
+
176
+ try {
177
+ const body = JSON.stringify({ api_name: apiName, skill_version: SKILL_VERSION, ...params })
178
+ const json = await new Promise<any>((resolve, reject) => {
179
+ const url = new URL(GATEWAY)
180
+ const req = https.request({
181
+ hostname: url.hostname,
182
+ path: url.pathname,
183
+ method: 'POST',
184
+ agent: httpsAgent,
185
+ headers: {
186
+ 'Authorization': `Bearer ${this.apiKey}`,
187
+ 'Content-Type': 'application/json',
188
+ 'Content-Length': Buffer.byteLength(body),
189
+ },
190
+ }, (res) => {
191
+ let data = ''
192
+ res.on('data', (chunk: Buffer) => data += chunk.toString())
193
+ res.on('end', () => {
194
+ try { resolve(JSON.parse(data)) }
195
+ catch { reject(new Error(`Invalid JSON response: ${data.slice(0, 200)}`)) }
196
+ })
197
+ })
198
+ req.on('error', reject)
199
+ req.write(body)
200
+ req.end()
201
+ })
202
+
203
+ if (json.errcode && json.errcode !== 0) {
204
+ return { ok: false, error: `API 错误: ${json.errmsg || json.errcode}` }
205
+ }
206
+ return { ok: true, data: json as T }
207
+ } catch (e: any) {
208
+ return { ok: false, error: `请求失败: ${e.message}` }
209
+ }
210
+ }
211
+
212
+ /** 书架同步 */
213
+ async shelf(): Promise<WereadResult<ShelfData>> {
214
+ return this.call<ShelfData>('/shelf/sync')
215
+ }
216
+
217
+ /** 阅读统计: mode = weekly | monthly | annually | overall */
218
+ async readData(mode: string = 'monthly', baseTime?: number): Promise<WereadResult<ReadDetail>> {
219
+ const params: any = { mode }
220
+ if (baseTime !== undefined) params.baseTime = baseTime
221
+ return this.call<ReadDetail>('/readdata/detail', params)
222
+ }
223
+
224
+ /** 笔记列表: 获取用户的笔记本列表 */
225
+ async notebooks(count: number = 100, lastSort?: number): Promise<WereadResult<{ books: NotebookItem[]; synckey: number }>> {
226
+ const params: any = { count }
227
+ if (lastSort !== undefined) params.lastSort = lastSort
228
+ return this.call('/user/notebooks', params)
229
+ }
230
+
231
+ /** 某本书的划线 */
232
+ async bookmarks(bookId: string, count: number = 50): Promise<WereadResult<{ updated: NoteDetail[]; removed: any[]; synckey: number }>> {
233
+ return this.call('/book/bookmarklist', { bookId, count })
234
+ }
235
+
236
+ /** 某本书的热门划线 */
237
+ async bestBookmarks(bookId: string, count: number = 20): Promise<WereadResult<{ chapters: any[] }>> {
238
+ return this.call('/book/bestbookmarks', { bookId, count })
239
+ }
240
+
241
+ /** 搜索书籍 */
242
+ async search(keyword: string, count: number = 10, scope: string = 'all'): Promise<WereadResult<{ books: SearchResult[] }>> {
243
+ return this.call('/store/search', { keyword, count, scope })
244
+ }
245
+
246
+ /** 书籍详情 */
247
+ async bookInfo(bookId: string): Promise<WereadResult<BookInfo>> {
248
+ return this.call<BookInfo>('/book/info', { bookId })
249
+ }
250
+
251
+ /** 章节目录 */
252
+ async chapterInfo(bookId: string): Promise<WereadResult<ChapterInfo>> {
253
+ return this.call<ChapterInfo>('/book/chapterinfo', { bookId })
254
+ }
255
+
256
+ /** 阅读进度 */
257
+ async getProgress(bookId: string): Promise<WereadResult<{ chapterUid: number; chapterTitle: string; progress: number }>> {
258
+ return this.call('/book/getprogress', { bookId })
259
+ }
260
+
261
+ /** 书评 */
262
+ async reviews(bookId: string, count: number = 20, sort: string = 'hot'): Promise<WereadResult<{ reviews: BookReview[] }>> {
263
+ return this.call('/book/readreviews', { bookId, count, sort })
264
+ }
265
+
266
+ /** 个性化推荐 */
267
+ async recommend(count: number = 10): Promise<WereadResult<{ books: RecommendBook[] }>> {
268
+ return this.call('/book/recommend', { count })
269
+ }
270
+
271
+ /** 相似推荐 */
272
+ async similar(bookId: string, count: number = 5): Promise<WereadResult<{ books: RecommendBook[] }>> {
273
+ return this.call('/book/recommend', { bookId, count, type: 'similar' })
274
+ }
275
+
276
+ /** 个人中心(聚合多个接口) */
277
+ async profile(): Promise<WereadResult<UserProfile>> {
278
+ const [shelfRes, readRes] = await Promise.all([
279
+ this.shelf(),
280
+ this.readData('overall'),
281
+ ])
282
+
283
+ if (!shelfRes.ok) return { ok: false, error: shelfRes.error }
284
+ if (!readRes.ok) return { ok: false, error: readRes.error }
285
+
286
+ const shelf = shelfRes.data!
287
+ const read = readRes.data!
288
+
289
+ return {
290
+ ok: true,
291
+ data: {
292
+ vid: '',
293
+ name: '',
294
+ avatar: '',
295
+ gender: 0,
296
+ totalReadTime: read.totalReadTime || 0,
297
+ totalReadDays: read.readDays || 0,
298
+ totalBooks: shelf.books?.length || 0,
299
+ totalFinished: shelf.books?.filter(b => b.finishReading === 1).length || 0,
300
+ totalNotes: 0,
301
+ totalHighlights: 0,
302
+ totalReviews: 0,
303
+ },
304
+ }
305
+ }
306
+ }
307
+
308
+ export const wereadService = new WereadService()