wechat-to-anything 0.6.5 → 0.6.6

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/README.en.md CHANGED
@@ -12,7 +12,7 @@
12
12
  </p>
13
13
 
14
14
  <p align="center">
15
- <a href="#quick-start">Quick Start</a> · <a href="#full-multimodal-matrix">Multimodal</a> · <a href="#media-protocol">Media Protocol</a> · <a href="#multi-agent-mode">Multi-Agent</a> · <a href="#bring-your-own-agent">Custom Agent</a>
15
+ <a href="#quick-start">Quick Start</a> · <a href="#full-multimodal-matrix">Multimodal</a> · <a href="#media-protocol">Media Protocol</a> · <a href="#multi-agent-mode">Multi-Agent</a> · <a href="#proactive-send-api">Send API</a> · <a href="#bring-your-own-agent">Custom Agent</a>
16
16
  </p>
17
17
 
18
18
  <p align="center">
@@ -38,6 +38,7 @@
38
38
  - 📡 **Full multimodal** — Text, images, voice, video, files — bidirectional
39
39
  - 🤖 **Multi-Agent** — Connect multiple Agents simultaneously, route with `@` prefix
40
40
  - ⌨️ **Typing indicator** — Shows "typing..." while Agent is thinking
41
+ - 📤 **Proactive Send API** — Agent can push multiple messages to simulate human typing rhythm
41
42
 
42
43
  ### Full Multimodal Matrix
43
44
 
@@ -152,6 +153,34 @@ npx wechat-to-anything \
152
153
  | `@list` | List all Agents |
153
154
  | `@switch gemini` | Switch default |
154
155
 
156
+ ## Proactive Send API
157
+
158
+ Bridge starts an HTTP API on `localhost:9099`. Agents can proactively push multiple messages (simulating human typing rhythm):
159
+
160
+ ```bash
161
+ curl -X POST http://localhost:9099/api/send \
162
+ -H "Content-Type: application/json" \
163
+ -d '{"to": "user_id", "content": "Hmm..."}'
164
+ ```
165
+
166
+ - `to` — WeChat user ID (bridge passes this via the `user` field when calling agents)
167
+ - `content` — Same formats as agent responses (plain text, `![](url)`, `[audio:path]`, etc.)
168
+ - Use `--port PORT` to customize the port
169
+
170
+ **Use case**: Agent splits one reply into multiple segments with controlled timing:
171
+
172
+ ```python
173
+ import requests, time
174
+ def send(to, text):
175
+ requests.post("http://localhost:9099/api/send", json={"to": to, "content": text})
176
+
177
+ send(user_id, "Hmm...")
178
+ time.sleep(1.5)
179
+ send(user_id, "Let me think")
180
+ time.sleep(2)
181
+ # Final segment returned as normal response
182
+ ```
183
+
155
184
  ## Credentials
156
185
 
157
186
  Login credentials are saved in `~/.wechat-to-anything/credentials.json`. Delete to re-login.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  </p>
13
13
 
14
14
  <p align="center">
15
- <a href="#快速开始">快速开始</a> · <a href="#全模态支持矩阵">全模态</a> · <a href="#多媒体协议">多媒体协议</a> · <a href="#多-agent-模式">多 Agent</a> · <a href="#接入自己的-agent">自定义 Agent</a>
15
+ <a href="#快速开始">快速开始</a> · <a href="#全模态支持矩阵">全模态</a> · <a href="#多媒体协议">多媒体协议</a> · <a href="#多-agent-模式">多 Agent</a> · <a href="#主动发送-api">主动发送</a> · <a href="#接入自己的-agent">自定义 Agent</a>
16
16
  </p>
17
17
 
18
18
  <p align="center">
@@ -38,6 +38,7 @@
38
38
  - 📡 **全模态** — 文本、图片、语音、视频、文件,双向全覆盖
39
39
  - 🤖 **多 Agent** — 同时接入多个 Agent,`@` 路由切换
40
40
  - ⌨️ **打字指示器** — Agent 思考时显示"对方正在输入"
41
+ - 📤 **主动发送 API** — Agent 可推送多条消息,模拟真人打字节奏
41
42
 
42
43
  ### 全模态支持矩阵
43
44
 
@@ -152,6 +153,34 @@ npx wechat-to-anything \
152
153
  | `@list` | 查看所有 Agent |
153
154
  | `@切换 gemini` | 切换默认 |
154
155
 
156
+ ## 主动发送 API
157
+
158
+ Bridge 启动时会在 `localhost:9099` 暴露 HTTP API,Agent 可主动推送多条消息(模拟真人打字节奏):
159
+
160
+ ```bash
161
+ curl -X POST http://localhost:9099/api/send \
162
+ -H "Content-Type: application/json" \
163
+ -d '{"to": "user_id", "content": "嗯……"}'
164
+ ```
165
+
166
+ - `to` — 微信用户 ID(bridge 调 agent 时通过 `user` 字段传入)
167
+ - `content` — 支持和 Agent 回复相同的格式(纯文本、`![](url)`、`[audio:path]` 等)
168
+ - 用 `--port PORT` 自定义端口
169
+
170
+ **用途**:Agent 对一条消息可分多段回复,控制发送间隔:
171
+
172
+ ```python
173
+ import requests, time
174
+ def send(to, text):
175
+ requests.post("http://localhost:9099/api/send", json={"to": to, "content": text})
176
+
177
+ send(user_id, "嗯……")
178
+ time.sleep(1.5)
179
+ send(user_id, "让我想想")
180
+ time.sleep(2)
181
+ # 最后一段作为正常 response 返回
182
+ ```
183
+
155
184
  ## 凭证
156
185
 
157
186
  登录凭证保存在 `~/.wechat-to-anything/credentials.json`,删除即可重新登录。
package/bin/cli.mjs CHANGED
@@ -32,6 +32,12 @@ ${pc.bold("参数:")}
32
32
  --openclaw ${pc.dim("内置 OpenClaw(需先 npm i -g openclaw)")}
33
33
  --agent ${pc.dim("name=url")} ${pc.dim("注册自定义 Agent")}
34
34
  --default ${pc.dim("name")} ${pc.dim("设置默认 Agent")}
35
+ --port ${pc.dim("PORT")} ${pc.dim("API 端口(默认 9099),暴露 POST /api/send")}
36
+
37
+ ${pc.bold("API:")}
38
+ POST http://localhost:PORT/api/send
39
+ ${pc.dim('{ "to": "user_id", "content": "消息内容" }')}
40
+ ${pc.dim("Agent 可主动推送多条消息,模拟真人节奏")}
35
41
 
36
42
  ${pc.dim("Docs: https://github.com/kellyvv/wechat-to-anything")}
37
43
  `);
@@ -40,6 +46,7 @@ ${pc.dim("Docs: https://github.com/kellyvv/wechat-to-anything")}
40
46
 
41
47
  // 解析参数
42
48
  let i = 0;
49
+ let port = 9099;
43
50
  while (i < args.length) {
44
51
  if (args[i] === "--codex") {
45
52
  agents.set("codex", "cli://codex");
@@ -71,6 +78,13 @@ while (i < args.length) {
71
78
  } else if (args[i] === "--default" && args[i + 1]) {
72
79
  defaultAgent = args[i + 1].toLowerCase();
73
80
  i += 2;
81
+ } else if (args[i] === "--port" && args[i + 1]) {
82
+ port = parseInt(args[i + 1], 10);
83
+ if (isNaN(port)) {
84
+ console.error(pc.red(`无效的端口号: ${args[i + 1]}`));
85
+ process.exit(1);
86
+ }
87
+ i += 2;
74
88
  } else if (!args[i].startsWith("--")) {
75
89
  if (!args[i].startsWith("acp://")) {
76
90
  try { new URL(args[i]); } catch {
@@ -114,7 +128,7 @@ if (agents.size === 1 && agents.has("default")) {
114
128
  }
115
129
  console.log();
116
130
 
117
- import("../cli/bridge.mjs").then((mod) => mod.start(agents, defaultAgent)).catch((err) => {
131
+ import("../cli/bridge.mjs").then((mod) => mod.start(agents, defaultAgent, { port })).catch((err) => {
118
132
  console.error(pc.red(err.message));
119
133
  process.exit(1);
120
134
  });
package/cli/bridge.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import pc from "picocolors";
2
+ import { createServer } from "node:http";
2
3
  import {
3
4
  loadCredentials, loginWithQR, getUpdates,
4
5
  sendMessage, sendImageByUrl, sendVideoByUrl,
@@ -14,7 +15,7 @@ import { stripMarkdown } from "./markdown.mjs";
14
15
  * 启动桥:WeChat ilinkai API ←→ Agent HTTP
15
16
  * 支持文本 + 图片 + 语音 + 文件,双向
16
17
  */
17
- export async function start(agents, defaultAgent) {
18
+ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
18
19
  // 兼容旧的单 URL 调用
19
20
  if (typeof agents === "string") {
20
21
  const url = agents;
@@ -103,6 +104,177 @@ export async function start(agents, defaultAgent) {
103
104
  const pendingImages = new Map(); // userId → { base64, timestamp }
104
105
  const IMAGE_BUFFER_TTL = 5 * 60_000; // 5 min 过期
105
106
 
107
+ // per-user contextToken 缓存(供 /api/send 使用)
108
+ const userContextTokens = new Map(); // userId → contextToken
109
+
110
+ /**
111
+ * 统一发送内容(纯文本 / 图片 / 语音 / 视频 / 文件)
112
+ * 复用 Agent 回复的多媒体协议格式
113
+ */
114
+ async function sendContent(to, content, tag = "") {
115
+ const ct = userContextTokens.get(to) || "";
116
+
117
+ // 检查回复是否包含 [audio:path/url]
118
+ const audioMatch = content.match(/\[audio:(.*?)\]/);
119
+ // 检查回复是否包含图片(markdown 格式,支持 URL 和 data URI)
120
+ const imageMatch = content.match(/!\[.*?\]\(((?:https?:\/\/|data:image\/|\/).+?)\)/);
121
+ // 检查回复是否包含 [video:path/url]
122
+ const videoMatch = content.match(/\[video:(.*?)\]/);
123
+ // 检查回复是否包含 [file:path/url]
124
+ const fileMatch = content.match(/\[file:(.*?)\]/);
125
+
126
+ if (audioMatch) {
127
+ const audioSrc = audioMatch[1];
128
+ const textPart = content.replace(/\[audio:.*?\]/g, "").trim();
129
+ console.log(pc.green(`→ [send] [语音] ${audioSrc.slice(0, 60)}`));
130
+ try {
131
+ const { execSync } = await import("node:child_process");
132
+ const { statSync, writeFileSync } = await import("node:fs");
133
+ const { uploadToCdn } = await import("./cdn.mjs");
134
+ const { buildHeaders, BASE_URL: baseUrl } = await import("./weixin.mjs");
135
+
136
+ let audioFile = audioSrc;
137
+ if (audioSrc.startsWith("http://") || audioSrc.startsWith("https://")) {
138
+ const resp = await fetch(audioSrc);
139
+ if (!resp.ok) throw new Error(`下载失败: ${resp.status}`);
140
+ writeFileSync("/tmp/wxta_audio_in.mp3", Buffer.from(await resp.arrayBuffer()));
141
+ audioFile = "/tmp/wxta_audio_in.mp3";
142
+ }
143
+
144
+ execSync(`ffmpeg -y -i "${audioFile}" -ar 16000 -ac 1 -f s16le /tmp/wxta_audio.pcm 2>/dev/null`);
145
+ execSync(`python3 -c "import pilk; pilk.encode('/tmp/wxta_audio.pcm', '/tmp/wxta_audio.silk', pcm_rate=16000, tencent=True)"`);
146
+ const pcmSize = statSync("/tmp/wxta_audio.pcm").size;
147
+ const durationMs = Math.round((pcmSize / 32000) * 1000);
148
+
149
+ const cdn = await uploadToCdn("/tmp/wxta_audio.silk", to, creds.token, 4);
150
+ const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
151
+ const crypto = await import("node:crypto");
152
+
153
+ const body = JSON.stringify({
154
+ msg: {
155
+ from_user_id: "", to_user_id: to,
156
+ client_id: crypto.randomUUID(),
157
+ message_type: 2, message_state: 2,
158
+ item_list: [{
159
+ type: 3,
160
+ voice_item: {
161
+ media: { encrypt_query_param: cdn.downloadParam, aes_key: aesKeyB64 },
162
+ encode_type: 4, bits_per_sample: 16, sample_rate: 16000, playtime: durationMs,
163
+ },
164
+ }],
165
+ context_token: ct,
166
+ },
167
+ base_info: {},
168
+ });
169
+ await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
170
+ method: "POST", headers: buildHeaders(creds.token, body), body,
171
+ });
172
+ console.log(pc.green(`→ [语音] 已发送 (${durationMs}ms)`));
173
+ if (textPart) await sendMessage(creds.token, to, tag + stripMarkdown(textPart), ct);
174
+ } catch (err) {
175
+ console.error(pc.red(` 语音发送失败: ${err.message}`));
176
+ await sendMessage(creds.token, to, tag + content.replace(/\[audio:.*?\]/g, "").trim() || content, ct);
177
+ }
178
+ } else if (imageMatch) {
179
+ const imageUrl = imageMatch[1];
180
+ const textPart = content.replace(/!\[.*?\]\(((?:https?:\/\/|data:image\/|\/).+?)\)/g, "").trim();
181
+ console.log(pc.green(`→ [send] [图片] ${imageUrl.slice(0, 60)}`));
182
+ try {
183
+ if (textPart) await sendMessage(creds.token, to, tag + stripMarkdown(textPart), ct);
184
+ await sendImageByUrl(creds.token, to, ct, imageUrl);
185
+ } catch (err) {
186
+ console.error(pc.red(` 图片发送失败: ${err.message}`));
187
+ await sendMessage(creds.token, to, tag + content, ct);
188
+ }
189
+ } else if (videoMatch) {
190
+ const videoSrc = videoMatch[1];
191
+ const textPart = content.replace(/\[video:.*?\]/g, "").trim();
192
+ console.log(pc.green(`→ [send] [视频] ${videoSrc.slice(0, 60)}`));
193
+ try {
194
+ if (textPart) await sendMessage(creds.token, to, tag + stripMarkdown(textPart), ct);
195
+ await sendVideoByUrl(creds.token, to, ct, videoSrc);
196
+ } catch (err) {
197
+ console.error(pc.red(` 视频发送失败: ${err.message}`));
198
+ await sendMessage(creds.token, to, tag + stripMarkdown(content), ct);
199
+ }
200
+ } else if (fileMatch) {
201
+ const fileSrc = fileMatch[1];
202
+ const textPart = content.replace(/\[file:.*?\]/g, "").trim();
203
+ const fileName = fileSrc.split("/").pop() || "file";
204
+ console.log(pc.green(`→ [send] [文件] ${fileSrc.slice(0, 60)}`));
205
+ try {
206
+ const { writeFileSync, unlinkSync } = await import("node:fs");
207
+ const { tmpdir } = await import("node:os");
208
+ const { join } = await import("node:path");
209
+ const resp = await fetch(fileSrc);
210
+ if (!resp.ok) throw new Error(`file download failed: ${resp.status}`);
211
+ const buf = Buffer.from(await resp.arrayBuffer());
212
+ const tmpPath = join(tmpdir(), `wx-file-${Date.now()}-${fileName}`);
213
+ writeFileSync(tmpPath, buf);
214
+ try {
215
+ const uploaded = await uploadToCdn(tmpPath, to, creds.token, 3);
216
+ const { sendFileMessage } = await import("./weixin.mjs");
217
+ await sendFileMessage(creds.token, to, ct, uploaded, fileName);
218
+ if (textPart) await sendMessage(creds.token, to, tag + stripMarkdown(textPart), ct);
219
+ } finally {
220
+ try { unlinkSync(tmpPath); } catch {}
221
+ }
222
+ } catch (err) {
223
+ console.error(pc.red(` 文件发送失败: ${err.message}`));
224
+ await sendMessage(creds.token, to, tag + stripMarkdown(content), ct);
225
+ }
226
+ } else {
227
+ // 纯文本
228
+ console.log(pc.green(`→ [send] ${content.slice(0, 80)}${content.length > 80 ? "..." : ""}`));
229
+ await sendMessage(creds.token, to, tag + stripMarkdown(content), ct);
230
+ }
231
+ }
232
+
233
+ // ─── HTTP API Server (/api/send) ────────────────────────────────
234
+ const httpServer = createServer(async (req, res) => {
235
+ // CORS
236
+ res.setHeader("Access-Control-Allow-Origin", "*");
237
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
238
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
239
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
240
+
241
+ if (req.method === "POST" && req.url === "/api/send") {
242
+ let body = "";
243
+ for await (const chunk of req) body += chunk;
244
+ try {
245
+ const { to, content } = JSON.parse(body);
246
+ if (!to || !content) {
247
+ res.writeHead(400, { "Content-Type": "application/json" });
248
+ res.end(JSON.stringify({ error: "missing 'to' or 'content'" }));
249
+ return;
250
+ }
251
+ console.log(pc.cyan(`← [API] → ${to.slice(0, 12)}...: ${content.slice(0, 60)}`));
252
+ await sendContent(to, content);
253
+ res.writeHead(200, { "Content-Type": "application/json" });
254
+ res.end("{}");
255
+ } catch (err) {
256
+ console.error(pc.red(` /api/send 错误: ${err.message}`));
257
+ res.writeHead(500, { "Content-Type": "application/json" });
258
+ res.end(JSON.stringify({ error: err.message }));
259
+ }
260
+ return;
261
+ }
262
+
263
+ res.writeHead(404, { "Content-Type": "application/json" });
264
+ res.end(JSON.stringify({ error: "not found" }));
265
+ });
266
+
267
+ httpServer.on("error", (err) => {
268
+ if (err.code === "EADDRINUSE") {
269
+ console.warn(pc.yellow(`⚠️ 端口 ${port} 已被占用,API 未启动(bridge 继续运行)`));
270
+ } else {
271
+ console.error(pc.red(`API 服务器错误: ${err.message}`));
272
+ }
273
+ });
274
+ httpServer.listen(port, () => {
275
+ console.log(pc.green(`📡 API 已启动: http://localhost:${port}/api/send`));
276
+ });
277
+
106
278
  const loop = async () => {
107
279
  while (true) {
108
280
  try {
@@ -117,6 +289,9 @@ export async function start(agents, defaultAgent) {
117
289
  const contextToken = msg.context_token || "";
118
290
  if (!from) continue;
119
291
 
292
+ // 缓存 contextToken 供 /api/send 使用
293
+ if (contextToken) userContextTokens.set(from, contextToken);
294
+
120
295
  const text = extractText(msg);
121
296
  const media = extractMedia(msg);
122
297
 
@@ -328,135 +503,7 @@ export async function start(agents, defaultAgent) {
328
503
  const reply = await callAgentAuto(agentUrl, agentMessages, from);
329
504
  if (typing) typing.onIdle();
330
505
  const agentTag = multiMode ? `[${targetAgent}] ` : "";
331
-
332
- // 检查回复是否包含 [audio:path/url]
333
- const audioMatch = reply.match(/\[audio:(.*?)\]/);
334
- // 检查回复是否包含图片(markdown 格式,支持 URL 和 data URI)
335
- const imageMatch = reply.match(/!\[.*?\]\(((?:https?:\/\/|data:image\/)[^\s)]+)\)/);
336
- // 检查回复是否包含 [video:path/url]
337
- const videoMatch = reply.match(/\[video:(.*?)\]/);
338
- // 检查回复是否包含 [file:path/url]
339
- const fileMatch = reply.match(/\[file:(.*?)\]/);
340
-
341
- if (audioMatch) {
342
- const audioSrc = audioMatch[1];
343
- const textPart = reply.replace(/\[audio:.*?\]/g, "").trim();
344
- console.log(pc.green(`→ [${targetAgent}] [语音] ${audioSrc.slice(0, 60)}`));
345
- try {
346
- const { execSync } = await import("node:child_process");
347
- const { statSync, writeFileSync } = await import("node:fs");
348
- const { uploadToCdn } = await import("./cdn.mjs");
349
- const { buildHeaders, BASE_URL: baseUrl } = await import("./weixin.mjs");
350
-
351
- // 下载或使用本地文件
352
- let audioFile = audioSrc;
353
- if (audioSrc.startsWith("http://") || audioSrc.startsWith("https://")) {
354
- const resp = await fetch(audioSrc);
355
- if (!resp.ok) throw new Error(`下载失败: ${resp.status}`);
356
- writeFileSync("/tmp/wxta_audio_in.mp3", Buffer.from(await resp.arrayBuffer()));
357
- audioFile = "/tmp/wxta_audio_in.mp3";
358
- }
359
-
360
- // 转码: audio → PCM(16kHz) → SILK
361
- execSync(`ffmpeg -y -i "${audioFile}" -ar 16000 -ac 1 -f s16le /tmp/wxta_audio.pcm 2>/dev/null`);
362
- execSync(`python3 -c "import pilk; pilk.encode('/tmp/wxta_audio.pcm', '/tmp/wxta_audio.silk', pcm_rate=16000, tencent=True)"`);
363
- const pcmSize = statSync("/tmp/wxta_audio.pcm").size;
364
- const durationMs = Math.round((pcmSize / 32000) * 1000);
365
-
366
- // CDN 上传 + 发送语音(与"语音测试"相同格式)
367
- const cdn = await uploadToCdn("/tmp/wxta_audio.silk", from, creds.token, 4);
368
- const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
369
- const crypto = await import("node:crypto");
370
-
371
- const body = JSON.stringify({
372
- msg: {
373
- from_user_id: "", to_user_id: from,
374
- client_id: crypto.randomUUID(),
375
- message_type: 2, message_state: 2,
376
- item_list: [{
377
- type: 3,
378
- voice_item: {
379
- media: {
380
- encrypt_query_param: cdn.downloadParam,
381
- aes_key: aesKeyB64,
382
- },
383
- encode_type: 4,
384
- bits_per_sample: 16,
385
- sample_rate: 16000,
386
- playtime: durationMs,
387
- },
388
- }],
389
- context_token: contextToken,
390
- },
391
- base_info: {},
392
- });
393
- await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
394
- method: "POST",
395
- headers: buildHeaders(creds.token, body),
396
- body,
397
- });
398
- console.log(pc.green(`→ [语音] 已发送 (${durationMs}ms)`));
399
- if (textPart) await sendMessage(creds.token, from, agentTag + stripMarkdown(textPart), contextToken);
400
- } catch (err) {
401
- console.error(pc.red(` 语音发送失败: ${err.message}`));
402
- await sendMessage(creds.token, from, agentTag + reply.replace(/\[audio:.*?\]/g, "").trim() || reply, contextToken);
403
- }
404
- } else if (imageMatch) {
405
- // Agent 回复了图片 URL → 直接发到微信
406
- const imageUrl = imageMatch[1];
407
- const textPart = reply.replace(/!\[.*?\]\(https?:\/\/[^\s)]+\)/g, "").trim();
408
- console.log(pc.green(`→ [${targetAgent}] [图片] ${imageUrl.slice(0, 60)}`));
409
- try {
410
- if (textPart) await sendMessage(creds.token, from, agentTag + stripMarkdown(textPart), contextToken);
411
- await sendImageByUrl(creds.token, from, contextToken, imageUrl);
412
- } catch (err) {
413
- console.error(pc.red(` 图片发送失败: ${err.message}`));
414
- await sendMessage(creds.token, from, agentTag + reply, contextToken);
415
- }
416
- } else if (videoMatch) {
417
- // Agent 回复了视频 → CDN 上传发到微信
418
- const videoSrc = videoMatch[1];
419
- const textPart = reply.replace(/\[video:.*?\]/g, "").trim();
420
- console.log(pc.green(`→ [${targetAgent}] [视频] ${videoSrc.slice(0, 60)}`));
421
- try {
422
- if (textPart) await sendMessage(creds.token, from, agentTag + stripMarkdown(textPart), contextToken);
423
- await sendVideoByUrl(creds.token, from, contextToken, videoSrc);
424
- } catch (err) {
425
- console.error(pc.red(` 视频发送失败: ${err.message}`));
426
- await sendMessage(creds.token, from, agentTag + stripMarkdown(reply), contextToken);
427
- }
428
- } else if (fileMatch) {
429
- // Agent 回复了文件 → CDN 上传发到微信
430
- const fileSrc = fileMatch[1];
431
- const textPart = reply.replace(/\[file:.*?\]/g, "").trim();
432
- const fileName = fileSrc.split("/").pop() || "file";
433
- console.log(pc.green(`→ [${targetAgent}] [文件] ${fileSrc.slice(0, 60)}`));
434
- try {
435
- const { writeFileSync, unlinkSync } = await import("node:fs");
436
- const { tmpdir } = await import("node:os");
437
- const { join } = await import("node:path");
438
- const resp = await fetch(fileSrc);
439
- if (!resp.ok) throw new Error(`file download failed: ${resp.status}`);
440
- const buf = Buffer.from(await resp.arrayBuffer());
441
- const tmpPath = join(tmpdir(), `wx-file-${Date.now()}-${fileName}`);
442
- writeFileSync(tmpPath, buf);
443
- try {
444
- const uploaded = await uploadToCdn(tmpPath, from, creds.token, 3);
445
- const { sendFileMessage } = await import("./weixin.mjs");
446
- await sendFileMessage(creds.token, from, contextToken, uploaded, fileName);
447
- if (textPart) await sendMessage(creds.token, from, agentTag + stripMarkdown(textPart), contextToken);
448
- } finally {
449
- try { unlinkSync(tmpPath); } catch {}
450
- }
451
- } catch (err) {
452
- console.error(pc.red(` 文件发送失败: ${err.message}`));
453
- await sendMessage(creds.token, from, agentTag + stripMarkdown(reply), contextToken);
454
- }
455
- } else {
456
- // 纯文本回复
457
- console.log(pc.green(`→ [${targetAgent}] ${reply.slice(0, 80)}${reply.length > 80 ? "..." : ""}`));
458
- await sendMessage(creds.token, from, agentTag + stripMarkdown(reply), contextToken);
459
- }
506
+ await sendContent(from, reply, agentTag);
460
507
  } catch (err) {
461
508
  if (typing) typing.onCleanup();
462
509
  console.error(pc.red(` ${targetAgent} 错误: ${err.message}`));
@@ -471,6 +518,7 @@ export async function start(agents, defaultAgent) {
471
518
  };
472
519
 
473
520
  process.on("SIGINT", () => {
521
+ httpServer.close();
474
522
  console.log(pc.dim("\n桥已停止"));
475
523
  process.exit(0);
476
524
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-to-anything",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {