wechat-to-anything 0.5.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kellyxiaowei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # wechat-to-anything
2
+
3
+ > 把微信变成任何 AI Agent 的前端。零依赖,一条命令。
4
+ >
5
+ > ⭐ 如果这个项目对你有帮助,请给个 Star!本项目仅用于技术学习和交流,开源不易。
6
+
7
+ 微信双向支持 Agent 多种模态消息发送和接收,支持文本、图片、语音、文件。
8
+
9
+ <p align="center">
10
+ <img src="docs/wechat-image-send.png" width="250" alt="发送图片给 Agent 识别" />
11
+ <img src="docs/wechat-image-receive.png" width="250" alt="Agent 发送图片到微信" />
12
+ <a href="https://github.com/kellyvv/wechat-to-anything/raw/main/docs/wechat-voice-demo.mp4">
13
+ <img src="docs/wechat-voice-demo.gif" width="250" alt="语音发送演示(点击播放有声版)" />
14
+ </a>
15
+ </p>
16
+
17
+ ## 原理
18
+
19
+ ```
20
+ 微信 ←→ ilinkai API (腾讯) ←→ wechat-to-anything ←→ 你的 Agent (HTTP)
21
+ ```
22
+
23
+ 直接调用腾讯 ilinkai 接口收发微信消息,无中间层。你的 Agent 只需暴露一个 OpenAI 兼容的 HTTP 接口(`POST /v1/chat/completions`),任何语言都行。
24
+
25
+ ### 多种模态支持
26
+
27
+ | 方向 | 图片 | 语音 | 文件 |
28
+ |---|---|---|---|
29
+ | **微信 → Agent** | ✅ 自动识别 | ✅ 语音转文字 | ✅ 提取文本 |
30
+ | **Agent → 微信** | ✅ 自动发图 | ✅ 语音消息 | 文本回复 |
31
+
32
+ ## 前置条件
33
+
34
+ - Node.js >= 22(`nvm install 22`)
35
+
36
+ ## 快速开始
37
+
38
+ ```bash
39
+ # 一条命令,选你喜欢的 Agent:
40
+ npx wechat-to-anything --codex # OpenAI Codex
41
+ npx wechat-to-anything --gemini # Google Gemini
42
+ npx wechat-to-anything --claude # Claude Code
43
+ npx wechat-to-anything --openclaw # OpenClaw
44
+
45
+ # 多 Agent 同时用:
46
+ npx wechat-to-anything --codex --gemini
47
+ ```
48
+
49
+ > 需要先安装对应 CLI:`npm i -g @openai/codex` / `@google/gemini-cli` / `@anthropic-ai/claude-code` / `openclaw`
50
+ >
51
+ > 也支持直接传 URL:`npx wechat-to-anything http://your-agent:8000/v1`
52
+
53
+ ### 接入 OpenClaw
54
+
55
+ ```bash
56
+ # 1. 安装并配置 OpenClaw
57
+ npm i -g openclaw
58
+ openclaw configure # 设置模型(如 Gemini / OpenAI)
59
+
60
+ # 2. 启动 Gateway
61
+ openclaw gateway
62
+
63
+ # 3. 启动桥(另一个终端)
64
+ npx wechat-to-anything --openclaw
65
+ ```
66
+
67
+ > OpenClaw 的 Gateway 需要先配好模型 provider(运行 `openclaw configure`)。
68
+ > 如果 OpenClaw 已有 `openclaw-weixin` 插件,需先禁用以避免消息冲突。
69
+
70
+ ### 首次使用
71
+
72
+ 终端会弹出二维码 → 微信扫码 → 完成。之后自动复用登录凭证。
73
+
74
+ ## 接入你自己的 Agent
75
+
76
+ 暴露 `POST /v1/chat/completions` 即可,任何语言:
77
+
78
+ ```python
79
+ @app.post("/v1/chat/completions")
80
+ def chat(request):
81
+ message = request.json["messages"][-1]["content"]
82
+ reply = your_agent(message)
83
+ return {"choices": [{"message": {"role": "assistant", "content": reply}}]}
84
+ ```
85
+
86
+ 然后 `npx wechat-to-anything http://your-agent:8000/v1`。
87
+
88
+ ## 多 Agent 模式
89
+
90
+ 同时接入多个 Agent,通过 `@` 前缀路由消息。支持 OpenAI 兼容格式和 [ACP (Agent Communication Protocol)](https://agentcommunicationprotocol.dev/) 两种协议:
91
+
92
+ ```bash
93
+ npx wechat-to-anything \
94
+ --agent codex=http://localhost:3001/v1 \
95
+ --agent gemini=http://localhost:3002/v1 \
96
+ --agent bee=acp://localhost:8000/chat \
97
+ --default codex
98
+ ```
99
+
100
+ > `http://` → OpenAI 格式,`acp://` → ACP 协议,自动识别。
101
+
102
+ 微信里使用:
103
+
104
+ | 消息 | 效果 |
105
+ |---|---|
106
+ | `你好` | 发给默认 Agent |
107
+ | `@codex 写个排序` | 路由到 Codex |
108
+ | `@gemini 审查代码` | 路由到 Gemini |
109
+ | `@bee 分析数据` | 路由到 ACP Agent |
110
+ | `@list` | 查看已注册的 Agent |
111
+ | `@切换 gemini` | 切换默认 Agent |
112
+
113
+ 多 Agent 模式下回复自动带 `[agentName]` 前缀标识来源。每个用户独立维护默认 Agent。
114
+
115
+ **图片支持**:消息格式遵循 [OpenAI Vision API](https://platform.openai.com/docs/guides/vision),`content` 为数组:
116
+
117
+ ```json
118
+ {
119
+ "messages": [{
120
+ "role": "user",
121
+ "content": [
122
+ { "type": "text", "text": "这是什么?" },
123
+ { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } }
124
+ ]
125
+ }]
126
+ }
127
+ ```
128
+
129
+ **图片回复**:Agent 回复中包含 markdown 图片 `![desc](https://...)` 会自动作为图片消息发到微信。
130
+
131
+
132
+ ## 凭证
133
+
134
+ 登录凭证保存在 `~/.wechat-to-anything/credentials.json`,删除即可重新登录。
135
+
136
+
137
+
138
+ ## Star History
139
+
140
+ 如果这个项目帮到了你,请给个 ⭐ Star,这是对我们最大的支持!
141
+
142
+ ## License
143
+
144
+ [MIT](LICENSE)
package/bin/cli.mjs ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ import pc from "picocolors";
4
+
5
+ // 解析命令行参数
6
+ const args = process.argv.slice(2);
7
+ const agents = new Map();
8
+ let defaultAgent = null;
9
+
10
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
11
+ console.log(`
12
+ ${pc.cyan("🌉 wechat-to-anything")}
13
+
14
+ ${pc.dim("一条命令,把微信变成任何 AI Agent 的入口")}
15
+
16
+ ${pc.bold("用法:")}
17
+ ${pc.green("最简单:")}
18
+ npx wechat-to-anything ${pc.green("--codex")}
19
+ npx wechat-to-anything ${pc.green("--gemini")}
20
+ npx wechat-to-anything ${pc.green("--claude")}
21
+ npx wechat-to-anything ${pc.green("--openclaw")}
22
+ npx wechat-to-anything ${pc.green("--codex --gemini --claude")} ${pc.dim("多 Agent")}
23
+
24
+ ${pc.green("自定义 Agent:")}
25
+ npx wechat-to-anything ${pc.green("<agent-url>")}
26
+ npx wechat-to-anything ${pc.green("--agent name=url --agent name2=url2")}
27
+
28
+ ${pc.bold("参数:")}
29
+ --codex ${pc.dim("内置 Codex CLI(需先 npm i -g @openai/codex)")}
30
+ --gemini ${pc.dim("内置 Gemini CLI(需先 npm i -g @google/gemini-cli)")}
31
+ --claude ${pc.dim("内置 Claude Code CLI(需先 npm i -g @anthropic-ai/claude-code)")}
32
+ --openclaw ${pc.dim("内置 OpenClaw(需先 npm i -g openclaw)")}
33
+ --agent ${pc.dim("name=url")} ${pc.dim("注册自定义 Agent")}
34
+ --default ${pc.dim("name")} ${pc.dim("设置默认 Agent")}
35
+
36
+ ${pc.dim("Docs: https://github.com/kellyvv/wechat-to-anything")}
37
+ `);
38
+ process.exit(args.length > 0 ? 0 : 1);
39
+ }
40
+
41
+ // 解析参数
42
+ let i = 0;
43
+ while (i < args.length) {
44
+ if (args[i] === "--codex") {
45
+ agents.set("codex", "cli://codex");
46
+ i++;
47
+ } else if (args[i] === "--gemini") {
48
+ agents.set("gemini", "cli://gemini");
49
+ i++;
50
+ } else if (args[i] === "--claude") {
51
+ agents.set("claude", "cli://claude");
52
+ i++;
53
+ } else if (args[i] === "--openclaw") {
54
+ agents.set("openclaw", "cli://openclaw");
55
+ i++;
56
+ } else if (args[i] === "--agent" && args[i + 1]) {
57
+ const [name, ...urlParts] = args[i + 1].split("=");
58
+ const url = urlParts.join("=");
59
+ if (!name || !url) {
60
+ console.error(pc.red(`无效的 --agent 参数: ${args[i + 1]},格式: name=url`));
61
+ process.exit(1);
62
+ }
63
+ if (!url.startsWith("acp://") && !url.startsWith("cli://")) {
64
+ try { new URL(url); } catch {
65
+ console.error(pc.red(`无效的 Agent URL: ${url}`));
66
+ process.exit(1);
67
+ }
68
+ }
69
+ agents.set(name.toLowerCase(), url);
70
+ i += 2;
71
+ } else if (args[i] === "--default" && args[i + 1]) {
72
+ defaultAgent = args[i + 1].toLowerCase();
73
+ i += 2;
74
+ } else if (!args[i].startsWith("--")) {
75
+ if (!args[i].startsWith("acp://")) {
76
+ try { new URL(args[i]); } catch {
77
+ console.error(pc.red(`无效的 URL: ${args[i]}`));
78
+ process.exit(1);
79
+ }
80
+ }
81
+ agents.set("default", args[i]);
82
+ defaultAgent = "default";
83
+ i++;
84
+ } else {
85
+ console.error(pc.red(`未知参数: ${args[i]}`));
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ if (agents.size === 0) {
91
+ console.error(pc.red("至少需要一个 Agent,用 --agent name=url 或直接传 URL"));
92
+ process.exit(1);
93
+ }
94
+
95
+ // 默认用第一个注册的 Agent
96
+ if (!defaultAgent) {
97
+ defaultAgent = agents.keys().next().value;
98
+ }
99
+
100
+ if (!agents.has(defaultAgent)) {
101
+ console.error(pc.red(`默认 Agent "${defaultAgent}" 未注册`));
102
+ process.exit(1);
103
+ }
104
+
105
+ console.log();
106
+ console.log(pc.cyan("🌉 wechat-to-anything"));
107
+ if (agents.size === 1 && agents.has("default")) {
108
+ console.log(pc.dim(` Agent: ${agents.get("default")}`));
109
+ } else {
110
+ for (const [name, url] of agents) {
111
+ const isDefault = name === defaultAgent;
112
+ console.log(pc.dim(` ${isDefault ? "★" : " "} ${name}: ${url}`));
113
+ }
114
+ }
115
+ console.log();
116
+
117
+ import("../cli/bridge.mjs").then((mod) => mod.start(agents, defaultAgent)).catch((err) => {
118
+ console.error(pc.red(err.message));
119
+ process.exit(1);
120
+ });
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Agent 调用适配器
3
+ *
4
+ * 根据 URL scheme 自动选择协议:
5
+ * - http:// / https:// → OpenAI 兼容格式
6
+ * - acp:// → ACP (Agent Communication Protocol)
7
+ * - cli://codex → 内置 Codex CLI 适配器
8
+ * - cli://gemini → 内置 Gemini CLI 适配器
9
+ */
10
+
11
+ import { execFile, spawn } from "node:child_process";
12
+ import { writeFile, readFile, unlink, mkdir } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import { randomBytes } from "node:crypto";
16
+
17
+ /**
18
+ * 统一调用接口 — 根据 URL 自动选择适配器
19
+ */
20
+ export async function callAgentAuto(url, messages) {
21
+ if (url.startsWith("acp://")) return callACP(url, messages);
22
+ if (url.startsWith("cli://")) return callCLI(url, messages);
23
+ return callOpenAI(url, messages);
24
+ }
25
+
26
+ /**
27
+ * 验证 Agent 是否可达
28
+ */
29
+ export async function checkAgent(url) {
30
+ if (url.startsWith("acp://")) {
31
+ const { httpUrl } = parseACPUrl(url);
32
+ const res = await fetch(`${httpUrl}/agents`, { signal: AbortSignal.timeout(5000) });
33
+ if (!res.ok) throw new Error(`ACP server 不可达: ${res.status}`);
34
+ return;
35
+ }
36
+ if (url.startsWith("cli://")) {
37
+ const name = url.replace("cli://", "");
38
+ const cmd = { codex: "codex", gemini: "gemini", claude: "claude", openclaw: "openclaw" }[name] || name;
39
+ return new Promise((resolve, reject) => {
40
+ execFile(cmd, ["--version"], { timeout: 5000 }, (err) => {
41
+ if (err) reject(new Error(`${cmd} CLI 未安装(npm install -g ${{
42
+ codex: "@openai/codex", gemini: "@google/gemini-cli", claude: "@anthropic-ai/claude-code", openclaw: "openclaw"
43
+ }[name] || cmd})`));
44
+ else resolve();
45
+ });
46
+ });
47
+ }
48
+ await fetch(url, { signal: AbortSignal.timeout(5000) });
49
+ }
50
+
51
+ // ========== OpenAI 适配器 ==========
52
+
53
+ async function callOpenAI(agentUrl, messages) {
54
+ const res = await fetch(`${agentUrl}/chat/completions`, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({ messages }),
58
+ signal: AbortSignal.timeout(300_000),
59
+ });
60
+ if (!res.ok) {
61
+ const text = await res.text();
62
+ throw new Error(`${res.status}: ${text.slice(0, 200)}`);
63
+ }
64
+ const data = await res.json();
65
+ return data.choices?.[0]?.message?.content || "(empty response)";
66
+ }
67
+
68
+ // ========== ACP 适配器 ==========
69
+
70
+ function parseACPUrl(acpUrl) {
71
+ const withoutScheme = acpUrl.replace(/^acp:\/\//, "");
72
+ const slashIdx = withoutScheme.indexOf("/");
73
+ if (slashIdx === -1) throw new Error(`无效的 ACP URL: ${acpUrl}`);
74
+ return { httpUrl: `http://${withoutScheme.slice(0, slashIdx)}`, agentName: withoutScheme.slice(slashIdx + 1) };
75
+ }
76
+
77
+ async function callACP(acpUrl, messages) {
78
+ const { httpUrl, agentName } = parseACPUrl(acpUrl);
79
+ const input = messages.map((msg) => {
80
+ if (typeof msg.content === "string") {
81
+ return { parts: [{ content: msg.content, content_type: "text/plain" }] };
82
+ }
83
+ const parts = [];
84
+ for (const item of msg.content) {
85
+ if (item.type === "text") parts.push({ content: item.text, content_type: "text/plain" });
86
+ else if (item.type === "image_url") parts.push({ content: item.image_url.url, content_type: "image/jpeg" });
87
+ }
88
+ return { parts };
89
+ });
90
+
91
+ const res = await fetch(`${httpUrl}/agents/${agentName}/run`, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ input }),
95
+ signal: AbortSignal.timeout(300_000),
96
+ });
97
+ if (!res.ok) { const t = await res.text(); throw new Error(`ACP ${res.status}: ${t.slice(0, 200)}`); }
98
+ const data = await res.json();
99
+ const texts = [];
100
+ for (const msg of data.output || []) {
101
+ for (const part of msg.parts || []) {
102
+ if (part.content_type === "text/plain" || !part.content_type) texts.push(part.content);
103
+ }
104
+ }
105
+ return texts.join("\n") || "(empty response)";
106
+ }
107
+
108
+ // ========== 内置 CLI 适配器 ==========
109
+
110
+ const TMP_DIR = join(tmpdir(), "wechat-cli-agents");
111
+
112
+ async function callCLI(cliUrl, messages) {
113
+ const name = cliUrl.replace("cli://", "");
114
+ const lastMsg = messages[messages.length - 1];
115
+
116
+ // 提取文本和图片
117
+ let prompt = "";
118
+ const imagePaths = [];
119
+ const tmpFiles = [];
120
+
121
+ if (typeof lastMsg.content === "string") {
122
+ prompt = lastMsg.content;
123
+ } else if (Array.isArray(lastMsg.content)) {
124
+ await mkdir(TMP_DIR, { recursive: true });
125
+ for (const part of lastMsg.content) {
126
+ if (part.type === "text") prompt += (prompt ? "\n" : "") + part.text;
127
+ else if (part.type === "image_url" && part.image_url?.url) {
128
+ const tmpPath = join(TMP_DIR, `img-${randomBytes(4).toString("hex")}.jpg`);
129
+ const url = part.image_url.url;
130
+ if (url.startsWith("data:")) {
131
+ await writeFile(tmpPath, Buffer.from(url.replace(/^data:[^;]+;base64,/, ""), "base64"));
132
+ } else {
133
+ const r = await fetch(url);
134
+ if (r.ok) await writeFile(tmpPath, Buffer.from(await r.arrayBuffer()));
135
+ }
136
+ imagePaths.push(tmpPath);
137
+ tmpFiles.push(tmpPath);
138
+ }
139
+ }
140
+ }
141
+
142
+ if (!prompt && imagePaths.length > 0) prompt = "请描述这张图片";
143
+ if (!prompt) throw new Error("empty prompt");
144
+
145
+ try {
146
+ if (name === "codex") return await runCodex(prompt, imagePaths);
147
+ if (name === "gemini") return await runGemini(prompt);
148
+ if (name === "claude") return await runClaude(prompt);
149
+ if (name === "openclaw") return await runOpenClaw(prompt);
150
+ throw new Error(`未知的内置 CLI Agent: ${name}`);
151
+ } finally {
152
+ for (const f of tmpFiles) unlink(f).catch(() => {});
153
+ }
154
+ }
155
+
156
+ function runCodex(prompt, imagePaths = []) {
157
+ return new Promise(async (resolve, reject) => {
158
+ await mkdir(TMP_DIR, { recursive: true });
159
+ const outFile = join(TMP_DIR, `out-${randomBytes(4).toString("hex")}.txt`);
160
+ const args = ["exec", "--skip-git-repo-check", "--ephemeral", "-o", outFile];
161
+ for (const img of imagePaths) args.push("-i", img);
162
+ args.push("--", prompt);
163
+
164
+ execFile("codex", args, { timeout: 300_000, maxBuffer: 2 * 1024 * 1024, cwd: tmpdir() },
165
+ async (err, stdout, stderr) => {
166
+ try {
167
+ const reply = await readFile(outFile, "utf-8").catch(() => "");
168
+ await unlink(outFile).catch(() => {});
169
+ if (reply.trim()) resolve(reply.trim());
170
+ else if (stdout.trim()) resolve(stdout.trim());
171
+ else if (err) reject(new Error((stderr || err.message).trim().slice(0, 300)));
172
+ else resolve("(empty response)");
173
+ } catch (e) { reject(e); }
174
+ });
175
+ });
176
+ }
177
+
178
+ function runGemini(prompt) {
179
+ return new Promise((resolve, reject) => {
180
+ const child = spawn("gemini", [], { cwd: tmpdir(), stdio: ["pipe", "pipe", "pipe"], timeout: 300_000 });
181
+ let stdout = "", stderr = "";
182
+ child.stdout.on("data", (d) => (stdout += d));
183
+ child.stderr.on("data", (d) => (stderr += d));
184
+ child.stdin.write(prompt);
185
+ child.stdin.end();
186
+ child.on("close", (code) => {
187
+ if (stdout.trim()) resolve(stdout.trim());
188
+ else if (code !== 0) reject(new Error((stderr || `exit code ${code}`).trim().slice(0, 300)));
189
+ else resolve("(empty response)");
190
+ });
191
+ child.on("error", (err) => reject(new Error(`gemini CLI 未安装: ${err.message}`)));
192
+ });
193
+ }
194
+
195
+ function runClaude(prompt) {
196
+ return new Promise((resolve, reject) => {
197
+ const child = spawn("claude", ["--print", prompt], {
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ timeout: 300_000,
200
+ });
201
+ let stdout = "", stderr = "";
202
+ child.stdout.on("data", (d) => (stdout += d));
203
+ child.stderr.on("data", (d) => (stderr += d));
204
+ child.on("close", (code) => {
205
+ if (code !== 0) reject(new Error((stderr + stdout).trim().slice(0, 300) || `exit code ${code}`));
206
+ else resolve(stdout.trim() || "(empty response)");
207
+ });
208
+ child.on("error", (err) => reject(new Error(`claude CLI 未安装: ${err.message}`)));
209
+ });
210
+ }
211
+
212
+ function runOpenClaw(prompt) {
213
+ return new Promise((resolve, reject) => {
214
+ const child = spawn("openclaw", [
215
+ "agent", "--agent", "main",
216
+ "--message", prompt, "--json",
217
+ ], {
218
+ cwd: tmpdir(),
219
+ stdio: ["ignore", "pipe", "pipe"],
220
+ timeout: 300_000,
221
+ });
222
+ let stdout = "", stderr = "";
223
+ child.stdout.on("data", (d) => (stdout += d));
224
+ child.stderr.on("data", (d) => (stderr += d));
225
+ child.on("close", (code) => {
226
+ if (code !== 0) {
227
+ reject(new Error((stderr + stdout).trim().slice(0, 300) || `exit code ${code}`));
228
+ return;
229
+ }
230
+ try {
231
+ // OpenClaw stdout 可能含有 [plugins] 日志行在 JSON 之前,跳过
232
+ const jsonStart = stdout.indexOf("\n{");
233
+ const raw = jsonStart >= 0 ? stdout.slice(jsonStart + 1) : stdout;
234
+ const data = JSON.parse(raw);
235
+ // OpenClaw JSON: { result: { payloads: [{ text: "...", mediaUrl: null }] } }
236
+ const payloads = data?.result?.payloads;
237
+ if (Array.isArray(payloads)) {
238
+ const texts = payloads.map(p => p.text).filter(Boolean);
239
+ if (texts.length) { resolve(texts.join("\n")); return; }
240
+ }
241
+ resolve((data?.summary || data?.reply || data?.text || "").trim() || "(empty response)");
242
+ } catch {
243
+ // JSON 解析仍然失败,过滤掉日志行返回纯文本
244
+ const lines = stdout.split("\n").filter(l => !l.match(/^\d{2}:\d{2}:\d{2} \[/));
245
+ resolve(lines.join("\n").trim() || "(empty response)");
246
+ }
247
+ });
248
+ child.on("error", (err) => reject(new Error(`openclaw CLI 未安装: ${err.message}`)));
249
+ });
250
+ }