weflow-cli 1.8.2 → 1.10.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 (185) hide show
  1. package/ARCHITECTURE.md +62 -26
  2. package/CHANGELOG.md +1290 -0
  3. package/OPERATIONS.md +144 -11
  4. package/README.en.md +20 -4
  5. package/README.md +30 -5
  6. package/bin/weflow-cli-electron.cjs +2 -0
  7. package/bin/weflow-cli.ts +996 -118
  8. package/dist/bin/weflow-cli.js +1038 -146
  9. package/dist/bin/weflow-cli.js.map +1 -1
  10. package/dist/mcp-server/http.d.ts +14 -0
  11. package/dist/mcp-server/http.d.ts.map +1 -0
  12. package/dist/mcp-server/http.js +84 -0
  13. package/dist/mcp-server/http.js.map +1 -0
  14. package/dist/mcp-server/httpConfig.d.ts +50 -0
  15. package/dist/mcp-server/httpConfig.d.ts.map +1 -0
  16. package/dist/mcp-server/httpConfig.js +104 -0
  17. package/dist/mcp-server/httpConfig.js.map +1 -0
  18. package/dist/mcp-server/index.d.ts +13 -1
  19. package/dist/mcp-server/index.d.ts.map +1 -1
  20. package/dist/mcp-server/index.js +58 -374
  21. package/dist/mcp-server/index.js.map +1 -1
  22. package/dist/src/core/dbPathService.d.ts +12 -0
  23. package/dist/src/core/dbPathService.d.ts.map +1 -1
  24. package/dist/src/core/dbPathService.js +20 -3
  25. package/dist/src/core/dbPathService.js.map +1 -1
  26. package/dist/src/panel/server.d.ts.map +1 -1
  27. package/dist/src/panel/server.js +61 -1
  28. package/dist/src/panel/server.js.map +1 -1
  29. package/dist/src/services/assistantEval.d.ts +69 -0
  30. package/dist/src/services/assistantEval.d.ts.map +1 -1
  31. package/dist/src/services/assistantEval.js +270 -8
  32. package/dist/src/services/assistantEval.js.map +1 -1
  33. package/dist/src/services/assistantScenes.d.ts +81 -0
  34. package/dist/src/services/assistantScenes.d.ts.map +1 -0
  35. package/dist/src/services/assistantScenes.js +322 -0
  36. package/dist/src/services/assistantScenes.js.map +1 -0
  37. package/dist/src/services/assistantService.d.ts +33 -4
  38. package/dist/src/services/assistantService.d.ts.map +1 -1
  39. package/dist/src/services/assistantService.js +102 -6
  40. package/dist/src/services/assistantService.js.map +1 -1
  41. package/dist/src/services/assistantSkills.d.ts +94 -0
  42. package/dist/src/services/assistantSkills.d.ts.map +1 -0
  43. package/dist/src/services/assistantSkills.js +330 -0
  44. package/dist/src/services/assistantSkills.js.map +1 -0
  45. package/dist/src/services/assistantTools.d.ts +34 -1
  46. package/dist/src/services/assistantTools.d.ts.map +1 -1
  47. package/dist/src/services/assistantTools.js +931 -88
  48. package/dist/src/services/assistantTools.js.map +1 -1
  49. package/dist/src/services/assistantTrace.d.ts +4 -0
  50. package/dist/src/services/assistantTrace.d.ts.map +1 -1
  51. package/dist/src/services/assistantTrace.js +9 -0
  52. package/dist/src/services/assistantTrace.js.map +1 -1
  53. package/dist/src/services/configService.d.ts +9 -0
  54. package/dist/src/services/configService.d.ts.map +1 -1
  55. package/dist/src/services/configService.js +5 -2
  56. package/dist/src/services/configService.js.map +1 -1
  57. package/dist/src/services/messageQuery.d.ts +18 -0
  58. package/dist/src/services/messageQuery.d.ts.map +1 -1
  59. package/dist/src/services/messageQuery.js +29 -0
  60. package/dist/src/services/messageQuery.js.map +1 -1
  61. package/dist/src/services/outboundSend.d.ts +53 -0
  62. package/dist/src/services/outboundSend.d.ts.map +1 -0
  63. package/dist/src/services/outboundSend.js +90 -0
  64. package/dist/src/services/outboundSend.js.map +1 -0
  65. package/dist/src/services/pythonBridge.d.ts.map +1 -1
  66. package/dist/src/services/pythonBridge.js +1 -0
  67. package/dist/src/services/pythonBridge.js.map +1 -1
  68. package/dist/src/services/wechatClient.d.ts +125 -0
  69. package/dist/src/services/wechatClient.d.ts.map +1 -0
  70. package/dist/src/services/wechatClient.js +344 -0
  71. package/dist/src/services/wechatClient.js.map +1 -0
  72. package/dist/src/services/wechatMedia.d.ts +44 -0
  73. package/dist/src/services/wechatMedia.d.ts.map +1 -0
  74. package/dist/src/services/wechatMedia.js +155 -0
  75. package/dist/src/services/wechatMedia.js.map +1 -0
  76. package/dist/src/services/wechatMessageService.d.ts +11 -0
  77. package/dist/src/services/wechatMessageService.d.ts.map +1 -1
  78. package/dist/src/services/wechatMessageService.js +55 -11
  79. package/dist/src/services/wechatMessageService.js.map +1 -1
  80. package/dist/src/types.d.ts +4 -0
  81. package/dist/src/types.d.ts.map +1 -1
  82. package/dist/src/utils/python.js +2 -2
  83. package/dist/src/utils/python.js.map +1 -1
  84. package/docs/BRANCHES.md +37 -0
  85. package/docs/CONTACT_DB_SCHEMA.md +1085 -0
  86. package/docs/DECISIONS.md +1624 -0
  87. package/docs/EXTENDING.md +98 -10
  88. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +6 -0
  89. package/docs/HEALTH-CHECK.md +12 -7
  90. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +2 -2
  91. package/docs/MCP.md +58 -4
  92. package/docs/PROJECT_STATE.md +180 -11
  93. package/docs/RELEASING.md +115 -1
  94. package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
  95. package/logo.png +0 -0
  96. package/mcp-server/http.ts +108 -0
  97. package/mcp-server/httpConfig.ts +126 -0
  98. package/mcp-server/index.ts +359 -681
  99. package/package.json +5 -3
  100. package/resources/js/graph3d/NOTICE.txt +35 -0
  101. package/resources/js/graph3d/d3-binarytree.min.js +2 -0
  102. package/resources/js/graph3d/d3-dispatch.min.js +2 -0
  103. package/resources/js/graph3d/d3-force-3d.min.js +2 -0
  104. package/resources/js/graph3d/d3-octree.min.js +2 -0
  105. package/resources/js/graph3d/d3-quadtree.min.js +2 -0
  106. package/resources/js/graph3d/d3-timer.min.js +2 -0
  107. package/resources/js/graph3d/layout.mjs +63 -0
  108. package/resources/js/graph3d/three.min.js +6 -0
  109. package/resources/js/graph3d/viewer2d.js +448 -0
  110. package/resources/panel/ball-position.cjs +48 -2
  111. package/resources/panel/index.html +18 -4
  112. package/resources/panel/main.cjs +137 -5
  113. package/resources/panel/mascot-base.png +0 -0
  114. package/resources/panel/mascot-blink-1.png +0 -0
  115. package/resources/panel/mascot-blink-2.png +0 -0
  116. package/resources/panel/mascot-iris.png +0 -0
  117. package/resources/panel/mascot-lift-down-1.png +0 -0
  118. package/resources/panel/mascot-lift-down-2.png +0 -0
  119. package/resources/panel/mascot-lift-down-3.png +0 -0
  120. package/resources/panel/mascot-lift-down-4.png +0 -0
  121. package/resources/panel/mascot-lift-down-5.png +0 -0
  122. package/resources/panel/mascot-lift-down-6.png +0 -0
  123. package/resources/panel/mascot-lift-up-1.png +0 -0
  124. package/resources/panel/mascot-lift-up-2.png +0 -0
  125. package/resources/panel/mascot-lift-up-3.png +0 -0
  126. package/resources/panel/mascot-lift-up-4.png +0 -0
  127. package/resources/panel/mascot-lift-up-5.png +0 -0
  128. package/resources/panel/mascot-lift-up-6.png +0 -0
  129. package/resources/panel/mascot-peek.png +0 -0
  130. package/resources/panel/mascot-tickle-1.png +0 -0
  131. package/resources/panel/mascot-tickle-2.png +0 -0
  132. package/resources/panel/mascot-tickle-3.png +0 -0
  133. package/resources/panel/mascot-tickle-4.png +0 -0
  134. package/resources/panel/panel.css +205 -59
  135. package/resources/panel/preload.cjs +26 -0
  136. package/resources/panel/renderer.js +646 -17
  137. package/scripts/_utils.py +1203 -931
  138. package/scripts/backfill_articles.py +202 -21
  139. package/scripts/biz_daily.py +26 -34
  140. package/scripts/bonds.py +331 -0
  141. package/scripts/chat_notes.py +561 -470
  142. package/scripts/chat_stats.py +8 -7
  143. package/scripts/check_readme_commands.py +426 -0
  144. package/scripts/compile_wiki.py +187 -59
  145. package/scripts/contact_schema.py +339 -0
  146. package/scripts/data/wechat_emoji.json +225 -0
  147. package/scripts/export_chat_html.py +3 -1
  148. package/scripts/fav_notes.py +4 -1
  149. package/scripts/graph_2d.py +151 -0
  150. package/scripts/graph_3d.py +646 -0
  151. package/scripts/health_check.py +163 -26
  152. package/scripts/mcp_bridge.py +6 -7
  153. package/scripts/nt_decrypt.py +3 -3
  154. package/scripts/panel_frames.py +683 -0
  155. package/scripts/pipeline.py +21 -10
  156. package/scripts/quality_eval.py +5 -0
  157. package/scripts/rag_chat.py +33 -16
  158. package/scripts/reply_debt.py +26 -6
  159. package/scripts/semantic_search.py +130 -22
  160. package/scripts/vault_rag.py +42 -16
  161. package/scripts/vault_search.py +44 -8
  162. package/scripts/wechat_emoticon.py +6 -0
  163. package/scripts/wechat_voice.py +9 -0
  164. package/scripts/wiki_lint.py +124 -24
  165. package/src/core/dbPathService.ts +22 -4
  166. package/src/panel/server.ts +58 -1
  167. package/src/services/assistantEval.ts +323 -12
  168. package/src/services/assistantScenes.ts +342 -0
  169. package/src/services/assistantService.ts +107 -6
  170. package/src/services/assistantSkills.ts +364 -0
  171. package/src/services/assistantTools.ts +2452 -1640
  172. package/src/services/assistantTrace.ts +9 -0
  173. package/src/services/configService.ts +415 -403
  174. package/src/services/messageQuery.ts +29 -0
  175. package/src/services/outboundSend.ts +126 -0
  176. package/src/services/pythonBridge.ts +1 -0
  177. package/src/services/wechatClient.ts +380 -0
  178. package/src/services/wechatMedia.ts +166 -0
  179. package/src/services/wechatMessageService.ts +62 -11
  180. package/src/types.ts +4 -0
  181. package/src/utils/python.ts +2 -2
  182. package/dist/src/utils/wechatEmoji.d.ts +0 -5
  183. package/dist/src/utils/wechatEmoji.d.ts.map +0 -1
  184. package/dist/src/utils/wechatEmoji.js +0 -35
  185. package/dist/src/utils/wechatEmoji.js.map +0 -1
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 官方通道(ilink)**入站媒体**的落地:把 CDN 上的密文拉下来、解密、写到本地磁盘。
3
+ *
4
+ * 为什么值得做:入站图片/语音/文件/视频此前只解析出"有这一项",`filePath` 是空串,于是助手拿到消息
5
+ * 也不知道图里是什么(那四项的落地当时是显式延后的)。下载与解密的本事本来就有
6
+ * (`WechatClient.downloadMedia`:`<cdn>/download?encrypted_query_param=…` + AES-128-ECB + 去 PKCS#7),
7
+ * 缺的只是把它接上入站这条路。
8
+ *
9
+ * 字段名与优先级照**厂商自己的实现**(`third-party/WeKnora/internal/im/wechat/longpoll.go`):
10
+ * 图片优先用 `image_item.aeskey`(十六进制字符串,**不走 base64**),退回 `media.aes_key`(base64);
11
+ * 语音/文件/视频只有 `media.aes_key`。两种编码都由 `WechatClient.parseMediaAesKey` 认。
12
+ *
13
+ * 三条边界写在这里,不指望调用方记得:
14
+ * 1. **它不是默认行为** —— 打开 `wechatMediaDownload` 才发生网络访问;
15
+ * 2. **文件名永远由本地决定** —— 服务端给的名字只取 basename 并过滤字符,`../` 之类必须在这里被吃掉;
16
+ * 3. **超过上限就不写**(`WECHAT_MEDIA_MAX_BYTES`),失败不留半个文件 —— 半个文件比没有更难查。
17
+ */
18
+ import { basename, join } from 'path'
19
+ import { mkdir, writeFile } from 'fs/promises'
20
+
21
+ /** 单件上限。够大(手机拍的视频也能过),又不至于一次 CDN 事故把磁盘写满 */
22
+ export const WECHAT_MEDIA_MAX_BYTES = 25 * 1024 * 1024
23
+
24
+ /** 落地目录:默认仓库相对 `output/wechat-media/`(`output/` 已在 .gitignore 里),可用环境变量改(测试用临时目录) */
25
+ export function wechatMediaDir(env: NodeJS.ProcessEnv = process.env): string {
26
+ const fromEnv = env.WEFLOW_WECHAT_MEDIA_DIR
27
+ return fromEnv && fromEnv.trim() ? fromEnv : join('output', 'wechat-media')
28
+ }
29
+
30
+ export type InboundMediaType = 'image' | 'record' | 'file' | 'video'
31
+
32
+ /** item.type 的数字编码(与服务端 item_list 一致) */
33
+ export function mediaTypeOf(itemType: number): InboundMediaType | null {
34
+ if (itemType === 2) return 'image'
35
+ if (itemType === 3) return 'record'
36
+ if (itemType === 4) return 'file'
37
+ if (itemType === 5) return 'video'
38
+ return null
39
+ }
40
+
41
+ /** 服务端给的名字只当作"一个字符串"用:取 basename、去掉控制字符与路径字符、去掉前导点、限长 */
42
+ export function sanitizeFileName(name: string): string {
43
+ const base = basename(String(name || '').replace(/\\/g, '/')).trim()
44
+ return base
45
+ .replace(/[\u0000-\u001f<>:"|?*]/g, '_')
46
+ .replace(/^\.+/, '')
47
+ .trim()
48
+ .slice(0, 80)
49
+ }
50
+
51
+ /** 按魔数认扩展名。认不出就返回空串(调用方退回按类型给的后缀),不猜 */
52
+ export function extFromMagic(buf: Buffer): string {
53
+ const b = buf
54
+ if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return '.jpg'
55
+ if (b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return '.png'
56
+ if (b.length >= 6 && b.subarray(0, 6).toString('latin1').startsWith('GIF8')) return '.gif'
57
+ if (b.length >= 12 && b.subarray(0, 4).toString('latin1') === 'RIFF'
58
+ && b.subarray(8, 12).toString('latin1') === 'WEBP') return '.webp'
59
+ if (b.length >= 2 && b[0] === 0x42 && b[1] === 0x4d) return '.bmp'
60
+ if (b.length >= 12 && b.subarray(4, 8).toString('latin1') === 'ftyp') return '.mp4'
61
+ if (b.length >= 4 && b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3) return '.webm'
62
+ if (b.length >= 5 && b.subarray(0, 5).toString('latin1') === '#!AMR') return '.amr'
63
+ if (b.length >= 4 && b.subarray(0, 4).toString('latin1') === 'OggS') return '.ogg'
64
+ if (b.length >= 2 && b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33) return '.mp3'
65
+ if (b.length >= 12 && b.subarray(0, 4).toString('latin1') === 'RIFF'
66
+ && b.subarray(8, 12).toString('latin1') === 'WAVE') return '.wav'
67
+ return ''
68
+ }
69
+
70
+ /** 该媒体类型在服务端字段树里的节点名 */
71
+ function mediaNode(item: any, mediaType: InboundMediaType): any {
72
+ if (mediaType === 'image') return item?.image_item
73
+ if (mediaType === 'record') return item?.voice_item
74
+ if (mediaType === 'file') return item?.file_item
75
+ return item?.video_item
76
+ }
77
+
78
+ /** 下载参数(`encrypt_query_param`)。取不到就没法下载——**这时候不猜、不留空文件** */
79
+ export function pickEncryptParam(item: any, mediaType: InboundMediaType): string {
80
+ const node = mediaNode(item, mediaType)
81
+ return String(node?.media?.encrypt_query_param || '')
82
+ }
83
+
84
+ /**
85
+ * AES key 的取值优先级。**图片这一支必须照厂商实现**:`image_item.aeskey` 是十六进制字符串,
86
+ * 比 `media.aes_key`(base64)更靠前;漏了这个优先级,图片会解出一堆乱码却不报错。
87
+ */
88
+ export function pickAesKeyValue(item: any, mediaType: InboundMediaType): string {
89
+ const node = mediaNode(item, mediaType)
90
+ if (mediaType === 'image' && node?.aeskey) return String(node.aeskey)
91
+ return String(node?.media?.aes_key || '')
92
+ }
93
+
94
+ export interface SaveMediaOptions {
95
+ item: any
96
+ mediaType: InboundMediaType
97
+ /** 同一条消息里的第几项(一条消息可以带多张图) */
98
+ index: number
99
+ /** 名字前缀:优先用服务端给的 client_id */
100
+ messageId: string
101
+ dir?: string
102
+ /** 注入的下载器(默认由调用方传 `client.downloadMedia`),测试里换成假的 */
103
+ download: (encryptedQueryParam: string, aesKeyValue: string) => Promise<Buffer>
104
+ /** 文件类消息里服务端给的文件名 */
105
+ serverFileName?: string
106
+ maxBytes?: number
107
+ }
108
+
109
+ export interface SaveMediaResult {
110
+ /** 成功是磁盘路径;失败是空串(**绝不返回"看起来成功"的半成品**) */
111
+ path: string
112
+ /** 失败原因,进日志用 */
113
+ reason: string
114
+ }
115
+
116
+ /**
117
+ * 下载一件入站媒体并落盘。失败一律返回 `{ path: '', reason }` 且**不留下文件**:
118
+ * 助手与导出拿到空 path 时知道"没有这玩意",比拿到一个 0 字节文件强。
119
+ */
120
+ export async function saveInboundMedia(opts: SaveMediaOptions): Promise<SaveMediaResult> {
121
+ const { item, mediaType, index, messageId, download } = opts
122
+ const cap = opts.maxBytes ?? WECHAT_MEDIA_MAX_BYTES
123
+
124
+ const encryptParam = pickEncryptParam(item, mediaType)
125
+ if (!encryptParam) return { path: '', reason: '服务端没给 encrypt_query_param' }
126
+
127
+ const aesKeyValue = pickAesKeyValue(item, mediaType)
128
+ if (!aesKeyValue) return { path: '', reason: '服务端没给 aes key' }
129
+
130
+ let bytes: Buffer
131
+ try {
132
+ bytes = await download(encryptParam, aesKeyValue)
133
+ } catch (error: any) {
134
+ return { path: '', reason: `下载或解密失败:${error?.message || error}` }
135
+ }
136
+ if (!bytes || bytes.length === 0) return { path: '', reason: '下载结果为空' }
137
+ if (bytes.length > cap) {
138
+ return { path: '', reason: `超过上限(${bytes.length} > ${cap} 字节)` }
139
+ }
140
+
141
+ const byMagic = extFromMagic(bytes)
142
+ const fallbackExt = mediaType === 'image' ? '.jpg' : ''
143
+ const prefix = sanitizeFileName(messageId) || 'msg'
144
+ let name: string
145
+ if (mediaType === 'file' && opts.serverFileName) {
146
+ const safe = sanitizeFileName(opts.serverFileName)
147
+ if (safe) {
148
+ const hasExt = /\.[A-Za-z0-9]{1,8}$/.test(safe)
149
+ name = `${prefix}-${index}-${hasExt || !byMagic ? safe : safe + byMagic}`
150
+ } else {
151
+ name = `${prefix}-${index}${byMagic}`
152
+ }
153
+ } else {
154
+ name = `${prefix}-${index}${byMagic || fallbackExt}`
155
+ }
156
+
157
+ const dir = opts.dir || wechatMediaDir()
158
+ const full = join(dir, name)
159
+ try {
160
+ await mkdir(dir, { recursive: true })
161
+ await writeFile(full, bytes)
162
+ } catch (error: any) {
163
+ return { path: '', reason: `写盘失败:${error?.message || error}` }
164
+ }
165
+ return { path: full, reason: '' }
166
+ }
@@ -8,6 +8,8 @@ import path from 'path'
8
8
  import { WechatClient } from '../core/wechatClient.js'
9
9
  import { configService } from './configService.js'
10
10
  import { resolveInboundRouting } from './assistantRouting.js'
11
+ import { mediaTypeOf, saveInboundMedia, wechatMediaDir } from './wechatMedia.js'
12
+ import type { InboundMediaType } from './wechatMedia.js'
11
13
  import type { WechatOCConfig, WechatLoginSession, WechatInboundMessage, WechatMessageComponent } from '../types.js'
12
14
 
13
15
  function sleep(ms: number): Promise<void> {
@@ -64,10 +66,16 @@ export class WechatMessageService {
64
66
  private messageCallbacks: Array<(msg: WechatInboundMessage) => void> = []
65
67
  /** 服务端说过 token 失效了。**只由长轮询置位**(见 `classifyPollResult`),恢复时清掉 */
66
68
  private tokenExpired = false
69
+ /** 入站媒体是否落地。undefined = 每次去读配置(默认关,见 `wechatMedia.ts` 的边界 1) */
70
+ private mediaDownload?: boolean | string
71
+ /** 落地目录。undefined = 用 `wechatMediaDir()`(可被环境变量改,测试用临时目录) */
72
+ private mediaDirOverride?: string
67
73
 
68
74
  constructor(config: WechatOCConfig = {}) {
69
75
  this.config = config
70
76
  this.syncBuf = config.syncBuf || ''
77
+ this.mediaDownload = config.mediaDownload
78
+ this.mediaDirOverride = config.mediaDir
71
79
  this.client = new WechatClient({
72
80
  baseUrl: config.baseUrl,
73
81
  cdnBaseUrl: config.cdnBaseUrl,
@@ -225,7 +233,7 @@ export class WechatMessageService {
225
233
  const msgs: any[] = data.msgs || []
226
234
  for (const msg of msgs) {
227
235
  if (this.shutdownFlag) return
228
- const inbound = this.parseInboundMessage(msg)
236
+ const inbound = await this.parseInboundMessage(msg)
229
237
  if (inbound) {
230
238
  for (const cb of this.messageCallbacks) {
231
239
  try { cb(inbound) } catch (error: any) {
@@ -409,7 +417,40 @@ export class WechatMessageService {
409
417
 
410
418
  // ====== Private ======
411
419
 
412
- private parseInboundMessage(msg: any): WechatInboundMessage | null {
420
+ /** 入站媒体落地的开关:构造时注入的优先,其次读配置。默认关 —— 关着就不发任何网络请求 */
421
+ private mediaDownloadEnabled(): boolean {
422
+ const raw = this.mediaDownload ?? configService.get('wechatMediaDownload')
423
+ return raw === true || raw === 'true'
424
+ }
425
+
426
+ /**
427
+ * 下载一件入站媒体并落盘,返回路径;**没开这个功能、或下载失败,都返回空串**。
428
+ * 失败会往日志里写一行原因 —— "没开"和"开了但失败了"必须分得开,否则出事时两种证据全无。
429
+ */
430
+ private async inboundMediaPath(
431
+ item: any,
432
+ mediaType: InboundMediaType,
433
+ index: number,
434
+ messageId: string,
435
+ ): Promise<string> {
436
+ if (!this.mediaDownloadEnabled()) return ''
437
+ const result = await saveInboundMedia({
438
+ item,
439
+ mediaType,
440
+ index,
441
+ messageId,
442
+ dir: this.mediaDirOverride || wechatMediaDir(),
443
+ serverFileName: mediaType === 'file' ? item?.file_item?.file_name : undefined,
444
+ download: (param, key) => this.client.downloadMedia(param, key),
445
+ })
446
+ if (!result.path) {
447
+ console.warn(`入站媒体未落地(${mediaType}):${result.reason}`)
448
+ return ''
449
+ }
450
+ return result.path
451
+ }
452
+
453
+ private async parseInboundMessage(msg: any): Promise<WechatInboundMessage | null> {
413
454
  const fromUserId: string = msg.from_user_id || ''
414
455
  if (!fromUserId) return null
415
456
  const routing = resolveInboundRouting(msg, String(configService.get('wechatOcAccountId') || ''))
@@ -423,19 +464,29 @@ export class WechatMessageService {
423
464
 
424
465
  const components: WechatMessageComponent[] = []
425
466
  const itemList: any[] = msg.item_list || []
467
+ const messageId: string = msg.client_id || uuidHex()
468
+ let mediaIndex = 0
426
469
 
427
470
  for (const item of itemList) {
428
471
  const itemType = item.type as number
429
472
  if (itemType === 1 && item.text_item) {
430
473
  components.push({ type: 'plain', text: item.text_item.text || '' })
431
- } else if (itemType === 2 && item.image_item) {
432
- components.push({ type: 'image', filePath: '' }) // image download deferred
433
- } else if (itemType === 3 && item.voice_item) {
434
- components.push({ type: 'record', filePath: '' })
435
- } else if (itemType === 4 && item.file_item) {
436
- components.push({ type: 'file', name: item.file_item.file_name || '', filePath: '' })
437
- } else if (itemType === 5 && item.video_item) {
438
- components.push({ type: 'video', filePath: '' })
474
+ continue
475
+ }
476
+ const mediaType = mediaTypeOf(itemType)
477
+ // 缺节点就整项跳过(与既有行为一致:形状不对的项不进 components)
478
+ if (!mediaType || !item[`${mediaType === 'record' ? 'voice' : mediaType}_item`]) continue
479
+ mediaIndex += 1
480
+ // 落盘(默认关,关着时这里不做任何 IO)
481
+ const filePath = await this.inboundMediaPath(item, mediaType, mediaIndex, messageId)
482
+ if (mediaType === 'image') {
483
+ components.push({ type: 'image', filePath })
484
+ } else if (mediaType === 'record') {
485
+ components.push({ type: 'record', filePath })
486
+ } else if (mediaType === 'file') {
487
+ components.push({ type: 'file', name: item.file_item.file_name || '', filePath })
488
+ } else {
489
+ components.push({ type: 'video', filePath })
439
490
  }
440
491
  }
441
492
 
@@ -454,7 +505,7 @@ export class WechatMessageService {
454
505
  const messageStr = textComponents.map(c => (c as { type: 'plain'; text: string }).text).join('')
455
506
 
456
507
  return {
457
- messageId: msg.client_id || uuidHex(),
508
+ messageId,
458
509
  fromUserId,
459
510
  ...routing,
460
511
  senderNickname: msg.from_user_id || '',
package/src/types.ts CHANGED
@@ -65,6 +65,10 @@ export interface WechatOCConfig {
65
65
  apiTimeoutMs?: number
66
66
  typingKeepaliveIntervalS?: number
67
67
  typingTicketTtlS?: number
68
+ /** 入站媒体是否落地(缺省=读 `wechatMediaDownload` 配置,默认关)。见 src/services/wechatMedia.ts */
69
+ mediaDownload?: boolean | string
70
+ /** 入站媒体落地目录(缺省=output/wechat-media/,可被 WEFLOW_WECHAT_MEDIA_DIR 改) */
71
+ mediaDir?: string
68
72
  }
69
73
 
70
74
  export interface WechatLoginSession {
@@ -38,7 +38,7 @@ function candidates(): string[] {
38
38
 
39
39
  function canRun(cmd: string): boolean {
40
40
  try {
41
- execFileSync(cmd, ['--version'], { stdio: 'ignore', timeout: 5000 })
41
+ execFileSync(cmd, ['--version'], { windowsHide: true, stdio: 'ignore', timeout: 5000 })
42
42
  return true
43
43
  } catch {
44
44
  return false
@@ -47,7 +47,7 @@ function canRun(cmd: string): boolean {
47
47
 
48
48
  function hasSqlcipher(cmd: string): boolean {
49
49
  try {
50
- execFileSync(cmd, ['-c', 'import sqlcipher3'], { stdio: 'ignore', timeout: 10000 })
50
+ execFileSync(cmd, ['-c', 'import sqlcipher3'], { windowsHide: true, stdio: 'ignore', timeout: 10000 })
51
51
  return true
52
52
  } catch {
53
53
  return false
@@ -1,5 +0,0 @@
1
- /** Replace known WeChat face codes like [害羞] with emoji; unknown codes pass through. */
2
- export declare function convertFaces(text: string | undefined | null): string;
3
- /** Convert faces, then truncate without splitting a surrogate pair. */
4
- export declare function convertFacesTruncated(text: string | undefined | null, max: number): string;
5
- //# sourceMappingURL=wechatEmoji.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wechatEmoji.d.ts","sourceRoot":"","sources":["../../../src/utils/wechatEmoji.ts"],"names":[],"mappings":"AA4BA,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAGpE;AAED,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1F"}
@@ -1,35 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import { dirname, join } from 'path';
3
- import { fileURLToPath } from 'url';
4
- const __filename = fileURLToPath(import.meta.url);
5
- const __dirname = dirname(__filename);
6
- // dist/src/utils -> package root. The table is shared with the Python side.
7
- const DATA_FILE = join(__dirname, '..', '..', '..', 'scripts', 'data', 'wechat_emoji.json');
8
- const faces = {};
9
- try {
10
- const parsed = JSON.parse(readFileSync(DATA_FILE, 'utf8'));
11
- for (const [name, emoji] of Object.entries(parsed)) {
12
- // Entries starting with "_" are documentation, not faces.
13
- if (!name.startsWith('_') && typeof emoji === 'string') {
14
- faces[name] = emoji;
15
- }
16
- }
17
- }
18
- catch {
19
- // Missing table just means faces stay as their [名称] source text.
20
- }
21
- const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
- const names = Object.keys(faces).sort((a, b) => b.length - a.length);
23
- // Longest name first so [左哼哼] can never be shadowed by a shorter prefix.
24
- const pattern = names.length > 0 ? new RegExp(`\\[(${names.map(escapeRe).join('|')})\\]`, 'g') : null;
25
- /** Replace known WeChat face codes like [害羞] with emoji; unknown codes pass through. */
26
- export function convertFaces(text) {
27
- if (!text || !pattern)
28
- return text || '';
29
- return text.replace(pattern, (match, name) => faces[name] ?? match);
30
- }
31
- /** Convert faces, then truncate without splitting a surrogate pair. */
32
- export function convertFacesTruncated(text, max) {
33
- return Array.from(convertFaces(text)).slice(0, max).join('');
34
- }
35
- //# sourceMappingURL=wechatEmoji.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wechatEmoji.js","sourceRoot":"","sources":["../../../src/utils/wechatEmoji.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAA;AAEnC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,4EAA4E;AAC5E,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAA;AAE3F,MAAM,KAAK,GAA2B,EAAE,CAAA;AACxC,IAAI,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAA4B,CAAA;IACrF,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,0DAA0D;QAC1D,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvD,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;QACrB,CAAC;IACH,CAAC;AACH,CAAC;AAAC,MAAM,CAAC;IACP,iEAAiE;AACnE,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;AACtF,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;AACpE,yEAAyE;AACzE,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAErG,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,IAA+B;IAC1D,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,IAAI,EAAE,CAAA;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAA;AAC7E,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,qBAAqB,CAAC,IAA+B,EAAE,GAAW;IAChF,OAAO,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAC9D,CAAC"}