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 +17 -3
- package/cli/bridge.mjs +28 -11
- package/cli/weixin.mjs +20 -2
- package/examples/image-test.mjs +21 -0
- package/examples/voice-test.mjs +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,7 +112,9 @@ npx wechat-to-anything \
|
|
|
112
112
|
|
|
113
113
|
多 Agent 模式下回复自动带 `[agentName]` 前缀标识来源。每个用户独立维护默认 Agent。
|
|
114
114
|
|
|
115
|
-
|
|
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
|
-
|
|
131
|
+
**图片(Agent → 微信)**:回复中包含 `` 自动发图。
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
await fetch(`${baseUrl}/
|
|
336
|
-
method: "POST",
|
|
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
|
-
*
|
|
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
|
-
|
|
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}`);
|