wechat-to-anything 0.6.6 → 0.6.8

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
+ }
@@ -14,6 +14,9 @@ import { join } from "node:path";
14
14
  import { tmpdir } from "node:os";
15
15
  import { randomBytes } from "node:crypto";
16
16
 
17
+ // Windows 上 npm 全局安装生成 .cmd,execFile/spawn 需要 shell: true 才能找到
18
+ const IS_WIN = process.platform === "win32";
19
+
17
20
  /**
18
21
  * 统一调用接口 — 根据 URL 自动选择适配器
19
22
  */
@@ -37,7 +40,7 @@ export async function checkAgent(url) {
37
40
  const name = url.replace("cli://", "");
38
41
  const cmd = { codex: "codex", gemini: "gemini", claude: "claude", openclaw: "openclaw" }[name] || name;
39
42
  return new Promise((resolve, reject) => {
40
- execFile(cmd, ["--version"], { timeout: 5000 }, (err) => {
43
+ execFile(cmd, ["--version"], { timeout: 5000, shell: IS_WIN }, (err) => {
41
44
  if (err) reject(new Error(`${cmd} CLI 未安装(npm install -g ${{
42
45
  codex: "@openai/codex", gemini: "@google/gemini-cli", claude: "@anthropic-ai/claude-code", openclaw: "openclaw"
43
46
  }[name] || cmd})`));
@@ -161,7 +164,7 @@ function runCodex(prompt, imagePaths = []) {
161
164
  for (const img of imagePaths) args.push("-i", img);
162
165
  args.push("--", prompt);
163
166
 
164
- execFile("codex", args, { timeout: 300_000, maxBuffer: 2 * 1024 * 1024, cwd: tmpdir() },
167
+ execFile("codex", args, { timeout: 300_000, maxBuffer: 2 * 1024 * 1024, cwd: tmpdir(), shell: IS_WIN },
165
168
  async (err, stdout, stderr) => {
166
169
  try {
167
170
  const reply = await readFile(outFile, "utf-8").catch(() => "");
@@ -177,7 +180,7 @@ function runCodex(prompt, imagePaths = []) {
177
180
 
178
181
  function runGemini(prompt) {
179
182
  return new Promise((resolve, reject) => {
180
- const child = spawn("gemini", [], { cwd: tmpdir(), stdio: ["pipe", "pipe", "pipe"], timeout: 300_000 });
183
+ const child = spawn("gemini", [], { cwd: tmpdir(), stdio: ["pipe", "pipe", "pipe"], timeout: 300_000, shell: IS_WIN });
181
184
  let stdout = "", stderr = "";
182
185
  child.stdout.on("data", (d) => (stdout += d));
183
186
  child.stderr.on("data", (d) => (stderr += d));
@@ -197,6 +200,7 @@ function runClaude(prompt) {
197
200
  const child = spawn("claude", ["--print", prompt], {
198
201
  stdio: ["ignore", "pipe", "pipe"],
199
202
  timeout: 300_000,
203
+ shell: IS_WIN,
200
204
  });
201
205
  let stdout = "", stderr = "";
202
206
  child.stdout.on("data", (d) => (stdout += d));
@@ -218,6 +222,7 @@ function runOpenClaw(prompt) {
218
222
  cwd: tmpdir(),
219
223
  stdio: ["ignore", "pipe", "pipe"],
220
224
  timeout: 300_000,
225
+ shell: IS_WIN,
221
226
  });
222
227
  let stdout = "", stderr = "";
223
228
  child.stdout.on("data", (d) => (stdout += d));
package/cli/bridge.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  loadCredentials, loginWithQR, getUpdates,
5
5
  sendMessage, sendImageByUrl, sendVideoByUrl,
6
6
  extractText, extractMedia,
7
- getConfig, sendTyping,
7
+ getConfig, sendTyping, buildHeaders, BASE_URL,
8
8
  } from "./weixin.mjs";
9
9
  import { downloadAndDecrypt, downloadMediaToFile, uploadToCdn } from "./cdn.mjs";
10
10
  import { callAgentAuto, checkAgent } from "./agent-adapter.mjs";
@@ -128,10 +128,8 @@ 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
- const { uploadToCdn } = await import("./cdn.mjs");
134
- const { buildHeaders, BASE_URL: baseUrl } = await import("./weixin.mjs");
135
133
 
136
134
  let audioFile = audioSrc;
137
135
  if (audioSrc.startsWith("http://") || audioSrc.startsWith("https://")) {
@@ -141,8 +139,8 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
141
139
  audioFile = "/tmp/wxta_audio_in.mp3";
142
140
  }
143
141
 
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)"`);
142
+ execFileSync("ffmpeg", ["-y", "-i", audioFile, "-ar", "16000", "-ac", "1", "-f", "s16le", "/tmp/wxta_audio.pcm"], { stdio: "ignore" });
143
+ execFileSync("python3", ["-c", "import pilk; pilk.encode('/tmp/wxta_audio.pcm', '/tmp/wxta_audio.silk', pcm_rate=16000, tencent=True)"]);
146
144
  const pcmSize = statSync("/tmp/wxta_audio.pcm").size;
147
145
  const durationMs = Math.round((pcmSize / 32000) * 1000);
148
146
 
@@ -166,7 +164,7 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
166
164
  },
167
165
  base_info: {},
168
166
  });
169
- await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
167
+ await fetch(`${BASE_URL}/ilink/bot/sendmessage`, {
170
168
  method: "POST", headers: buildHeaders(creds.token, body), body,
171
169
  });
172
170
  console.log(pc.green(`→ [语音] 已发送 (${durationMs}ms)`));
@@ -240,7 +238,17 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
240
238
 
241
239
  if (req.method === "POST" && req.url === "/api/send") {
242
240
  let body = "";
243
- for await (const chunk of req) body += chunk;
241
+ let bodySize = 0;
242
+ for await (const chunk of req) {
243
+ bodySize += chunk.length;
244
+ if (bodySize > 1_048_576) { // 1MB limit
245
+ res.writeHead(413, { "Content-Type": "application/json" });
246
+ res.end(JSON.stringify({ error: "body too large (max 1MB)" }));
247
+ req.destroy();
248
+ return;
249
+ }
250
+ body += chunk;
251
+ }
244
252
  try {
245
253
  const { to, content } = JSON.parse(body);
246
254
  if (!to || !content) {
@@ -359,12 +367,6 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
359
367
  }
360
368
 
361
369
  } 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
370
  const voiceText = media.voiceText || text;
369
371
  if (voiceText) {
370
372
  console.log(pc.cyan(`← [微信] ${from}: [语音] ${voiceText.slice(0, 80)}`));
@@ -399,65 +401,6 @@ export async function start(agents, defaultAgent, { port = 9099 } = {}) {
399
401
  continue;
400
402
  }
401
403
 
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
404
 
462
405
  // 解析 @agentName 路由
463
406
  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("node: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("node: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.8",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {