
Openqq
- 8 installs
- 33 repo stars
- Updated April 26, 2026
- bighardperson/computer-science-skills-collection
openqq is a Claude skill that connects a QQ bot to the OpenClaw agent to auto-reply to QQ private chats and group @mentions with session isolation.
About
openqq connects a QQ bot to the OpenClaw agent so it can auto-reply to QQ private chats and group @mentions. It keeps each user or group in an isolated conversation session, logs activity with rotation, and reconnects the WebSocket automatically. A developer configures QQ Open Platform credentials, then runs the bot with npm scripts.
- Bridges a QQ bot to OpenClaw for AI auto-reply in private chats and group @mentions
- Per-user and per-group session isolation with logging and message retry
- WebSocket auto-reconnect and graceful shutdown
Openqq by the numbers
- 8 all-time installs (skills.sh)
- Ranked #12,269 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
openqq capabilities & compatibility
Free skill; requires QQ Open Platform credentials (appId/token/appSecret).
- Capabilities
- orchestration
- Use cases
- orchestration
- Pricing
- Bring your own API key
What openqq says it does
QQ bot integration for OpenClaw. Enables AI auto-reply for QQ private chats and group @mentions with session isolation and comprehensive logging.
**Session Isolation**: Each user/group has independent conversation history
npx skills add https://github.com/bighardperson/computer-science-skills-collection --skill openqqAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | April 26, 2026 |
| Repository | bighardperson/computer-science-skills-collection ↗ |
What it does
Run a QQ bot that auto-replies to private chats and group @mentions through the OpenClaw agent with per-session isolation.
Who is it for?
Deploying an AI auto-reply bot on the QQ messaging platform via OpenClaw.
When should I use this skill?
You want an OpenClaw-powered auto-reply bot for QQ private chats or group @mentions.
What you get
A running QQ bot that auto-replies via OpenClaw with isolated per-user and per-group sessions.
- A running QQ auto-reply bot
By the numbers
- Ships 7 npm commands (start, health, logs, setup, status, clean, etc.)
- Retries failed messages up to 2 times
Files
OpenQQ Bot
QQ bot integration for OpenClaw. Enables AI auto-reply for QQ private chats and group @mentions with session isolation and comprehensive logging.
Quick Start
# Initialize config
npm run setup
# Install dependencies
npm install
# Edit config
vim ~/.openclaw/workspace/open-qq-config.json
# Start bot
npm startConfiguration
Edit ~/.openclaw/workspace/open-qq-config.json:
{
"qq": {
"appId": "YOUR_APP_ID",
"token": "YOUR_TOKEN",
"appSecret": "YOUR_APP_SECRET"
}
}Get credentials from QQ Open Platform.
Usage
npm start # Start bot
npm run health # Health check
npm run logs # View today's logs
npm run status # Check running statusFeatures
- Session Isolation: Each user/group has independent conversation history
- Private:
qq-private-{user_openid} - Group:
qq-group-{group_openid} - Comprehensive Logging: China timezone, log rotation, sensitive data filtering
- Auto Reconnect: WebSocket auto-reconnect with heartbeat
- Message Retry: Auto-retry failed messages (up to 2 times)
- Graceful Shutdown: Clean shutdown on SIGTERM/SIGINT
Files
| File | Description |
|---|---|
qq-bot.js | Main program (WebSocket + OpenClaw integration) |
logger.js | Logging system (China timezone + rotation) |
scripts/health-check.sh | Health check script |
package.json | Dependencies (axios, ws) |
npm Commands
| Command | Description |
|---|---|
npm start | Start bot |
npm run health | Health check |
npm run logs | View logs |
npm run setup | Initialize config |
npm run status | Check status |
npm run clean | Clean node_modules |
Security
- Do not commit
open-qq-config.jsonto version control - Set permissions:
chmod 600 ~/.openclaw/workspace/open-qq-config.json - Uses
spawninstead ofexecto prevent command injection - Session IDs are whitelisted (alphanumeric + hyphen only)
Troubleshooting
| Issue | Solution |
|---|---|
| Config not found | Run npm run setup |
| Missing credentials | Edit config file with appId/token/appSecret |
| WebSocket failed | Check Token and network |
| No reply | Test with openclaw agent --message "test" |
Changelog
See CHANGELOG.md
v0.0.3 (Latest)
- 7 new npm commands
- FAQ section
- Message retry mechanism
- Graceful shutdown
License
MIT License
Links
{
"ownerId": "kn73ngf0s02mcqawszh8pgzcmh8140sg",
"slug": "openqq",
"version": "0.0.5",
"publishedAt": 1772090217841
}{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "openqq",
"installedVersion": "0.0.5",
"installedAt": 1776067938937
}
Changelog
All notable changes to this project will be documented in this file.
---
[0.0.4] - 2026-02-26
🎯 Added
- 配置优化(简化模板)
- 删除不必要的文件(LICENSE, config examples)
- 纯文本文件发布(符合 ClawHub 要求)
🗑️ Removed
- LICENSE 文件(ClawHub 不需要)
- open-qq-config.full.example
- open-qq-config.json.example
- .env.example
---
[0.0.3] - 2026-02-26
🎯 Added
- npm 命令增强 - 新增 7 个实用命令
npm run setup- 一键初始化配置npm run health- 完整健康检查npm run test-msg- 测试消息(模拟)npm run version- 查看版本号npm run restart- 重启提示npm run stop- 停止服务
- 文档增强
- FAQ 章节(8 个常见问题)
- 启动完成提示("已就绪")
- 健康检查输出示例
- 配置备选方案
.env.example- 环境变量配置模板
- 可靠性提升
- LICENSE 文件(MIT)
- Graceful Shutdown(SIGTERM/SIGINT 处理)
- 消息发送重试机制(最多 2 次)
🔧 Changed
- 日志系统优化
- 新增
shouldLog()方法,统一日志级别控制 - 更清晰的日志级别判断逻辑
- 代码优化
- 清理冗余注释(保留关键注释)
- Token 获取重试机制(最多 3 次,指数退避)
- WebSocket 错误事件监听
- 断开连接原因记录
- 文档定位优化
- README.md - 详细使用教程
- SKILL.md - 技术参考文档
🐛 Fixed
- 健康检查脚本
- 修复整数比较错误(
[: 0\n0: integer expression expected) - 添加错误处理和默认值
- WebSocket 处理
- 添加
error事件监听 - 记录断开连接代码和原因
📝 Documentation
- README.md 新增 FAQ 章节
- CHANGELOG.md 格式优化
- .gitignore 添加
.env - 新增 CONFIG.md - 详细配置指南
- 新增 open-qq-config.full.example - 完整配置模板
- 简化 open-qq-config.json.example(只保留核心配置)
---
[0.0.2] - 2026-02-26
🎯 Added
- 健康检查脚本
scripts/health-check.sh- 检查进程、日志、配置状态
- npm 命令
npm run status- 检查运行状态npm run logs- 查看今日日志npm run dev- 开发模式(热重载)
- 配置管理
open-qq-config.json.example移到 skill 目录- Node.js 版本要求(>=16.0.0)
🔧 Changed
- 代码优化(精简 30%)
- 统一 require(axios、spawn 移到顶部)
- 提取通用函数(
handleMessage) - 简化条件判断(三元表达式)
- 合并群聊/私聊逻辑
- 日志系统简化
- 移除 gzip 压缩(容易失败且不需要)
- 简化 writeLog 异步回退逻辑
- 简化日志轮转逻辑
- 文档优化
- SKILL.md 精简 41%
- README.md 精简 46%
- package.json 简化
🐛 Fixed
- 会话 ID 生成逻辑
- 日志写入错误处理
🗑️ Removed
- 未使用的方法
healthCheck()logRawEvent()
- 冗余内容
- node_modules(按需安装)
- package-lock.json
- 冗余注释和文档
📊 Stats
| 文件 | 优化前 | 优化后 | 减少 |
|---|---|---|---|
| qq-bot.js | 356 行 | 307 行 | -14% |
| logger.js | 274 行 | 194 行 | -29% |
| SKILL.md | ~200 行 | 118 行 | -41% |
| README.md | ~200 行 | 109 行 | -46% |
| 总计 | ~1081 行 | 757 行 | -30% |
---
[0.0.1] - 2026-02-26
🎯 Added
- 核心功能
- QQ 私聊消息处理(
C2C_MESSAGE_CREATE) - QQ 群@消息处理(
GROUP_AT_MESSAGE_CREATE) - OpenClaw session 隔离
- 自动心跳和重连
- 日志系统
- 中国时区(Asia/Shanghai)
- 日志轮转(按大小和天数)
- 敏感数据过滤
- INFO/DEBUG/ERROR 分级
- 配置管理
- 集中配置文件(
open-qq-config.json) - 移除环境变量依赖
- 安全加固
- 命令注入防护(
spawn而非exec) - 会话 ID 白名单过滤
- 配置文件权限管理
📁 Files
qq-bot.js- 主程序logger.js- 日志系统start-qq-bot.sh- 启动脚本package.json- 依赖配置SKILL.md- 技术文档README.md- 使用说明
🔧 Technical Details
- WebSocket 连接:
wss://api.sgroup.qq.com/websocket - Intents:
(1 << 0) | (1 << 25)- 私域完整权限 - Session 命名:
qq-private-{openid}/qq-group-{groupid} - 心跳间隔: 30 秒
- 重连延迟: 5 秒
---
Version Summary
| 版本 | 日期 | 主要更新 | 代码行数 |
|---|---|---|---|
| 0.0.3 | 2026-02-26 | 7 个新命令、FAQ、重试机制 | ~550 |
| 0.0.2 | 2026-02-26 | 代码精简 30%、健康检查 | ~750 |
| 0.0.1 | 2026-02-26 | 初始版本 | ~1080 |
---
Upcoming (v0.0.4)
- [ ] 添加单元测试
- [ ] 支持群聊非@消息(可选)
- [ ] 添加消息队列(防止并发过高)
- [ ] 支持多机器人配置
---
格式说明: 本 changelog 遵循 Keep a Changelog 规范。
配置指南
本文档详细说明 openqq 的配置选项。
---
📁 配置文件位置
主配置文件: ~/.openclaw/workspace/open-qq-config.json
---
⚡ 快速配置(推荐)
对于大多数用户,只需配置 QQ 凭据即可:
{
"qq": {
"appId": "你的 APP ID",
"token": "你的 Token",
"appSecret": "你的 App Secret"
}
}其他配置会使用默认值,通常不需要修改。
---
📝 配置项详解
QQ 凭据(必需)
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
appId | string | ✅ | QQ 机器人应用 ID,从 QQ 开放平台获取 |
token | string | ✅ | QQ Bot Token,从 QQ 开放平台获取 |
appSecret | string | ✅ | QQ 应用密钥,从 QQ 开放平台获取 |
获取方式: 1. 访问 QQ 开放平台 2. 登录并创建机器人应用 3. 在应用详情页面获取凭据
---
日志配置(可选)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
logDir | string | /root/.openclaw/workspace/logs/qq-bot | 日志文件存储目录 |
maxLogSize | string | 10MB | 单个日志文件最大大小(支持 B/KB/MB/GB) |
maxLogFiles | number | 7 | 保留的日志文件数量 |
timezone | string | Asia/Shanghai | 日志时间戳时区 |
logLevel | string | debug | 日志级别:debug/info/error |
sanitizeSensitive | boolean | true | 是否自动过滤敏感数据(token、密码等) |
日志级别说明:
debug- 记录所有日志(包括调试信息),适合开发环境info- 只记录信息和错误,适合生产环境error- 只记录错误,适合高负载生产环境
---
机器人配置(可选)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
intents | number | 33554433 | QQ 事件订阅权限(私域完整权限) |
shard | array | [0, 1] | WebSocket 分片配置 |
heartbeatInterval | number | 30000 | 心跳间隔(毫秒) |
reconnectDelay | number | 5000 | 断线重连延迟(毫秒) |
intents 说明:
33554433=(1 << 0) | (1 << 25)- 私域完整权限(QQ 群 + 私聊)- 一般不需要修改,除非需要订阅其他事件
heartbeatInterval 建议:
- 默认
30000(30 秒)即可 - 不要设置过小(<5000),可能被服务器断开
- 不要设置过大(>60000),可能被判定为离线
---
🔒 安全提示
1. 保护配置文件
# 设置文件权限(只有所有者可读写)
chmod 600 ~/.openclaw/workspace/open-qq-config.json
# 验证权限
ls -l ~/.openclaw/workspace/open-qq-config.json
# 应该显示:-rw------- 1 user user ...2. 不要提交到版本控制
# 添加到 .gitignore
echo "open-qq-config.json" >> ~/.openclaw/workspace/.gitignore3. 备份配置
# 备份配置文件(不要分享真实凭据!)
cp ~/.openclaw/workspace/open-qq-config.json \
~/.openclaw/workspace/open-qq-config.json.backup
# 编辑备份文件,删除真实凭据后再分享
vim ~/.openclaw/workspace/open-qq-config.json.backup---
📋 配置示例
最小配置(推荐)
{
"qq": {
"appId": "123456789",
"token": "your_token",
"appSecret": "your_secret"
}
}生产环境配置
{
"qq": {
"appId": "123456789",
"token": "your_token",
"appSecret": "your_secret"
},
"logger": {
"logLevel": "info",
"maxLogFiles": 14
},
"bot": {
"heartbeatInterval": 30000,
"reconnectDelay": 5000
}
}开发环境配置
{
"qq": {
"appId": "123456789",
"token": "your_token",
"appSecret": "your_secret"
},
"logger": {
"logLevel": "debug"
}
}高负载环境配置
{
"qq": {
"appId": "123456789",
"token": "your_token",
"appSecret": "your_secret"
},
"logger": {
"logLevel": "error",
"maxLogFiles": 30
}
}---
🔍 配置验证
检查配置文件格式
# 使用 node 验证 JSON 格式
node -e "JSON.parse(require('fs').readFileSync('~/.openclaw/workspace/open-qq-config.json'))" && echo "✅ JSON 格式正确"检查必需配置
确保配置文件包含以下字段:
- ✅
qq.appId - ✅
qq.token - ✅
qq.appSecret
测试配置
# 启动机器人,检查是否有配置错误
cd ~/.openclaw/workspace/skills/openqq
npm start如果配置正确,会看到:
🚀 正在启动 QQ Bot...
✅ QQ Bot 已就绪,可以接收消息了!---
❓ 常见问题
Q: 配置文件在哪里?
A: ~/.openclaw/workspace/open-qq-config.json
Q: 如何重置配置?
A:
cp ~/.openclaw/workspace/skills/openqq/open-qq-config.json.example \
~/.openclaw/workspace/open-qq-config.jsonQ: 配置修改后需要重启吗?
A: 是的,修改配置后需要重启机器人:
# 停止
pkill -f "node qq-bot.js"
# 启动
npm startQ: 可以使用环境变量吗?
A: 推荐使用配置文件。环境变量作为备选方案,参考 .env.example。
---
📚 相关文件
| 文件 | 说明 |
|---|---|
open-qq-config.json | 主配置文件 |
open-qq-config.json.example | 简化配置模板 |
open-qq-config.full.example | 完整配置模板 |
.env.example | 环境变量模板 |
CONFIG.md | 本文档 |
---
最后更新: 2026-02-26 | 版本: 0.0.3
const fs = require('fs');
const path = require('path');
class QQBotLogger {
constructor(config) {
this.config = config?.logger || this.getDefaultConfig();
this.logDir = this.config.logDir;
this.ensureLogDir();
const dateStr = this.getChinaDate();
this.logFile = path.join(this.logDir, `qq-bot-${dateStr}.log`);
this.debugFile = path.join(this.logDir, `qq-bot-${dateStr}-debug.log`);
this.initLogRotation();
}
getDefaultConfig() {
return {
logDir: '/root/.openclaw/workspace/logs/qq-bot',
maxLogSize: '10MB',
maxLogFiles: 7,
timezone: 'Asia/Shanghai',
logLevel: 'debug',
sanitizeSensitive: true
};
}
ensureLogDir() {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
fs.chmodSync(this.logDir, 0o755);
}
}
getChinaTime() {
const now = new Date();
const chinaTime = new Date(now.getTime() + (8 * 60 * 60 * 1000));
return chinaTime.toISOString().replace('Z', '+08:00');
}
getChinaDate() {
const now = new Date();
const chinaTime = new Date(now.getTime() + (8 * 60 * 60 * 1000));
return chinaTime.toISOString().split('T')[0];
}
sanitizeMessage(message) {
if (!this.config.sanitizeSensitive) return message;
return message
.replace(/(token|password|key|secret)=\w+/gi, '$1=***')
.replace(/sk-[a-zA-Z0-9]{32}/gi, 'sk-***');
}
writeLog(filePath, logStr) {
fs.appendFile(filePath, logStr + '\n', (err) => {
if (err) {
try {
fs.appendFileSync(filePath, logStr + '\n');
} catch (syncErr) {
console.error('Log write failed:', syncErr);
}
}
});
}
initLogRotation() {
this.checkAndRotateLogs();
setInterval(() => this.checkAndRotateLogs(), 24 * 60 * 60 * 1000);
}
checkAndRotateLogs() {
try {
const dateStr = this.getChinaDate();
const files = [
path.join(this.logDir, `qq-bot-${dateStr}.log`),
path.join(this.logDir, `qq-bot-${dateStr}-debug.log`)
];
const maxSize = this.parseSize(this.config.maxLogSize);
for (const file of files) {
if (fs.existsSync(file)) {
const stats = fs.statSync(file);
if (stats.size > maxSize) {
this.rotateLogFile(file);
}
}
}
this.cleanupOldLogs();
} catch (error) {
console.error('Log rotation check failed:', error);
}
}
parseSize(sizeStr) {
const units = { 'B': 1, 'KB': 1024, 'MB': 1024*1024, 'GB': 1024*1024*1024 };
const match = sizeStr.match(/^(\d+)([KMGT]?B?)$/i);
if (match) {
const value = parseInt(match[1]);
const unit = (match[2] || 'B').toUpperCase();
return value * (units[unit] || 1);
}
return 10 * 1024 * 1024;
}
rotateLogFile(filePath) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const rotatedPath = `${filePath}.${timestamp}`;
try {
fs.renameSync(filePath, rotatedPath);
} catch (error) {
console.error('Log rotation failed:', error);
}
}
cleanupOldLogs() {
try {
const files = fs.readdirSync(this.logDir);
const logFiles = files.filter(f =>
f.startsWith('qq-bot-') && (f.endsWith('.log') || f.endsWith('.log.gz'))
);
logFiles.sort((a, b) => {
const timeA = fs.statSync(path.join(this.logDir, a)).mtime;
const timeB = fs.statSync(path.join(this.logDir, b)).mtime;
return timeB - timeA;
});
for (let i = this.config.maxLogFiles; i < logFiles.length; i++) {
fs.unlinkSync(path.join(this.logDir, logFiles[i]));
}
} catch (error) {
console.error('Log cleanup failed:', error);
}
}
shouldLog(level) {
const levels = { error: 0, info: 1, debug: 2 };
const current = levels[this.config.logLevel] ?? 2;
return current >= levels[level];
}
log(message, context = {}) {
if (!this.shouldLog('info')) return;
const timestamp = this.getChinaTime();
const sanitizedMessage = this.sanitizeMessage(message);
const logEntry = { timestamp, level: 'INFO', message: sanitizedMessage, ...context };
const logStr = JSON.stringify(logEntry);
console.log(logStr);
this.writeLog(this.logFile, logStr);
}
debug(message, context = {}) {
if (!this.shouldLog('debug')) return;
const timestamp = this.getChinaTime();
const sanitizedMessage = this.sanitizeMessage(message);
const logEntry = { timestamp, level: 'DEBUG', message: sanitizedMessage, ...context };
const logStr = JSON.stringify(logEntry, null, 2);
console.log(logStr);
this.writeLog(this.debugFile, logStr);
}
error(message, error = {}) {
if (!this.shouldLog('error')) return;
const timestamp = this.getChinaTime();
const sanitizedMessage = this.sanitizeMessage(message);
const logEntry = {
timestamp,
level: 'ERROR',
message: sanitizedMessage,
error: { message: error.message, stack: error.stack, code: error.code }
};
const logStr = JSON.stringify(logEntry, null, 2);
console.error(logStr);
this.writeLog(this.debugFile, logStr);
}
logOpenClawInteraction(sessionMessage, openclawReply) {
this.debug('OpenClaw session interaction', {
sentToOpenClaw: sessionMessage,
receivedFromOpenClaw: openclawReply
});
}
logApiCall(apiName, params, response) {
this.debug(`API call: ${apiName}`, {
requestParams: params,
responseStatus: response?.status,
responseData: response?.data
});
}
}
module.exports = QQBotLogger;
{
"name": "openqq",
"version": "0.0.4",
"description": "QQ bot integration for OpenClaw with session isolation and logging",
"main": "qq-bot.js",
"bin": {
"openqq": "./qq-bot.js"
},
"scripts": {
"start": "node qq-bot.js",
"dev": "node --watch qq-bot.js",
"test": "echo \"No tests specified\" && exit 0",
"setup": "cp open-qq-config.json.example ~/.openclaw/workspace/open-qq-config.json && echo '✅ Config created! Edit ~/.openclaw/workspace/open-qq-config.json'",
"install-deps": "npm install",
"clean": "rm -rf node_modules package-lock.json",
"logs": "tail -f ../../logs/qq-bot/qq-bot-$(date +%Y-%m-%d).log",
"status": "pgrep -f 'node qq-bot.js' > /dev/null && echo '✅ Running' || echo '❌ Not running'",
"health": "bash scripts/health-check.sh",
"version": "echo \"openqq v$(node -p \"require('./package.json').version\")\"",
"test-msg": "node -e \"const {spawn}=require('child_process'); const m=process.argv.slice(3).join(' ')||'你好'; const p=spawn('openclaw',['agent','--session-id','qq-test','--message',m]); p.stdout.on('data',d=>console.log(d.toString())); p.stderr.on('data',d=>console.error(d.toString()));\"",
"restart": "echo '❌ Use systemctl restart qq-bot (if installed as service)'",
"stop": "echo '❌ Use systemctl stop qq-bot (if installed as service)' || pkill -f 'node qq-bot.js'"
},
"keywords": ["qq", "bot", "openclaw", "websocket"],
"author": "番茄",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://clawhub.com/skills/openqq"
},
"dependencies": {
"axios": "^1.13.5",
"ws": "^8.19.0"
},
"engines": {
"node": ">=16.0.0"
}
}
#!/usr/bin/env node
const WebSocket = require('ws');
const axios = require('axios');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
// 从 workspace 配置文件加载配置
const CONFIG_PATH = path.join(process.env.HOME || '/root', '.openclaw/workspace/open-qq-config.json');
function loadConfig() {
try {
if (!fs.existsSync(CONFIG_PATH)) {
console.error(`❌ 配置文件不存在:${CONFIG_PATH}`);
console.error('请复制 open-qq-config.json.example 到 ~/.openclaw/workspace/open-qq-config.json 并填写配置');
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
if (!config.qq?.appId || !config.qq?.token || !config.qq?.appSecret) {
console.error('❌ 配置文件中缺少必需的 QQ 凭据 (qq.appId, qq.token, qq.appSecret)');
process.exit(1);
}
return config;
} catch (error) {
console.error('❌ 读取配置文件失败:', error.message);
process.exit(1);
}
}
const config = loadConfig();
const QQBotLogger = require('./logger');
const logger = new QQBotLogger(config);
class QQBot {
constructor(config) {
this.appId = config.qq.appId;
this.token = config.qq.token;
this.appSecret = config.qq.appSecret;
this.botConfig = config.bot || {};
this.ws = null;
this.sessionId = null;
this.lastSeq = 0;
}
async getAccessToken(retryCount = 0) {
logger.log('Requesting access token from QQ API', { appId: this.appId });
try {
const response = await axios.post(
'https://bots.qq.com/app/getAppAccessToken',
{ appId: this.appId, clientSecret: this.appSecret },
{ timeout: 30000 }
);
logger.log('Access token received successfully', { expiresIn: response.data.expires_in });
logger.logApiCall('getAppAccessToken', { appId: this.appId }, response);
return response.data.access_token;
} catch (error) {
logger.error('Failed to get access token', error);
if (retryCount < 3) {
logger.log(`Retrying access token request (${retryCount + 1}/3)`);
await new Promise(resolve => setTimeout(resolve, 2000 * (retryCount + 1)));
return this.getAccessToken(retryCount + 1);
}
throw error;
}
}
async sendToOpenClawAndWait(messageData) {
const message = messageData.content.trim();
logger.log('Sending message to OpenClaw session', {
userId: messageData.author.id,
groupId: messageData.group_id || 'direct',
message,
});
try {
const isPrivate = messageData.msg_type === 'private';
const openId = isPrivate
? messageData.author.user_openid
: messageData.group_openid;
let sessionLabel = isPrivate
? `qq-private-${openId}`
: `qq-group-${openId}`;
sessionLabel = sessionLabel.replace(/[^a-zA-Z0-9-]/g, '');
const senderId = isPrivate
? (messageData.author.user_openid || 'unknown_user')
: (messageData.author.member_openid || 'unknown_member');
const senderPrefix = isPrivate
? `[QQ User: ${senderId}] `
: `[QQ Member: ${senderId}] `;
const messageWithSender = `${senderPrefix}${message}`;
const openclaw = spawn('openclaw', [
'agent',
'--session-id', sessionLabel,
'--message', messageWithSender
]);
const stdoutChunks = [];
const stderrChunks = [];
openclaw.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
openclaw.stderr.on('data', (chunk) => stderrChunks.push(chunk));
const exitCode = await new Promise((resolve) => {
openclaw.on('close', resolve);
openclaw.on('error', () => resolve(1));
});
const output = Buffer.concat(stdoutChunks).toString().trim();
const errorOutput = Buffer.concat(stderrChunks).toString().trim();
logger.log('OpenClaw raw output', {
outputLength: output.length,
errorLength: errorOutput.length,
exitCode
});
// 处理回复
let openclawReply = output || errorOutput;
openclawReply = openclawReply.replace(/^```[\s\S]*?\n/, '').replace(/```$/, '').trim();
if (!openclawReply || openclawReply.trim() === '') {
openclawReply = '🤖 抱歉,机器人没有返回内容!';
}
logger.log('OpenClaw reply received', {
replyLength: openclawReply.length,
sessionLabel,
exitCode
});
logger.logOpenClawInteraction(messageWithSender, openclawReply);
return openclawReply;
} catch (error) {
logger.error('OpenClaw session failed', { error: error.message });
return '🤖 抱歉,机器人处理消息时出错了!请稍后再试。';
}
}
async sendReply(messageData, content, retryCount = 0) {
try {
const accessToken = await this.getAccessToken();
const isGroup = messageData.msg_type === 'group';
logger.log('Sending reply to QQ', {
content,
target: isGroup ? `group:${messageData.group_id}` : `user:${messageData.author.id}`
});
const url = isGroup
? `https://api.sgroup.qq.com/v2/groups/${messageData.group_id}/messages`
: `https://api.sgroup.qq.com/v2/users/${messageData.author.id}/messages`;
const body = isGroup
? { content, msg_id: messageData.id }
: { content };
const response = await axios.post(url, body, {
headers: {
'Authorization': `QQBot ${accessToken}`,
'Content-Type': 'application/json'
},
timeout: 10000
});
logger.log(`${isGroup ? 'Group' : 'Private'} reply sent successfully`, {
[isGroup ? 'groupId' : 'userId']: isGroup ? messageData.group_id : messageData.author.id,
messageId: response.data?.id
});
logger.logApiCall('sendMessage', {
target: isGroup ? `group:${messageData.group_id}` : `user:${messageData.author.id}`,
content
}, response);
} catch (error) {
if (retryCount < 2) {
logger.log(`Retrying message send (${retryCount + 1}/2)`);
await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
return this.sendReply(messageData, content, retryCount + 1);
}
logger.error('Failed to send reply to QQ after retries', error);
}
}
async handleMessage(event) {
const isPrivate = event.t === 'C2C_MESSAGE_CREATE';
const isGroupAt = event.t === 'GROUP_AT_MESSAGE_CREATE';
if (!isPrivate && !isGroupAt) {
logger.log('Unhandled event type', { eventType: event.t });
return;
}
// 记录消息
const msgData = event.d;
logger.log(`${isPrivate ? 'Private' : 'Group @'} message received from QQ`, {
eventType: event.t,
messageId: msgData.id,
content: msgData.content,
authorId: isPrivate ? msgData.author.user_openid : msgData.author.member_openid,
...(isGroupAt && { groupId: msgData.group_openid }),
timestamp: msgData.timestamp
});
// 标记消息类型
msgData.msg_type = isPrivate ? 'private' : 'group';
if (isPrivate) {
msgData.author.id = msgData.author.user_openid;
} else {
msgData.group_id = msgData.group_openid;
}
// 获取回复并发送
const reply = await this.sendToOpenClawAndWait(msgData);
await this.sendReply(msgData, reply);
}
async connect() {
const accessToken = await this.getAccessToken();
// 获取 WebSocket 连接参数
const wsParams = await axios.get('https://api.sgroup.qq.com/gateway/bot', {
headers: { 'Authorization': `QQBot ${accessToken}` }
});
const wsUrl = `${wsParams.data.url}?compress=0`;
console.log('🔗 Connecting to QQ WebSocket:', wsUrl);
this.ws = new WebSocket(wsUrl);
this.ws.on('open', () => {
console.log('✅ WebSocket connected');
});
this.ws.on('message', async (data) => {
const event = JSON.parse(data);
logger.log('Received WebSocket message', {
op: event.op,
t: event.t,
hasD: !!event.d,
dKeys: event.d ? Object.keys(event.d) : []
});
// HELLO 事件
if (event.op === 10) {
this.sessionId = event.d.session_id;
this.lastSeq = 0;
logger.log('Received HELLO event from QQ gateway', {
sessionId: this.sessionId,
lastSeq: this.lastSeq
});
this.ws.send(JSON.stringify({
op: 2, // IDENTIFY
d: {
token: `QQBot ${accessToken}`,
intents: (1 << 0) | (1 << 25), // 私域完整权限
shard: [0, 1],
properties: { os: 'linux', browser: 'openclaw' }
}
}));
logger.log('Sent IDENTIFY payload to QQ gateway');
} else if (event.op === 11) {
// HEARTBEAT ACK
logger.log('Heartbeat ACK received from QQ gateway');
} else if (event.op === 0 && event.t) {
// 消息事件
await this.handleMessage(event);
} else {
// 其他事件
logger.log('Non-message event received', { op: event.op, t: event.t });
}
});
this.ws.on('error', (error) => {
logger.error('WebSocket error', error);
});
this.ws.on('close', (code, reason) => {
console.log(`❌ WebSocket disconnected (code: ${code}), reconnecting...`);
logger.log('WebSocket disconnected', { code, reason: reason?.toString() });
setTimeout(() => this.connect(), this.botConfig.reconnectDelay || 5000);
});
// 启动心跳
const heartbeatInterval = this.botConfig.heartbeatInterval || 30000;
this.heartbeatInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ op: 1, d: this.lastSeq }));
console.log('💓 Sending heartbeat');
}
}, heartbeatInterval);
// 启动完成提示
console.log('');
console.log('✅ QQ Bot 已就绪,可以接收消息了!');
console.log('');
logger.log('QQ Bot started successfully', {
appId: this.appId,
heartbeatInterval,
reconnectDelay: this.botConfig.reconnectDelay || 5000
});
}
}
// 启动机器人
console.log('🚀 正在启动 QQ Bot...');
const bot = new QQBot(config);
bot.connect().catch((error) => {
console.error('❌ 启动失败:', error.message);
process.exit(1);
});
// 优雅关闭处理
let isShuttingDown = false;
process.on('SIGTERM', handleShutdown);
process.on('SIGINT', handleShutdown);
async function handleShutdown() {
if (isShuttingDown) return;
isShuttingDown = true;
console.log('');
console.log('🛑 正在关闭 QQ Bot...');
logger.log('Shutting down QQ Bot');
// 关闭 WebSocket 连接
if (bot.ws) {
bot.ws.removeAllListeners('close');
bot.ws.close();
}
// 停止心跳
if (bot.heartbeatInterval) {
clearInterval(bot.heartbeatInterval);
}
// 等待一下确保连接关闭
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('✅ QQ Bot 已关闭');
logger.log('QQ Bot shutdown complete');
process.exit(0);
}
openqq - QQ Bot for OpenClaw 🤖
  
让 OpenClaw 通过 QQ 与你互动 — 支持私聊和群聊@消息的 AI 自动回复。
---
✨ 特性
| 特性 | 说明 |
|---|---|
| 🚀 快速集成 | 配置文件填写凭据即可启动 |
| 📁 集中配置 | 所有配置在 open-qq-config.json,无需环境变量 |
| 💬 双模式 | QQ 私聊 + 群聊@消息 |
| 🔐 会话隔离 | 每个用户独立对话历史 |
| 📝 完整日志 | 中国时区、自动轮转、敏感数据过滤 |
| 🔄 自动重连 | WebSocket 断线自动重连 + 心跳保活 |
| 🛡️ 安全加固 | 命令注入防护、敏感数据过滤 |
---
🚀 快速开始
步骤 1: 获取 QQ 机器人凭据
访问 QQ 开放平台 创建机器人,获取:
- APP ID
- Token
- App Secret
步骤 2: 安装并配置
# 进入 skill 目录
cd ~/.openclaw/workspace/skills/openqq
# 一键初始化配置
npm run setup
# 编辑配置文件
vim ~/.openclaw/workspace/open-qq-config.json填写你的凭据(最小配置):
{
"qq": {
"appId": "你的实际 APP ID",
"token": "你的实际 Token",
"appSecret": "你的实际 App Secret"
}
}详细配置说明见 CONFIG.md
步骤 3: 安装依赖并启动
# 安装依赖
npm install
# 启动机器人
npm start看到以下输出表示成功:
🚀 正在启动 QQ Bot...
✅ QQ Bot 已就绪,可以接收消息了!---
📖 使用示例
私聊消息
用户直接给机器人发消息:
用户:你好
机器人:哇啊啊~!你好呀!(≧∇≦)群聊@消息
在群里@机器人:
@机器人 讲个笑话
机器人:有一天,0 遇到了 8...会话隔离
每个用户/群组有独立会话:
- 私聊:
qq-private-{user_openid} - 群聊:
qq-group-{group_openid}
---
🎯 常用命令
cd ~/.openclaw/workspace/skills/openqq
# 初始化配置(首次使用)
npm run setup
# 启动机器人
npm start
# 开发模式(热重载)
npm run dev
# 查看运行状态
npm run status
# 查看今日日志
npm run logs
# 健康检查
npm run health
# 测试消息(不通过 QQ)
npm run test-msg "你好"
# 查看版本
npm run version
# 清理依赖
npm run clean---
⚙️ 配置说明
配置文件位置
~/.openclaw/workspace/open-qq-config.json
完整配置示例
{
"qq": {
"appId": "YOUR_APP_ID",
"token": "YOUR_TOKEN",
"appSecret": "YOUR_APP_SECRET"
},
"logger": {
"logLevel": "debug",
"maxLogFiles": 7
},
"bot": {
"heartbeatInterval": 30000,
"reconnectDelay": 5000
}
}配置项说明
QQ 凭据(必需)
| 字段 | 说明 |
|---|---|
qq.appId | QQ 机器人应用 ID |
qq.token | QQ Bot Token |
qq.appSecret | QQ 应用密钥 |
日志配置(可选)
| 字段 | 默认值 | 说明 |
|---|---|---|
logger.logLevel | debug | debug/info/error |
logger.maxLogFiles | 7 | 保留日志文件数 |
机器人配置(可选)
| 字段 | 默认值 | 说明 |
|---|---|---|
bot.heartbeatInterval | 30000 | 心跳间隔(毫秒) |
bot.reconnectDelay | 5000 | 重连延迟(毫秒) |
---
🔧 运维管理
查看日志
# 实时日志
tail -f ~/.openclaw/workspace/logs/qq-bot/qq-bot-$(date +%Y-%m-%d).log
# 调试日志
tail -f ~/.openclaw/workspace/logs/qq-bot/qq-bot-$(date +%Y-%m-%d)-debug.log
# 搜索错误
grep ERROR ~/.openclaw/workspace/logs/qq-bot/*.log健康检查
# 使用脚本
bash scripts/health-check.sh
# 或使用命令
npm run health输出示例:
🔍 QQ Bot 健康检查
==================
✅ 进程状态:运行中
✅ 日志文件:存在
✅ 日志更新:正常
✅ 最近错误:无
✅ 配置文件:存在
==================
✅ 健康检查完成生产部署(systemd)
创建服务文件 /etc/systemd/system/qq-bot.service:
[Unit]
Description=OpenClaw QQ Bot
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/.openclaw/workspace/skills/openqq
ExecStart=/usr/bin/node qq-bot.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target启动服务:
sudo systemctl daemon-reload
sudo systemctl enable qq-bot
sudo systemctl start qq-bot
sudo systemctl status qq-bot---
❓ FAQ
Q: 机器人启动后立即断开?
A: 检查 Token 是否过期,每次启动会自动获取新 Token。如果持续失败,检查 QQ 开放平台的机器人配置。
Q: 收不到消息回复?
A: 1. 检查机器人是否有接收消息权限 2. 确认 QQ 开放平台的回调地址配置正确 3. 查看日志是否有错误信息
Q: 如何查看机器人是否在运行?
A:
npm run status
# 或
pgrep -f "node qq-bot.js"Q: 如何修改 AI 的人设?
A: 修改 OpenClaw 的 SOUL.md、IDENTITY.md 等文件,QQ 机器人会使用当前 OpenClaw 的人设。
Q: 支持群聊非@消息吗?
A: 不支持。为了避免骚扰,机器人只响应私聊和群聊@消息。
Q: 如何停止机器人?
A:
# systemd 服务
sudo systemctl stop qq-bot
# 手动启动
pkill -f "node qq-bot.js"Q: 配置文件在哪里?
A: ~/.openclaw/workspace/open-qq-config.json
Q: 如何备份配置?
A: 备份 open-qq-config.json 文件即可,不要分享真实凭据。
---
🐛 故障排查
| 问题 | 解决方案 |
|---|---|
配置文件不存在 | npm run setup |
缺少必需的 QQ 凭据 | 编辑配置文件填写凭据 |
| WebSocket 连接失败 | 检查 QQ 开放平台配置 |
| 消息无回复 | openclaw agent --message "test" 测试 |
| 日志不生成 | chmod 755 ~/.openclaw/workspace/logs/qq-bot |
---
🛡️ 安全
配置文件安全
# 设置权限
chmod 600 ~/.openclaw/workspace/open-qq-config.json
# 添加到 .gitignore
echo "open-qq-config.json" >> ~/.openclaw/workspace/.gitignore防护机制
| 风险 | 防护措施 |
|---|---|
| 命令注入 | 使用 spawn 而非 exec |
| 凭据泄露 | 配置文件 + 日志敏感数据过滤 |
| 路径遍历 | 会话 ID 白名单过滤 |
---
📦 文件结构
~/.openclaw/workspace/
├── open-qq-config.json # 主配置文件
├── open-qq-config.json.example # 配置模板
└── skills/openqq/
├── .env.example # 环境变量模板
├── .gitignore
├── CHANGELOG.md # 版本历史
├── scripts/
│ └── health-check.sh # 健康检查
├── qq-bot.js # 主程序
├── logger.js # 日志系统
├── start-qq-bot.sh # 启动脚本
├── package.json # 依赖配置
├── README.md # 本文档
└── SKILL.md # 技术文档---
📝 更新日志
详见 CHANGELOG.md
v0.0.3 (最新)
- ✨ 新增 7 个 npm 命令
- ✨ 添加 FAQ 章节
- ✨ Token 获取重试机制
- 🐛 修复健康检查脚本
v0.0.2
- 代码精简 30%
- 添加健康检查脚本
- 优化文档结构
---
🤝 贡献
发现问题或有新功能建议?欢迎在 ClawHub 反馈!
---
🔗 相关链接
---
版本: 0.0.3 | 许可证: MIT | 作者: 番茄 🍅
#!/bin/bash
# QQ Bot 健康检查脚本
LOG_DIR="$HOME/.openclaw/workspace/logs/qq-bot"
TODAY=$(date +%Y-%m-%d)
LOG_FILE="$LOG_DIR/qq-bot-$TODAY.log"
echo "🔍 QQ Bot 健康检查"
echo "=================="
# 检查进程
if pgrep -f "node qq-bot.js" > /dev/null; then
echo "✅ 进程状态:运行中"
else
echo "❌ 进程状态:未运行"
exit 1
fi
# 检查日志文件
if [ -f "$LOG_FILE" ]; then
echo "✅ 日志文件:存在"
# 检查最近 5 分钟是否有日志
if find "$LOG_FILE" -mmin -5 2>/dev/null | grep -q .; then
echo "✅ 日志更新:正常"
else
echo "⚠️ 日志更新:可能停滞"
fi
# 检查最近的错误
ERRORS=$(tail -100 "$LOG_FILE" 2>/dev/null | grep -c "ERROR" || true)
ERRORS=${ERRORS:-0}
if [ "$ERRORS" -gt 0 ] 2>/dev/null; then
echo "⚠️ 最近错误:$ERRORS 条"
else
echo "✅ 最近错误:无"
fi
else
echo "❌ 日志文件:不存在"
fi
# 检查配置文件
CONFIG_FILE="$HOME/.openclaw/workspace/open-qq-config.json"
if [ -f "$CONFIG_FILE" ]; then
echo "✅ 配置文件:存在"
else
echo "❌ 配置文件:不存在"
exit 1
fi
echo "=================="
echo "✅ 健康检查完成"
#!/bin/bash
CONFIG_FILE="$HOME/.openclaw/workspace/open-qq-config.json"
if [ ! -f "$CONFIG_FILE" ]; then
echo "❌ 配置文件不存在:$CONFIG_FILE"
exit 1
fi
cd /root/.openclaw/workspace/skills/openqq
echo "🚀 Starting QQ Bot..."
node qq-bot.js
Related skills
FAQ
Where does openqq get its credentials?
From the QQ Open Platform; you set appId, token, and appSecret in ~/.openclaw/workspace/open-qq-config.json.
How does openqq keep conversations separate?
Each user and group gets an isolated session id, such as qq-private-{user_openid} or qq-group-{group_openid}.