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.
- package/.claude/settings.local.json +11 -0
- package/cli/agent-adapter.mjs +8 -3
- package/cli/bridge.mjs +16 -73
- 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/agent-adapter.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
145
|
-
|
|
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(`${
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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("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 =
|
|
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
|
/**
|