用 OpenClaw + Telegram 实现双模型群聊中继:可指定模型、可纯文本提问、避免双回复
execFile("openclaw.cmd", args)` 在某些 Windows/Node 环境会报 `spawn EINVAL`。有的返回在 `result.payloads`,有的在顶层 `payloads`。- 支持指定模型回答(`/ask qwen ...`、`/codex ...`、`/qwen ...`)解决:在 Windows 下把 `.cmd/.bat` 包装成 `cmd.e
这篇文章把我在本地落地的完整方案写透:
- 一个群里同时放 `codex bot` 和 `qwen bot`
- 支持多轮辩论(duel)
- 支持指定模型回答(`/ask qwen ...`、`/codex ...`、`/qwen ...`)
- 支持普通文本直接提问(不加 `/ask`)
- 解决 `@codex` 时 qwen 抢答的问题
- 解决 Windows 下 `openclaw.cmd` 调用报错、空响应等坑
文中代码可直接复用。
---
## 1. 需求背景
目标不是“两个 bot 自嗨”,而是一个可用的群助手系统:
1. 用户可以在群里自由提问
2. 可以明确指定哪个模型回答
3. 需要多轮对话能力(对比、辩论)
4. 不要出现重复回复和抢答
Telegram 的限制是:bot 默认不能像人一样直接读取另一个 bot 的消息上下文来“自然互聊”。
所以正确做法是:
- 由本地中继脚本统一接收命令
- 脚本分别调用 OpenClaw 的两个 agent(`main` / `qwen`)
- 脚本再用两个 bot token 将结果发回群里
---
## 2. 总体架构
```text
User in Telegram Group
|
v
Qwen Bot (polling by relay script)
|
v
telegram-dual-bot-relay.js
|- parse command / plain text
|- call OpenClaw agent(main/qwen)
|- sendMessage via codex bot or qwen bot
|
+--> OpenClaw agent: main (Codex model)
+--> OpenClaw agent: qwen (Qwen model)
```
关键点:
- relay 只轮询 qwen bot(`POLL_QWEN_BOT=true`)
- codex bot 由 OpenClaw 主通道继续管理
- 为避免双回复,OpenClaw 群消息配置设为 `requireMention=true`
---
## 3. 文件结构
本实现对应文件:
- `scripts/telegram-dual-bot-relay.js`
- `scripts/start-telegram-dual-bot-relay.ps1`
- `scripts/stop-telegram-dual-bot-relay.ps1`
- `scripts/status-telegram-dual-bot-relay.ps1`
- `scripts/start-telegram-dual-bot-relay.cmd`
- `.env.dual-bot`(运行配置,建议本地保存)
- `.env.dual-bot.example`(模板)
---
## 4. 环境准备
### 4.1 准备两个 Telegram Bot
- codex bot(已在 OpenClaw 里使用)
- qwen bot(新建)
把两个 bot 都拉进同一个群。
### 4.2 OpenClaw 里有两个 agent
- `main`(Codex 模型)
- `qwen`(Qwen 模型)
### 4.3 配置 `.env.dual-bot`
参考模板:
```env
QWEN_BOT_TOKEN=...
CODEX_BOT_TOKEN=
TELEGRAM_GROUP_ID=-100xxxxxxxxxx
CONTROL_USER_IDS=8114485802
OPENCLAW_BIN=openclaw.cmd
CODEX_AGENT_ID=main
QWEN_AGENT_ID=qwen
DEFAULT_ROUNDS=3
TURN_DELAY_MS=1800
AUTO_INTERVAL_MS=1800000
AGENT_TIMEOUT_SEC=180
POLL_TIMEOUT_SEC=20
MAX_MESSAGE_CHARS=3500
POLL_CODEX_BOT=false
POLL_QWEN_BOT=true
ENABLE_PLAIN_TEXT_ASK=true
DEFAULT_ASK_MODEL=
```
说明:
- `DEFAULT_ASK_MODEL` 留空:按来源 bot 决定模型(qwen bot -> qwen)
- 设为 `qwen` 或 `codex`:普通文本固定走指定模型
---
## 5. 核心实现拆解
下面只贴关键代码,完整代码看仓库脚本。
### 5.1 Windows 下安全调用 `openclaw.cmd`
`execFile("openclaw.cmd", args)` 在某些 Windows/Node 环境会报 `spawn EINVAL`。
解决:在 Windows 下把 `.cmd/.bat` 包装成 `cmd.exe /d /c` 调用。
```js
function buildExecInvocation(bin, args) {
const command = String(bin || "").trim();
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
const quotedCommand = /[\s"]/u.test(command) ? quoteForCmd(command) : command;
const cmdline = [quotedCommand, ...args.map(quoteForCmd)].join(" ");
return {
file: process.env.ComSpec || "cmd.exe",
args: ["/d", "/c", cmdline],
windowsVerbatimArguments: true,
};
}
return { file: command, args, windowsVerbatimArguments: false };
}
```
这是稳定运行的关键。
### 5.2 兼容 OpenClaw JSON 返回结构,修复“空响应”
有的返回在 `result.payloads`,有的在顶层 `payloads`。只读一种会导致“执行成功但内容为空”。
```js
const payloads = Array.isArray(parsed?.result?.payloads)
? parsed.result.payloads
: Array.isArray(parsed?.payloads)
? parsed.payloads
: [];
const text = payloads
.map((p) => (typeof p?.text === "string" ? p.text : ""))
.filter(Boolean)
.join("\n\n");
```
### 5.3 指定模型回答接口
实现了三种方式:
- `/ask <codex|qwen> <问题>`
- `/codex <问题>`
- `/qwen <问题>`
核心:把模型名映射到 agentId + botToken。
```js
function getModelRuntime(model) {
if (model === "qwen") {
return { model: "qwen", label: "Qwen", agentId: cfg.qwenAgentId, botToken: cfg.qwenBotToken };
}
return { model: "codex", label: "Codex", agentId: cfg.codexAgentId, botToken: cfg.codexBotToken };
}
async function runAsk(chatId, model, query, triggeredBy) {
const runtime = getModelRuntime(model);
const reply = await callAgent(cfg.openclawBin, runtime.agentId, query, cfg.agentTimeoutSec);
await sendText(runtime.botToken, chatId, `${runtime.label}:\n${reply}`, cfg.maxMessageChars);
}
```
### 5.4 普通文本直接提问(无需 `/ask`)
用户体验优化:非命令文本自动转问答。
```js
const rawText = String(message.text || "").trim();
const command = parseCommand(rawText);
if (!command) {
if (!cfg.enablePlainTextAsk || !rawText) return;
if (hasTelegramMention(rawText)) return; // 防抢答,见下节
const sourceModel = sourceBot === "qwen" ? "qwen" : "codex";
const model = cfg.defaultAskModel || sourceModel;
await runAsk(chatId, model, rawText, fromName);
return;
}
```
### 5.5 `@codex` 时避免 qwen 抢答
当用户显式 `@某bot`,relay 不该把它当普通问答再走一遍。
```js
function hasTelegramMention(text) {
return /(^|\s)@[A-Za-z0-9_]{3,}/.test(String(text || ""));
}
```
在 plain-text fallback 前拦截即可。
---
## 6. 多轮对话(duel)实现
流程:
1. codex 先答
2. qwen 接着答
3. 把上一轮对方观点注入 prompt
4. 循环 N 轮
```js
for (let i = 1; i <= rounds; i += 1) {
lastCodex = await callAgent(cfg.openclawBin, cfg.codexAgentId, buildCodexPrompt(topic, i, rounds, lastQwen), cfg.agentTimeoutSec);
await sendText(cfg.codexBotToken, chatId, `Codex round ${i}:\n${lastCodex}`, cfg.maxMessageChars);
lastQwen = await callAgent(cfg.openclawBin, cfg.qwenAgentId, buildQwenPrompt(topic, i, rounds, lastCodex), cfg.agentTimeoutSec);
await sendText(cfg.qwenBotToken, chatId, `Qwen round ${i}:\n${lastQwen}`, cfg.maxMessageChars);
}
```
---
## 7. 启停脚本
PowerShell 启动脚本负责:
- 校验 `.env.dual-bot`
- 后台启动 Node
- 输出 `pid` 到 `logs/dual-bot-relay.pid`
- 重定向 stdout/stderr 到日志文件
启动:
```powershell
.\scripts\start-telegram-dual-bot-relay.ps1
```
状态:
```powershell
.
\scripts\status-telegram-dual-bot-relay.ps1
```
停止:
```powershell
\scripts\status-telegram-dual-bot-relay.ps1
```
---
## 8. 防双回复的系统级配置
如果 OpenClaw 主通道也在同群监听,普通消息可能被主通道和 relay 同时处理。
建议在 `~/.openclaw/openclaw.json` 中设置:
```json
"channels": {
"telegram": {
"groups": {
"*": { "requireMention": true },
"-1003847525507": { "requireMention": true }
}
}
}
```
这样主通道只在被 `@` 时响应,普通文本留给 relay。
---
## 9. 常见问题与修复
### Q1: 日志报 `spawn EINVAL`
原因:Windows 下直接 `execFile(openclaw.cmd)` 不稳定。
修复:使用 `cmd.exe /d /c` 包装(见 5.1)。
### Q2: 模型执行了但群里显示空响应
原因:JSON 只读了 `result.payloads`,忽略了顶层 `payloads`。
修复:双路径兼容解析(见 5.2)。
### Q3: `@codex` 时 qwen 也回
原因:plain-text fallback 误触发。
修复:检测 `@mention` 后直接跳过 relay 兜底(见 5.5)。
### Q4: `/duel` 和 `/once` 没有执行
原因:命令参数为空。
示例:
- 错误:`/duel`
- 正确:`/duel 2 AI手机对比`
---
## 10. 你可以直接复用的最小步骤
1. 配 `.env.dual-bot`
2. 启动 relay:
```powershell
.\scripts\start-telegram-dual-bot-relay.ps1
```
3. 群里测试:
```text
/ask qwen 帮我总结 iPhone 16 和小米14 的差异
/codex 用三点解释 RAG 和微调区别
普通文本不带斜杠也可直接问
```
4. 查看日志:
```powershell
Get-Content .\logs\dual-bot-relay.out.log -Tail 100
Get-Content .\logs\dual-bot-relay.err.log -Tail 100
```
---
## 11. 后续可扩展方向
- 增加 `traceId`:把用户消息、agent 调用、bot 回复串起来
- 增加速率限制与排队:避免高并发时消息乱序
- 增加回答格式器:支持 Markdown / 表格 / 长文本分片
- 增加消息去重缓存:跨进程重启后仍可防重复触发
---
如果你照着本文落地,核心体验会是:
- 想简单问:直接发文本
- 想精确控模型:`/ask` 或 `/codex` `/qwen`
- 想让两个模型互相打擂台:`/duel`
这套方案在 Windows + OpenClaw + Telegram 群聊场景下可稳定工作。
更多推荐



所有评论(0)