wechat-to-anything 0.5.1 → 0.5.3

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.md CHANGED
@@ -112,7 +112,9 @@ npx wechat-to-anything \
112
112
 
113
113
  多 Agent 模式下回复自动带 `[agentName]` 前缀标识来源。每个用户独立维护默认 Agent。
114
114
 
115
- **图片支持**:消息格式遵循 [OpenAI Vision API](https://platform.openai.com/docs/guides/vision),`content` 为数组:
115
+ ## 多媒体格式
116
+
117
+ **图片(微信 → Agent)**:遵循 [OpenAI Vision API](https://platform.openai.com/docs/guides/vision),`content` 为数组:
116
118
 
117
119
  ```json
118
120
  {
@@ -126,9 +128,21 @@ npx wechat-to-anything \
126
128
  }
127
129
  ```
128
130
 
129
- **图片回复**:Agent 回复中包含 markdown 图片 `![desc](https://...)` 会自动作为图片消息发到微信。
131
+ **图片(Agent 微信)**:回复中包含 `![desc](https://...)` 自动发图。
132
+
133
+ **语音(Agent → 微信)**:回复中包含 `[audio:path 或 URL]` 自动发语音气泡。支持 MP3、WAV、OGG 等。需要 `ffmpeg` 和 `pip install pilk`。
134
+
135
+ ```python
136
+ @app.post("/v1/chat/completions")
137
+ def chat(request):
138
+ message = request.json["messages"][-1]["content"]
139
+ audio_path = your_tts(message) # → /tmp/reply.mp3
140
+ reply = f"[audio:{audio_path}]\n这是文字版内容"
141
+ return {"choices": [{"message": {"role": "assistant", "content": reply}}]}
142
+ ```
143
+
144
+ > 完整语音发送示例见 [examples/voice-test.mjs](examples/voice-test.mjs)
130
145
 
131
- **语音回复**:Agent 回复中包含 `[audio:path 或 URL]`,桥会自动转为微信语音消息。支持本地路径和 HTTP URL,格式支持 MP3、WAV、OGG 等。
132
146
  ## 凭证
133
147
 
134
148
  登录凭证保存在 `~/.wechat-to-anything/credentials.json`,删除即可重新登录。
package/cli/bridge.mjs CHANGED
@@ -320,20 +320,37 @@ export async function start(agents, defaultAgent) {
320
320
  const pcmSize = statSync("/tmp/wxta_audio.pcm").size;
321
321
  const durationMs = Math.round((pcmSize / 32000) * 1000);
322
322
 
323
- // CDN 上传 + 发送语音
323
+ // CDN 上传 + 发送语音(与"语音测试"相同格式)
324
324
  const cdn = await uploadToCdn("/tmp/wxta_audio.silk", from, creds.token, 4);
325
325
  const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
326
-
327
- const voiceBody = {
328
- to_user: from,
329
- voice_item: {
330
- voice_url: cdn.file_url, aes_buf_key: aesKeyB64,
331
- file_id: cdn.file_id, voice_length: durationMs, voice_format: 4,
326
+ const crypto = await import("node:crypto");
327
+
328
+ const body = JSON.stringify({
329
+ msg: {
330
+ from_user_id: "", to_user_id: from,
331
+ client_id: crypto.randomUUID(),
332
+ message_type: 2, message_state: 2,
333
+ item_list: [{
334
+ type: 3,
335
+ voice_item: {
336
+ media: {
337
+ encrypt_query_param: cdn.downloadParam,
338
+ aes_key: aesKeyB64,
339
+ },
340
+ encode_type: 4,
341
+ bits_per_sample: 16,
342
+ sample_rate: 16000,
343
+ playtime: durationMs,
344
+ },
345
+ }],
346
+ context_token: contextToken,
332
347
  },
333
- };
334
- const headers = buildHeaders(creds.token, contextToken);
335
- await fetch(`${baseUrl}/cgi-bin/mmchatgpt-wechat/sendvoicemessage`, {
336
- method: "POST", headers, body: JSON.stringify(voiceBody),
348
+ base_info: {},
349
+ });
350
+ await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
351
+ method: "POST",
352
+ headers: buildHeaders(creds.token, body),
353
+ body,
337
354
  });
338
355
  console.log(pc.green(`→ [语音] 已发送 (${durationMs}ms)`));
339
356
  if (textPart) await sendMessage(creds.token, from, agentTag + textPart, contextToken);
package/cli/weixin.mjs CHANGED
@@ -187,9 +187,24 @@ export async function sendMessage(token, to, text, contextToken) {
187
187
  }
188
188
 
189
189
  /**
190
- * 发送图片消息(通过 URL 直接发送)
190
+ * 发送图片消息(下载 CDN 上传 → 发送)
191
191
  */
192
192
  export async function sendImageByUrl(token, to, contextToken, imageUrl) {
193
+ const { writeFile: wf } = await import("node:fs/promises");
194
+
195
+ // 下载图片
196
+ const resp = await fetch(imageUrl);
197
+ if (!resp.ok) throw new Error(`图片下载失败: ${resp.status}`);
198
+ const buf = Buffer.from(await resp.arrayBuffer());
199
+ const tmpPath = "/tmp/wxta_image_send.jpg";
200
+ await wf(tmpPath, buf);
201
+
202
+ // CDN 上传 (mediaType=1 = IMAGE)
203
+ const { uploadToCdn } = await import("./cdn.mjs");
204
+ const cdn = await uploadToCdn(tmpPath, to, token, 1);
205
+ const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
206
+
207
+ // 发送
193
208
  await apiPost(
194
209
  "ilink/bot/sendmessage",
195
210
  {
@@ -202,7 +217,10 @@ export async function sendImageByUrl(token, to, contextToken, imageUrl) {
202
217
  item_list: [{
203
218
  type: 2, // IMAGE
204
219
  image_item: {
205
- url: imageUrl,
220
+ media: {
221
+ encrypt_query_param: cdn.downloadParam,
222
+ aes_key: aesKeyB64,
223
+ },
206
224
  },
207
225
  }],
208
226
  context_token: contextToken,
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from "fs";
2
+ import { homedir } from "os";
3
+
4
+ // 凭证
5
+ const creds = JSON.parse(readFileSync(homedir() + "/.wechat-to-anything/credentials.json", "utf-8"));
6
+ const token = creds.token;
7
+ const to = creds.userId;
8
+
9
+ // 测试图片 URL(可替换为任意图片地址)
10
+ const imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png";
11
+
12
+ // 获取 contextToken
13
+ const { getUpdates, buildHeaders, BASE_URL } = await import("../cli/weixin.mjs");
14
+ const msgs = await getUpdates(token);
15
+ const contextToken = msgs?.context_token || "";
16
+
17
+ // 发送图片
18
+ console.log("发送图片:", imageUrl.slice(0, 60) + "...");
19
+ const { sendImageByUrl } = await import("../cli/weixin.mjs");
20
+ await sendImageByUrl(token, to, contextToken, imageUrl);
21
+ console.log("✅ 图片已发送");
@@ -0,0 +1,62 @@
1
+ import { readFileSync, statSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { execSync } from "child_process";
4
+ import crypto from "crypto";
5
+
6
+ // 凭证
7
+ const creds = JSON.parse(readFileSync(homedir() + "/.wechat-to-anything/credentials.json", "utf-8"));
8
+ const token = creds.token;
9
+ const to = creds.userId;
10
+
11
+ // 1. MP3 → PCM → SILK
12
+ console.log("1. 转码 MP3 → PCM → SILK");
13
+ execSync(`ffmpeg -y -i /tmp/wxta_test_voice.mp3 -ar 16000 -ac 1 -f s16le /tmp/wxta_test_voice.pcm 2>/dev/null`);
14
+ execSync(`python3 -c "import pilk; pilk.encode('/tmp/wxta_test_voice.pcm', '/tmp/wxta_test_voice.silk', pcm_rate=16000, tencent=True)"`);
15
+ const pcmSize = statSync("/tmp/wxta_test_voice.pcm").size;
16
+ const durationMs = Math.round((pcmSize / 32000) * 1000);
17
+ console.log(` SILK ok, duration=${durationMs}ms`);
18
+
19
+ // 2. CDN 上传
20
+ console.log("2. CDN 上传");
21
+ const { uploadToCdn } = await import("../cli/cdn.mjs");
22
+ const cdn = await uploadToCdn("/tmp/wxta_test_voice.silk", to, token, 4);
23
+ const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
24
+ console.log(` CDN ok, downloadParam=${cdn.downloadParam.slice(0, 30)}...`);
25
+
26
+ // 3. 获取 contextToken
27
+ const { getUpdates, buildHeaders, BASE_URL } = await import("../cli/weixin.mjs");
28
+ const msgs = await getUpdates(token);
29
+ const contextToken = msgs?.context_token || "";
30
+
31
+ // 4. 发送语音(与"语音测试"完全一致的格式)
32
+ console.log("3. 发送语音");
33
+ const body = JSON.stringify({
34
+ msg: {
35
+ from_user_id: "", to_user_id: to,
36
+ client_id: crypto.randomUUID(),
37
+ message_type: 2, message_state: 2,
38
+ item_list: [{
39
+ type: 3,
40
+ voice_item: {
41
+ media: {
42
+ encrypt_query_param: cdn.downloadParam,
43
+ aes_key: aesKeyB64,
44
+ },
45
+ encode_type: 4,
46
+ bits_per_sample: 16,
47
+ sample_rate: 16000,
48
+ playtime: durationMs,
49
+ },
50
+ }],
51
+ context_token: contextToken,
52
+ },
53
+ base_info: {},
54
+ });
55
+
56
+ const res = await fetch(`${BASE_URL}/ilink/bot/sendmessage`, {
57
+ method: "POST",
58
+ headers: buildHeaders(token, body),
59
+ body,
60
+ });
61
+ const text = await res.text();
62
+ console.log(` 结果: ${res.status} ${text}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-to-anything",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {