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.
@@ -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 { execSync } = await import("node:child_process");
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
- 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)"`);
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
- for await (const chunk of req) body += chunk;
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 { execSync } = await import("child_process");
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
- execSync(`sips --resampleWidth 120 "${filePath}" --out "${thumbPath}" 2>/dev/null`);
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 = execSync(`sips -g pixelWidth -g pixelHeight "${thumbPath}" 2>/dev/null`).toString();
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 { execSync } = await import("child_process");
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 = execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${filePath}"`, { encoding: "utf-8" }).trim();
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
- execSync(`ffmpeg -y -i "${filePath}" -vframes 1 -vf "scale=224:-1" -q:v 5 "${thumbPath}" 2>/dev/null`);
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 = execSync(`sips -g pixelWidth -g pixelHeight "${thumbPath}" 2>/dev/null`).toString();
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-to-anything",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {