这篇文章把我在本地落地的完整方案写透:

- 一个群里同时放 `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 群聊场景下可稳定工作。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐