1. 项目缘起:为什么要把OpenClaw和QQ机器人连起来?
最近在折腾一些自动化流程,发现一个挺有意思的需求:我手头有个自己写的工具,姑且叫它“OpenClaw”,主要功能是帮我从各种网页或者API里抓取、处理一些结构化的信息,比如新闻摘要、商品价格、天气数据,或者是一些特定格式的文档内容。这东西用起来挺顺手,但有个问题——它是个命令行工具,或者是个跑在后台的服务,每次想看结果都得去开个终端敲命令,或者刷新一下网页界面,总觉得不够“丝滑”。
这时候我就想,要是能把处理结果直接推送到我日常高频使用的聊天软件里,比如QQ,那不就方便多了吗?想象一下,我设置一个定时任务,让OpenClaw每天早上9点自动抓取行业资讯,然后整理成简报,直接发到我的QQ上;或者,我临时想查个数据,直接在QQ里@一下机器人,发个指令,它就能调用OpenClaw去干活,然后把结果返回来。这种“服务找人”的模式,比“人找服务”体验好太多了。
所以,“OpenClaw接入QQ机器人”这个事,本质上是在搭建一个“信息处理中枢”与“即时通讯前端”之间的桥梁。OpenClaw负责后端的“重活”——数据获取、清洗、分析和格式化;QQ机器人则充当了最自然、最便捷的用户交互界面。这个组合能极大地拓展OpenClaw的应用场景,让它从一个“工程师工具”变成一个可以服务更广泛人群的“智能助手”。无论是个人用于信息聚合提醒,还是小团队内部用来同步数据看板,都非常实用。
2. 技术栈选型与核心架构设计
要实现这个目标,我们需要一个稳定、可扩展的QQ机器人框架作为中间件,来接收QQ消息、解析指令、调用OpenClaw,并返回结果。目前社区主流的选择有几个,我们需要根据OpenClaw的特性和我们的需求来权衡。
2.1 QQ机器人框架对比
目前比较活跃和成熟的方案主要有基于Mirai生态的各类框架,以及go-cqhttp配合其他语言SDK的方案。
- NoneBot2 (Python):这是一个异步的、插件化的机器人框架,生态非常丰富。它底层可以对接
go-cqhttp(一个兼容OneBot协议的QQ客户端实现)。如果你的OpenClaw本身就是用Python写的,或者你更熟悉Python生态,NoneBot2是集成度最高的选择。它的插件系统可以让你把OpenClaw的功能封装成一个或多个插件,管理起来非常清晰。 - Koishi (JavaScript/TypeScript):这是一个功能极其强大的机器人框架,同样基于插件化,前端有图形化控制台。它原生支持
go-cqhttp。如果你的团队更偏向Web全栈,或者希望有更美观的管理界面,Koishi是很好的选择。你可以用Node.js写插件来调用OpenClaw(如果OpenClaw提供HTTP API)或者通过子进程执行命令。 - 直接使用 go-cqhttp 的 HTTP/WebSocket API:这是最灵活但也最“原始”的方案。
go-cqhttp会提供一个标准的HTTP API或WebSocket服务,你可以用任何语言(Python, Java, Go, PHP等)编写一个后台服务,监听这些接口,实现消息处理和逻辑调用。这种方式耦合度最低,适合对架构有洁癖或者OpenClaw核心逻辑非常复杂、不便嵌入特定框架的情况。
2.2 我们的架构决策
为了普适性,我们假设OpenClaw是一个独立的进程或服务,它可能通过命令行参数调用,也可能提供了一个本地HTTP API。我们选择go-cqhttp+ 自定义中间层服务的架构。理由如下:
- 解耦清晰:OpenClaw的业务逻辑和QQ机器人的消息调度逻辑完全分离。OpenClaw可以独立升级、部署,机器人服务只负责协议转换和路由。
- 语言无关:中间层服务可以用你最熟悉的语言来写,无论是调用OpenClaw的命令行,还是请求它的HTTP接口,都很方便。
- 便于扩展:未来如果想接入微信、钉钉等其他平台,只需要为中间层服务增加新的消息适配器即可,OpenClaw核心代码无需改动。
因此,最终架构流如下:
QQ用户 --发送消息--> go-cqhttp --(HTTP上报)--> 我们的自定义中间层服务 --(调用)--> OpenClaw OpenClaw --(返回结果)--> 我们的自定义中间层服务 --(HTTP API调用)--> go-cqhttp --(发送消息)--> QQ用户/群这个架构中,go-cqhttp负责QQ协议通讯,我们的服务负责业务逻辑,OpenClaw负责核心数据处理。
3. 实战部署:一步步搭建桥梁
接下来,我们进入实操环节。我会以使用Python编写中间层服务为例,因为它语法简洁,生态库丰富,适合快速原型开发。
3.1 第一步:部署与配置 go-cqhttp
- 下载:前往
go-cqhttp的GitHub发布页,根据你的操作系统(Windows, Linux, macOS)下载对应的可执行文件。 - 首次运行:在终端中运行它,首次运行会生成配置文件
config.yml和设备信息文件device.json。 - 关键配置 (
config.yml):
这里最核心的是account: # 账号配置 uin: 1233456 # QQ账号 password: '' # 密码为空,推荐使用扫码登录 encrypt: false # 是否启用密码加密,如启用需使用工具加密 # 连接服务列表 servers: - http: # HTTP通信配置 host: 127.0.0.1 port: 5700 # HTTP服务端口 secret: 'your_http_secret_key' # 密钥,用于验证,中间层服务需要带上 post: - url: 'http://127.0.0.1:8080/cqhttp/event' # 重点!事件上报地址,指向我们的中间层服务 secret: 'your_http_secret_key' # 与上面一致 - ws: # WebSocket配置,可选,用于主动推送 host: 127.0.0.1 port: 6700servers.http.post.url,它告诉go-cqhttp,所有收到的事件(消息、加群请求等)都要POST到这个URL。secret用于简单鉴权。 - 登录:再次运行
go-cqhttp,根据提示选择扫码登录或密码登录。登录成功后,它会监听5700端口(HTTP API)和6700端口(WebSocket),并开始向http://127.0.0.1:8080/cqhttp/event上报事件。
3.2 第二步:编写Python中间层服务
我们的服务需要做两件事:1. 接收go-cqhttp上报的事件并处理;2. 调用OpenClaw。
我们使用FastAPI来快速搭建一个HTTP服务,因为它异步性能好,写起来简单。
pip install fastapi uvicorn requests新建一个文件bot_server.py:
from fastapi import FastAPI, Request, HTTPException, Header from pydantic import BaseModel import subprocess import json import asyncio from typing import Optional import httpx app = FastAPI() # 配置项,应与 go-cqhttp 配置一致 CQHTTP_POST_SECRET = 'your_http_secret_key' CQHTTP_API_URL = 'http://127.0.0.1:5700' # go-cqhttp 的API地址 API_TOKEN = 'your_token' # 调用API时可选的Token # 定义消息上报的数据模型(简化版) class CQEvent(BaseModel): post_type: str message_type: str sub_type: str message_id: int user_id: int message: str raw_message: str font: int sender: dict group_id: Optional[int] = None # 如果是群消息,则有此字段 # 1. 接收事件上报的端点 @app.post("/cqhttp/event") async def handle_event(request: Request, x_signature: Optional[str] = Header(None)): """ 处理 go-cqhttp 上报的所有事件。 x_signature 是 go-cqhttp 可能携带的签名头,这里我们用固定的secret验证。 """ # 简单的Secret验证(生产环境建议更复杂的签名验证) client_host = request.client.host # 这里可以加IP白名单校验 # if client_host not in ['127.0.0.1']: # raise HTTPException(status_code=403, detail="Forbidden") body_bytes = await request.body() try: event_data = json.loads(body_bytes) except json.JSONDecodeError: raise HTTPException(status_code=400, detail="Invalid JSON") # 这里可以验证 secret,例如通过请求头或body的某个字段 # 本例假设在url配置了secret,go-cqhttp会在header `X-Signature` 携带签名,验证逻辑略。 # 只处理私聊和群聊中的文本消息 if event_data.get('post_type') == 'message': message_type = event_data.get('message_type') user_id = event_data.get('user_id') group_id = event_data.get('group_id') raw_message = event_data.get('raw_message', '').strip() # 判断是否是调用OpenClaw的命令,例如以 `!claw` 或 `/claw` 开头 if raw_message.startswith('!claw '): command_args = raw_message[6:] # 去掉 '!claw ' 前缀 # 异步处理,避免阻塞事件上报 asyncio.create_task(process_openclaw_command(command_args, user_id, group_id, message_type)) return {"status": "ok"} # 2. 处理OpenClaw命令的异步任务 async def process_openclaw_command(args: str, user_id: int, group_id: Optional[int], message_type: str): """ 调用OpenClaw并返回结果到QQ。 """ # 这里根据你的OpenClaw调用方式编写 # 方式A:命令行调用(假设OpenClaw是个命令行工具) try: # 安全警告:直接拼接命令参数有风险,务必做好过滤和验证! # 这里仅作示例,生产环境需要严格校验args safe_args = [arg for arg in args.split() if arg.isalnum()] # 一个简单的安全过滤 if not safe_args: result_text = "参数无效,请检查输入。" else: # 假设 openclaw 命令在PATH中 process = await asyncio.create_subprocess_exec( 'openclaw', *safe_args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if process.returncode == 0: result_text = stdout.decode('utf-8', errors='ignore')[:500] # 限制长度 else: result_text = f"执行失败: {stderr.decode('utf-8', errors='ignore')}" except FileNotFoundError: result_text = "错误:未找到 openclaw 命令,请检查安装和PATH配置。" except Exception as e: result_text = f"调用过程发生未知错误: {str(e)}" # 方式B:HTTP API调用(假设OpenClaw提供了HTTP服务) # async with httpx.AsyncClient() as client: # try: # resp = await client.post('http://localhost:8000/claw', json={'query': args}, timeout=30.0) # resp.raise_for_status() # result_data = resp.json() # result_text = result_data.get('result', '无结果返回') # except httpx.RequestError as e: # result_text = f"请求OpenClaw服务失败: {str(e)}" # except Exception as e: # result_text = f"处理响应失败: {str(e)}" # 3. 将结果发送回QQ await send_qq_message(result_text, user_id, group_id, message_type) # 3. 调用 go-cqhttp API 发送消息 async def send_qq_message(message: str, user_id: int, group_id: Optional[int], message_type: str): """通过 go-cqhttp 的 HTTP API 发送消息""" api_url = f"{CQHTTP_API_URL}/send_msg" payload = { "message_type": message_type, "user_id": user_id, "group_id": group_id, "message": message, "auto_escape": False # 允许发送CQ码,如图片 } # 清理None值 payload = {k: v for k, v in payload.items() if v is not None} headers = {'Authorization': f'Bearer {API_TOKEN}'} if API_TOKEN else {} async with httpx.AsyncClient() as client: try: resp = await client.post(api_url, json=payload, headers=headers, timeout=10.0) resp.raise_for_status() print(f"消息发送成功: {resp.json()}") except httpx.RequestError as e: print(f"发送消息API请求失败: {e}") except Exception as e: print(f"发送消息失败: {e}") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=8080)这个服务做了三件事:
- 在
/cqhttp/event端点接收go-cqhttp推送的消息事件。 - 识别以
!claw开头的命令,提取参数。 - 在
process_openclaw_command异步函数中,通过子进程调用本地的openclaw命令行工具(示例A),或者通过HTTP调用OpenClaw服务(示例B,注释状态)。 - 获取结果后,调用
go-cqhttp的/send_msgAPI,将结果发回给对应的用户或群。
3.3 第三步:启动与测试
- 确保
go-cqhttp已在运行并登录。 - 在终端运行你的中间层服务:
python bot_server.py。现在服务运行在http://127.0.0.1:8080。 - 用你的QQ向机器人账号发送消息:
!claw get_news。 - 观察
go-cqhttp的日志和你的bot_server.py输出,查看消息上报、命令处理、API调用的整个流程。如果一切正常,你应该会收到机器人回复的OpenClaw执行结果。
4. 核心细节解析与安全加固
上面的示例是一个最简可用的原型,但在生产环境中,我们需要考虑更多。
4.1 命令解析与权限控制
我们不能让任何人都能随意调用OpenClaw,尤其是可能执行危险操作的命令。我们需要一个更健壮的解析器和权限系统。
import re from functools import wraps # 定义允许的命令和参数模式 ALLOWED_COMMANDS = { 'get_news': r'^(\d+)?$', # 可选数字参数,如 `!claw get_news 5` 'query_price': r'^[a-zA-Z0-9_]+$', # 商品ID 'help': r'^$', # 无参数 } # 简单的用户权限映射(实际应从数据库或配置读取) USER_PERMISSIONS = { 12345678: ['get_news', 'help'], # 用户A只能看新闻和帮助 87654321: ['get_news', 'query_price', 'help'], # 用户B权限更多 } def check_permission(user_id: int, command: str) -> bool: """检查用户是否有执行该命令的权限""" allowed_commands = USER_PERMISSIONS.get(user_id, []) return command in allowed_commands async def process_command_v2(raw_msg: str, user_id: int): """增强版命令解析""" match = re.match(r'^!claw\s+(\w+)(?:\s+(.+))?$', raw_msg) if not match: return "命令格式错误。正确格式:!claw <命令> [参数]" cmd, arg_str = match.groups() args = arg_str.split() if arg_str else [] # 1. 检查命令是否存在于白名单 if cmd not in ALLOWED_COMMANDS: return f"未知命令: {cmd}。输入 `!claw help` 查看帮助。" # 2. 检查用户权限 if not check_permission(user_id, cmd): return "权限不足,无法执行此命令。" # 3. 验证参数格式 param_pattern = ALLOWED_COMMANDS[cmd] # 将参数列表拼接成字符串进行匹配(简单处理) param_to_check = ' '.join(args) if args else '' if not re.match(param_pattern, param_to_check): return f"命令 `{cmd}` 的参数格式不正确。" # 4. 安全地构造系统命令或API参数 # 绝对不要直接拼接!使用参数列表。 # 例如,对于命令行调用: safe_args = ['openclaw', cmd] + args # 现在 safe_args 是一个列表,subprocess会安全处理 # ... 后续调用逻辑4.2 异步处理与超时管理
OpenClaw任务可能耗时较长(比如抓取大量网页)。我们必须避免阻塞主事件循环,并设置超时。
async def call_openclaw_with_timeout(cmd_args: list, timeout: int = 60): """带超时的命令行调用""" try: process = await asyncio.create_subprocess_exec( *cmd_args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) try: stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) except asyncio.TimeoutError: process.kill() # 超时则杀死进程 await process.wait() # 等待进程终止 return None, f"命令执行超时(>{timeout}秒),已终止。", -1 return stdout, stderr, process.returncode except FileNotFoundError: return None, "未找到可执行文件。", -1 except Exception as e: return None, f"创建子进程失败: {str(e)}", -14.3 结果格式化与多媒体支持
纯文本可能不够友好。OpenClaw可以返回结构化数据(JSON),由中间层服务格式化成更易读的消息,甚至支持图片。
async def format_and_send_result(raw_data, user_id, group_id, msg_type): """格式化结果并发送""" # 假设 raw_data 是OpenClaw返回的JSON # {"type": "text", "content": "..."} # {"type": "image", "url": "http://..."} # {"type": "news_list", "items": [...]} if not raw_data: message = "未获取到有效结果。" await send_qq_message(message, user_id, group_id, msg_type) return resp_type = raw_data.get('type', 'text') if resp_type == 'text': message = raw_data['content'][:1000] # 限制长度 elif resp_type == 'image': # 使用CQ码发送图片 image_url = raw_data['url'] message = f"[CQ:image,file={image_url}]" elif resp_type == 'news_list': items = raw_data['items'][:5] # 最多5条 msg_parts = ["最新资讯:"] for i, item in enumerate(items, 1): msg_parts.append(f"{i}. {item['title']} - {item['brief']}") message = '\n'.join(msg_parts) else: message = f"未知的响应类型: {resp_type}" await send_qq_message(message, user_id, group_id, msg_type)5. 生产环境部署与运维要点
当这个机器人开始服务真实用户时,我们需要考虑稳定性、可维护性和可观测性。
5.1 进程管理与高可用
不能让服务因为一个未处理的异常就彻底挂掉。推荐使用进程管理工具。
- Systemd (Linux):为
go-cqhttp和你的bot_server.py分别创建service文件。可以配置Restart=always和RestartSec=3,让它们在崩溃后自动重启。# /etc/systemd/system/openclaw-bot.service [Unit] Description=OpenClaw QQ Bot Service After=network.target [Service] Type=simple User=your_username WorkingDirectory=/path/to/your/bot ExecStart=/usr/bin/python3 /path/to/your/bot/bot_server.py Restart=always RestartSec=3 Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin" [Install] WantedBy=multi-user.target - Docker Compose:将
go-cqhttp和中间层服务都容器化,用docker-compose.yml定义依赖和重启策略,部署和管理更干净。version: '3' services: go-cqhttp: image: ... # 或使用构建的镜像 volumes: - ./cqhttp-data:/data restart: unless-stopped bot-server: build: ./bot-server depends_on: - go-cqhttp restart: unless-stopped
5.2 日志与监控
完善的日志是排查问题的生命线。
- 结构化日志:使用
structlog或logging模块的JSONFormatter,将时间、级别、用户ID、命令、结果状态、耗时等关键字段结构化输出。便于后续用ELK或Loki收集分析。import logging import sys logger = logging.getLogger(__name__) handler = logging.StreamHandler(sys.stdout) # 配置JSON格式器 logger.addHandler(handler) logger.setLevel(logging.INFO) async def process_command(...): start_time = asyncio.get_event_loop().time() logger.info("command_received", user_id=user_id, command=cmd, args=args) # ... 处理逻辑 duration = asyncio.get_event_loop().time() - start_time logger.info("command_completed", user_id=user_id, command=cmd, success=success, duration=duration) - 关键指标监控:可以简单地在代码中埋点,统计命令调用次数、成功率、平均耗时,定期打印或推送到监控系统(如Prometheus)。
5.3 配置管理
不要将secret、API Token、用户权限列表等硬编码在代码里。使用环境变量或配置文件。
import os from pydantic_settings import BaseSettings class Settings(BaseSettings): cqhttp_api_url: str = "http://localhost:5700" cqhttp_post_secret: str api_token: Optional[str] = None allowed_users: dict = {} # 可以从环境变量JSON字符串解析 class Config: env_file = ".env" settings = Settings()然后在.env文件或系统环境变量中配置。
5.4 网络与安全
- HTTPS:如果中间层服务暴露在公网(例如为了回调),务必使用HTTPS。
go-cqhttp上报的post.url可以配置为https://your-domain.com/cqhttp/event。可以使用Nginx反向代理并配置SSL证书。 - IP白名单:在中间层服务的
/cqhttp/event端点,严格校验请求来源IP,只允许go-cqhttp所在服务器的IP。 - 限流:对每个用户或每个QQ号进行命令调用频率限制,防止滥用。
from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded limiter = Limiter(key_func=lambda: request.headers.get("X-Real-IP", "global")) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) @app.post("/event") @limiter.limit("5/minute") # 每分钟5次 async def handle_event(...): ...
6. 进阶玩法与扩展思路
基础功能跑通后,可以玩出更多花样。
6.1 状态管理与会话上下文
让机器人变得更“智能”,能处理多轮对话。例如,用户问!claw 查询天气,机器人回复“请问查询哪个城市?”,用户再回复“北京”,机器人再调用OpenClaw查询北京天气。
这需要引入一个简单的会话状态机或使用内存数据库(如Redis)来存储上下文。为每个(user_id, session_id)存储当前状态和临时数据。
6.2 集成其他数据源与动作
OpenClaw不再是唯一的数据处理器。中间层服务可以作为一个“机器人中枢”,根据命令路由到不同的后端服务。
!claw news-> 调用 OpenClaw!todo add 买牛奶-> 调用 TodoList 服务!server status-> 调用运维监控API 这样,一个QQ机器人就成为了团队的统一操作入口。
6.3 图形化结果与富文本
除了文字和图片,go-cqhttp还支持发送XML和JSON格式的卡片消息(需要客户端支持),可以做出更美观的新闻卡片、数据报表预览等。这需要更复杂的结果格式化逻辑,但体验提升巨大。
6.4 插件化改造
如果你发现中间层服务的代码越来越臃肿,可以考虑将其改造成类似NoneBot2的插件化架构。定义一个基础的Plugin类,每个功能(如新闻查询、价格监控)都是一个独立的插件,负责自己的命令解析、权限检查和逻辑处理。主程序只负责加载插件、路由消息。这样功能迭代会清晰很多。
整个接入过程,从最初的一个简单想法,到搭建起一个稳定、可扩展的生产级服务,涉及了网络通信、安全编程、异步处理、系统部署等多个方面的知识。最关键的体会是,一定要把边界划清楚:go-cqhttp只管协议,中间服务只管路由和业务逻辑组装,OpenClaw只管核心数据处理。各司其职,出了问题也容易定位。另外,对用户输入保持绝对警惕,任何从QQ消息中提取出来用于构造系统命令或API参数的部分,都必须经过严格的白名单校验或转义,这是线上服务安全运行的底线。