wechat-to-anything 0.6.6 → 0.6.7
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/.claude/settings.local.json +11 -0
- package/cli/bridge.mjs +13 -69
- package/cli/cdn.mjs +7 -7
- package/cli/weixin.mjs +0 -28
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"mcp__filesystem__directory_tree",
|
|
5
|
+
"Bash(find /Users/zxw/AITOOL/wechat-to-anything/examples -name *.mjs ! -path */node_modules/* -exec head -40 {})",
|
|
6
|
+
"Bash(2)",
|
|
7
|
+
"Bash(ls -la /Users/zxw/AITOOL/wechat-to-anything/examples/*/package.json)",
|
|
8
|
+
"mcp__filesystem__read_text_file"
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
}
|
package/cli/bridge.mjs
CHANGED
|
@@ -128,7 +128,7 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
|
|
|
128
128
|
const textPart = content.replace(/\[audio:.*?\]/g, "").trim();
|
|
129
129
|
console.log(pc.green(`→ [send] [语音] ${audioSrc.slice(0, 60)}`));
|
|
130
130
|
try {
|
|
131
|
-
const {
|
|
131
|
+
const { execFileSync } = await import("node:child_process");
|
|
132
132
|
const { statSync, writeFileSync } = await import("node:fs");
|
|
133
133
|
const { uploadToCdn } = await import("./cdn.mjs");
|
|
134
134
|
const { buildHeaders, BASE_URL: baseUrl } = await import("./weixin.mjs");
|
|
@@ -141,8 +141,8 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
|
|
|
141
141
|
audioFile = "/tmp/wxta_audio_in.mp3";
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
execFileSync("ffmpeg", ["-y", "-i", audioFile, "-ar", "16000", "-ac", "1", "-f", "s16le", "/tmp/wxta_audio.pcm"], { stdio: "ignore" });
|
|
145
|
+
execFileSync("python3", ["-c", "import pilk; pilk.encode('/tmp/wxta_audio.pcm', '/tmp/wxta_audio.silk', pcm_rate=16000, tencent=True)"]);
|
|
146
146
|
const pcmSize = statSync("/tmp/wxta_audio.pcm").size;
|
|
147
147
|
const durationMs = Math.round((pcmSize / 32000) * 1000);
|
|
148
148
|
|
|
@@ -240,7 +240,16 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
|
|
|
240
240
|
|
|
241
241
|
if (req.method === "POST" && req.url === "/api/send") {
|
|
242
242
|
let body = "";
|
|
243
|
-
|
|
243
|
+
let bodySize = 0;
|
|
244
|
+
for await (const chunk of req) {
|
|
245
|
+
bodySize += chunk.length;
|
|
246
|
+
if (bodySize > 1_048_576) { // 1MB limit
|
|
247
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
248
|
+
res.end(JSON.stringify({ error: "body too large (max 1MB)" }));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
body += chunk;
|
|
252
|
+
}
|
|
244
253
|
try {
|
|
245
254
|
const { to, content } = JSON.parse(body);
|
|
246
255
|
if (!to || !content) {
|
|
@@ -359,12 +368,6 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
|
|
|
359
368
|
}
|
|
360
369
|
|
|
361
370
|
} else if (media?.type === "voice") {
|
|
362
|
-
// 打印完整 voice_item 结构,用于对照发送格式
|
|
363
|
-
const voiceItem = (msg.item_list || []).find(i => i.type === 3)?.voice_item;
|
|
364
|
-
if (voiceItem) {
|
|
365
|
-
console.log(pc.yellow("📋 收到的 voice_item 完整结构:"));
|
|
366
|
-
console.log(JSON.stringify(voiceItem, null, 2));
|
|
367
|
-
}
|
|
368
371
|
const voiceText = media.voiceText || text;
|
|
369
372
|
if (voiceText) {
|
|
370
373
|
console.log(pc.cyan(`← [微信] ${from}: [语音] ${voiceText.slice(0, 80)}`));
|
|
@@ -399,65 +402,6 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
|
|
|
399
402
|
continue;
|
|
400
403
|
}
|
|
401
404
|
|
|
402
|
-
// === 语音测试触发器 ===
|
|
403
|
-
if (text === "语音测试") {
|
|
404
|
-
console.log(pc.yellow("🎤 语音测试..."));
|
|
405
|
-
try {
|
|
406
|
-
const { execSync } = await import("node:child_process");
|
|
407
|
-
const { statSync } = await import("node:fs");
|
|
408
|
-
const crypto = await import("node:crypto");
|
|
409
|
-
const { buildHeaders, BASE_URL: baseUrl } = await import("./weixin.mjs");
|
|
410
|
-
const { uploadToCdn } = await import("./cdn.mjs");
|
|
411
|
-
|
|
412
|
-
// TTS → MP3 → PCM(16kHz) → SILK
|
|
413
|
-
execSync(`python3 -m edge_tts --text "你好,这是一条AI语音消息测试" --voice zh-CN-XiaoxiaoNeural --write-media /tmp/tts_bridge.mp3`);
|
|
414
|
-
execSync(`ffmpeg -y -i /tmp/tts_bridge.mp3 -ar 16000 -ac 1 -f s16le /tmp/tts_bridge.pcm 2>/dev/null`);
|
|
415
|
-
execSync(`python3 -c "import pilk; pilk.encode('/tmp/tts_bridge.pcm', '/tmp/tts_bridge.silk', pcm_rate=16000, tencent=True)"`);
|
|
416
|
-
const pcmSize = statSync("/tmp/tts_bridge.pcm").size;
|
|
417
|
-
const durationMs = Math.round((pcmSize / 32000) * 1000);
|
|
418
|
-
console.log(pc.dim(` TTS+SILK 完成 (duration=${durationMs}ms)`));
|
|
419
|
-
|
|
420
|
-
// CDN 上传 (mediaType=4 = 语音)
|
|
421
|
-
const cdn = await uploadToCdn("/tmp/tts_bridge.silk", from, creds.token, 4);
|
|
422
|
-
const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
|
|
423
|
-
console.log(pc.dim(` CDN 上传成功 (mediaType=4)`));
|
|
424
|
-
|
|
425
|
-
// 发送语音消息
|
|
426
|
-
const body = JSON.stringify({
|
|
427
|
-
msg: {
|
|
428
|
-
from_user_id: "", to_user_id: from,
|
|
429
|
-
client_id: crypto.randomUUID(),
|
|
430
|
-
message_type: 2, message_state: 2,
|
|
431
|
-
item_list: [{
|
|
432
|
-
type: 3,
|
|
433
|
-
voice_item: {
|
|
434
|
-
media: {
|
|
435
|
-
encrypt_query_param: cdn.downloadParam,
|
|
436
|
-
aes_key: aesKeyB64,
|
|
437
|
-
},
|
|
438
|
-
encode_type: 4,
|
|
439
|
-
bits_per_sample: 16,
|
|
440
|
-
sample_rate: 16000,
|
|
441
|
-
playtime: durationMs,
|
|
442
|
-
},
|
|
443
|
-
}],
|
|
444
|
-
context_token: contextToken,
|
|
445
|
-
},
|
|
446
|
-
base_info: {},
|
|
447
|
-
});
|
|
448
|
-
const res = await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
|
|
449
|
-
method: "POST",
|
|
450
|
-
headers: buildHeaders(creds.token, body),
|
|
451
|
-
body,
|
|
452
|
-
});
|
|
453
|
-
console.log(pc.green(`→ [语音] status: ${res.status}`));
|
|
454
|
-
await sendMessage(creds.token, from, `🎤 语音已发送 (${durationMs}ms)`, contextToken);
|
|
455
|
-
} catch (err) {
|
|
456
|
-
console.error(pc.red(` 语音测试失败: ${err.message}`));
|
|
457
|
-
await sendMessage(creds.token, from, `⚠️ 语音测试失败: ${err.message}`, contextToken);
|
|
458
|
-
}
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
405
|
|
|
462
406
|
// 解析 @agentName 路由
|
|
463
407
|
let targetAgent = userDefaults.get(from) || defaultAgent;
|
package/cli/cdn.mjs
CHANGED
|
@@ -80,7 +80,7 @@ export async function downloadMediaToFile(encryptQueryParam, aesKeyBase64, ext =
|
|
|
80
80
|
*/
|
|
81
81
|
export async function uploadImageWithThumb(filePath, toUserId, token) {
|
|
82
82
|
const { buildHeaders, BASE_URL } = await import("./weixin.mjs");
|
|
83
|
-
const {
|
|
83
|
+
const { execFileSync } = await import("child_process");
|
|
84
84
|
|
|
85
85
|
const plaintext = await readFile(filePath);
|
|
86
86
|
const rawsize = plaintext.length;
|
|
@@ -92,7 +92,7 @@ export async function uploadImageWithThumb(filePath, toUserId, token) {
|
|
|
92
92
|
// 生成缩略图
|
|
93
93
|
const thumbPath = `/tmp/wxta_thumb_${Date.now()}.jpg`;
|
|
94
94
|
try {
|
|
95
|
-
|
|
95
|
+
execFileSync("sips", ["--resampleWidth", "120", filePath, "--out", thumbPath], { stdio: "ignore" });
|
|
96
96
|
} catch {
|
|
97
97
|
// sips 失败时用原图当缩略图
|
|
98
98
|
await writeFile(thumbPath, plaintext);
|
|
@@ -102,7 +102,7 @@ export async function uploadImageWithThumb(filePath, toUserId, token) {
|
|
|
102
102
|
// 获取缩略图尺寸
|
|
103
103
|
let thumbWidth = 120, thumbHeight = 120;
|
|
104
104
|
try {
|
|
105
|
-
const sipsOut =
|
|
105
|
+
const sipsOut = execFileSync("sips", ["-g", "pixelWidth", "-g", "pixelHeight", thumbPath], { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
106
106
|
const wm = sipsOut.match(/pixelWidth:\s*(\d+)/);
|
|
107
107
|
const hm = sipsOut.match(/pixelHeight:\s*(\d+)/);
|
|
108
108
|
if (wm) thumbWidth = parseInt(wm[1]);
|
|
@@ -183,7 +183,7 @@ export async function uploadImageWithThumb(filePath, toUserId, token) {
|
|
|
183
183
|
*/
|
|
184
184
|
export async function uploadVideoWithThumb(filePath, toUserId, token) {
|
|
185
185
|
const { buildHeaders, BASE_URL } = await import("./weixin.mjs");
|
|
186
|
-
const {
|
|
186
|
+
const { execFileSync } = await import("child_process");
|
|
187
187
|
|
|
188
188
|
const plaintext = await readFile(filePath);
|
|
189
189
|
const rawsize = plaintext.length;
|
|
@@ -195,14 +195,14 @@ export async function uploadVideoWithThumb(filePath, toUserId, token) {
|
|
|
195
195
|
// 获取视频时长
|
|
196
196
|
let playLength = 10;
|
|
197
197
|
try {
|
|
198
|
-
const dur =
|
|
198
|
+
const dur = execFileSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { encoding: "utf-8" }).trim();
|
|
199
199
|
playLength = Math.round(parseFloat(dur));
|
|
200
200
|
} catch {}
|
|
201
201
|
|
|
202
202
|
// 生成缩略图(第一帧)
|
|
203
203
|
const thumbPath = `/tmp/wxta_video_thumb_${Date.now()}.jpg`;
|
|
204
204
|
try {
|
|
205
|
-
|
|
205
|
+
execFileSync("ffmpeg", ["-y", "-i", filePath, "-vframes", "1", "-vf", "scale=224:-1", "-q:v", "5", thumbPath], { stdio: "ignore" });
|
|
206
206
|
} catch {
|
|
207
207
|
// ffmpeg 失败时创建空白缩略图(sendmessage 仍需 thumb 注册)
|
|
208
208
|
const { writeFileSync } = await import("node:fs");
|
|
@@ -213,7 +213,7 @@ export async function uploadVideoWithThumb(filePath, toUserId, token) {
|
|
|
213
213
|
// 缩略图尺寸
|
|
214
214
|
let thumbWidth = 224, thumbHeight = 224;
|
|
215
215
|
try {
|
|
216
|
-
const sipsOut =
|
|
216
|
+
const sipsOut = execFileSync("sips", ["-g", "pixelWidth", "-g", "pixelHeight", thumbPath], { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
217
217
|
const wm = sipsOut.match(/pixelWidth:\s*(\d+)/);
|
|
218
218
|
const hm = sipsOut.match(/pixelHeight:\s*(\d+)/);
|
|
219
219
|
if (wm) thumbWidth = parseInt(wm[1]);
|
package/cli/weixin.mjs
CHANGED
|
@@ -254,34 +254,6 @@ export async function sendImageByUrl(token, to, contextToken, imageUrl) {
|
|
|
254
254
|
);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/**
|
|
258
|
-
* 发送语音消息(base64 音频数据)
|
|
259
|
-
*/
|
|
260
|
-
export async function sendVoiceMessage(token, to, contextToken, audioBase64, durationSec) {
|
|
261
|
-
await apiPost(
|
|
262
|
-
"ilink/bot/sendmessage",
|
|
263
|
-
{
|
|
264
|
-
msg: {
|
|
265
|
-
from_user_id: "",
|
|
266
|
-
to_user_id: to,
|
|
267
|
-
client_id: crypto.randomUUID(),
|
|
268
|
-
message_type: 2,
|
|
269
|
-
message_state: 2,
|
|
270
|
-
item_list: [{
|
|
271
|
-
type: 3, // VOICE
|
|
272
|
-
voice_item: {
|
|
273
|
-
url: `data:audio/mpeg;base64,${audioBase64}`,
|
|
274
|
-
duration: durationSec || 5,
|
|
275
|
-
},
|
|
276
|
-
}],
|
|
277
|
-
context_token: contextToken,
|
|
278
|
-
},
|
|
279
|
-
base_info: {},
|
|
280
|
-
},
|
|
281
|
-
token,
|
|
282
|
-
API_TIMEOUT_MS
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
257
|
|
|
286
258
|
|
|
287
259
|
/**
|