【全网首发!】OpenClaw 接入 QQ 个人号进阶教程:图片/文件发送、多媒体消息、主动推送与定时任务全攻略
摘要:本文详细介绍了如何扩展OpenClaw与NapCat的QQ机器人功能,使其支持文件发送、图片处理、主动消息推送等高级特性。教程涵盖文件服务器搭建(通过HTTP暴露工作目录)、多媒体消息类型支持、定时任务实现(包括随机问候)以及服务器事件监听告警功能。关键实现包括路径安全校验、MIME类型映射和Docker环境下的网络配置,最终打造一个具备完整交互能力的AI助手。文中还提供了完整代码示例和常见
前言:在上一篇教程中,我们实现了 OpenClaw 通过 NapCat 接入 QQ 个人号的基础文字聊天功能。但仅仅能发文字显然不够——我们希望 AI 能发送图片、文件、表情包,能理解用户发来的图片,甚至能主动给我们发消息(定时问候、服务器告警、任务进度通知等)。本篇教程将带你一步步实现这些高级功能。
目录
- 功能概览
- 前置条件
- 功能一:文件发送(文档、PDF等)
- 功能二:图片发送与接收
- 功能三:多媒体消息全类型支持
- 功能四:主动发送消息
- 功能五:定时任务与随机问候
- 功能六:服务器事件监听与告警
- 踩坑记录与解决方案
- 完整代码汇总
- 总结
1. 功能概览
在基础文字聊天的基础上,本教程将实现以下功能:
| 功能 | 说明 | 难度 |
|---|---|---|
| 文件发送 | AI 可以发送 txt、pdf、docx 等文件给你 | ★★☆ |
| 图片发送 | AI 可以发送图片(包括生成的图表等) | ★★☆ |
| 多媒体支持 | 语音、视频、表情等全类型消息支持 | ★★★ |
| 主动发消息 | 绕过 Agent 限制,直接调用 QQ API 发送消息 | ★★☆ |
| 定时任务 | 定时发送问候、提醒等(支持 cron 表达式) | ★★☆ |
| 随机问候 | 每天随机时间发送问候,模拟真人行为 | ★★★ |
| 事件告警 | 服务器端口被访问、磁盘满等自动通知 | ★★★ |
最终效果:你的 QQ 机器人不仅能聊天,还能发文件、发图片、定时问候你、在服务器出问题时主动通知你——就像一个真正的 AI 助手。
2. 前置条件
本教程假设你已经完成了以下准备工作:
- OpenClaw 已安装并运行(宿主机部署或 Docker 部署均可)
- NapCat 已安装并运行(推荐 Docker 部署)
- QQ 个人号已登录 NapCat
- OpenClaw 已通过 OneBot11 WebSocket 连接到 NapCat
- 基础文字聊天功能正常
关键信息(后续代码中会用到,请替换为你自己的值):
OpenClaw Gateway 端口:18789
NapCat WebSocket 端口:3001
NapCat WebSocket Token:your_token_here
你的 QQ 号:替换为你自己的
OpenClaw 工作目录:/root/openclaw/work
架构回顾:
+------------+ WebSocket +----------+ QQ协议 +----------+
| OpenClaw | <--------------> | NapCat | <----------> | QQ 服务器 |
| (Agent) | ws://...:3001 | (Docker) | | |
+------------+ +----------+ +----------+
|
| HTTP :18790
v
+------------+
| 文件服务器 | <-- 为 NapCat 提供文件下载
+------------+
3. 功能一:文件发送(文档、PDF等)
3.1 问题分析
OpenClaw 的 Agent 在执行任务时会生成文件(如 txt、pdf 等),这些文件保存在 Agent 的工作目录中(如 /root/openclaw/work/)。但 NapCat 运行在 Docker 容器中,无法直接访问宿主机的文件系统。
核心思路:在 OpenClaw 的 QQ 插件中内置一个 HTTP 文件服务器,将工作目录中的文件通过 HTTP 暴露出来,NapCat 就可以通过 URL 下载并发送给用户。
3.2 实现文件服务器
创建 ~/.openclaw/extensions/qq/src/file-server.ts:
import * as http from "http";
import * as fs from "fs";
import * as path from "path";
let server: http.Server | null = null;
export function startFileServer(port: number = 18790): void {
if (server) {
console.log("[QQ FileServer] Already running");
return;
}
server = http.createServer((req, res) => {
try {
// 解码 URL(处理中文文件名等)
const decodedUrl = decodeURIComponent(req.url || "/");
const filePath = path.join("/root/openclaw/work", decodedUrl);
console.log("[QQ FileServer] Request: " + decodedUrl);
// 安全检查:防止路径穿越攻击
const realPath = fs.realpathSync(filePath);
if (!realPath.startsWith("/root/openclaw/work")) {
res.writeHead(403);
res.end("Forbidden");
return;
}
if (fs.existsSync(realPath) && fs.statSync(realPath).isFile()) {
const stat = fs.statSync(realPath);
const ext = path.extname(realPath).toLowerCase();
// MIME 类型映射
const mimeTypes: Record<string, string> = {
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".gif": "image/gif",
".webp": "image/webp", ".txt": "text/plain",
".pdf": "application/pdf",
".mp3": "audio/mpeg", ".mp4": "video/mp4",
};
const contentType = mimeTypes[ext] || "application/octet-stream";
res.writeHead(200, {
"Content-Type": contentType,
"Content-Length": stat.size,
"Access-Control-Allow-Origin": "*",
});
fs.createReadStream(realPath).pipe(res);
} else {
res.writeHead(404);
res.end("Not Found");
}
} catch (err) {
console.error("[QQ FileServer] Error:", err);
res.writeHead(500);
res.end("Error");
}
});
server.listen(port, "0.0.0.0", () => {
console.log("[QQ FileServer] Started on port " + port);
});
}
export function stopFileServer(): void {
if (server) {
server.close();
server = null;
console.log("[QQ FileServer] Stopped");
}
}
3.3 路径转 URL 函数
在 channel.ts 中添加路径转换函数。这是整个文件发送功能的核心——将本地文件路径转换为 NapCat 可以访问的 HTTP URL:
const FILE_SERVER_PORT = 18790;
// 如果 NapCat 在 Docker 中,需要使用宿主机的 Docker 网桥 IP
// 查看方法:ip addr show docker0 | grep inet
const FILE_SERVER_BASE_URL = "http://172.17.0.1:" + FILE_SERVER_PORT;
// 如果 NapCat 也在宿主机上,直接用 127.0.0.1 即可:
// const FILE_SERVER_BASE_URL = "http://127.0.0.1:" + FILE_SERVER_PORT;
function convertLocalPathToUrl(filePath: string): string {
// 已经是 URL 的直接返回
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
return filePath;
}
if (filePath.startsWith("base64://")) {
return filePath;
}
// 将本地路径转换为 HTTP URL
if (filePath.startsWith("/root/openclaw/work/")) {
const relativePath = filePath.substring("/root/openclaw/work".length);
// 注意:不要对路径使用 encodeURIComponent,否则 / 会被编码为 %2F
const url = FILE_SERVER_BASE_URL + relativePath;
console.log("[QQ] Converted: " + filePath + " -> " + url);
return url;
}
return filePath;
}
踩坑提醒:如果你对
relativePath使用了encodeURIComponent(),会导致路径中的/被编码为%2F,最终生成类似http://172.17.0.1:18790%2Ftest.txt的错误 URL,NapCat 会报Invalid URL错误。
3.4 文件上传接口
在 client.ts 中封装 OneBot11 的文件上传 API:
// 上传私聊文件
async uploadPrivateFile(userId: number, file: string, name: string) {
return this.callApi("upload_private_file", { user_id: userId, file, name });
}
// 上传群文件
async uploadGroupFile(groupId: number, file: string, name: string, folder = "") {
return this.callApi("upload_group_file", { group_id: groupId, file, name, folder });
}
3.5 发送文件的完整流程
在 channel.ts 中实现文件发送逻辑:
async function sendFileToTarget(
client: OneBotClient, to: string, fileUrl: string, fileName: string
): Promise<boolean> {
const target = normalizeTarget(to);
const processedUrl = convertLocalPathToUrl(fileUrl);
if (target.startsWith("group:")) {
const groupId = parseInt(target.replace("group:", ""), 10);
if (isNaN(groupId)) return false;
await client.uploadGroupFile(groupId, processedUrl, fileName);
return true;
} else if (/^\d+$/.test(target)) {
const userId = parseInt(target, 10);
await client.uploadPrivateFile(userId, processedUrl, fileName);
return true;
}
return false;
}
3.6 验证文件发送
重启 OpenClaw 后测试:
# 检查文件服务器是否启动
curl -I http://127.0.0.1:18790/
# 创建测试文件
echo "Hello World" > /root/openclaw/work/test.txt
# 检查文件是否可访问
curl http://127.0.0.1:18790/test.txt
然后在 QQ 中对机器人说:“帮我创建一个 txt 文件并发送给我”。
4. 功能二:图片发送与接收
4.1 图片发送
图片发送使用 OneBot11 的 image 消息段,原理和文件类似,但不需要走文件上传接口:
// 发送图片消息段
[{ type: "image", data: { file: "http://your-server:18790/path/to/image.png" } }]
关键是根据文件扩展名判断媒体类型:
function detectMediaType(url: string): "image" | "audio" | "video" | "file" {
const ext = path.extname(url).toLowerCase();
if ([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"].includes(ext)) return "image";
if ([".mp3", ".wav", ".amr", ".silk", ".ogg"].includes(ext)) return "audio";
if ([".mp4", ".avi", ".mov", ".mkv"].includes(ext)) return "video";
return "file";
}
4.2 接收用户发送的图片
当用户发送图片时,raw_message 可能为空。我们需要为媒体消息添加描述性文本,否则 OpenClaw 会回复"没收到文本":
client.on("message", async (event) => {
if (event.post_type !== "message") return;
let text = event.raw_message || "";
// 如果消息为空但包含媒体,添加描述性文本
if (!text && event.message && Array.isArray(event.message)) {
const mediaTypes = event.message
.map((seg: any) => seg.type)
.filter((t: string) => t !== "text");
if (mediaTypes.length > 0) {
const descriptions = mediaTypes.map((type: string) => {
if (type === "image") return "[图片]";
if (type === "file") return "[文件]";
if (type === "video") return "[视频]";
if (type === "record") return "[语音]";
return "[" + type + "]";
});
text = descriptions.join(" ");
}
}
// 忽略空消息(系统消息、回执等)
if (!text || text.trim() === "") {
console.log("[QQ] Ignoring empty message event");
return;
}
// ... 后续处理
});
4.3 在 sendMedia 中统一处理
outbound.sendMedia 是 OpenClaw 发送媒体的统一出口,需要根据媒体类型选择不同的发送方式:
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
const client = getClientForAccount(accountId || DEFAULT_ACCOUNT_ID);
if (!client) return { channel: "qq", sent: false, error: "Client not connected" };
try {
// 拒绝 data URL(base64 编码的内容用户看不懂)
if (mediaUrl.startsWith("data:")) {
return { channel: "qq", sent: false,
error: "Data URLs not supported. Save file and use HTTP URL." };
}
const mediaType = detectMediaType(mediaUrl);
if (mediaType === "image" || mediaType === "audio" || mediaType === "video") {
const processedUrl = convertLocalPathToUrl(mediaUrl);
const message: OneBotMessage = [];
if (text) message.push({ type: "text", data: { text } });
if (mediaType === "image") {
message.push({ type: "image", data: { file: processedUrl } });
} else if (mediaType === "audio") {
message.push({ type: "record", data: { file: processedUrl } });
} else if (mediaType === "video") {
message.push({ type: "video", data: { file: processedUrl } });
}
const success = await sendToTarget(client, to, message);
return { channel: "qq", sent: success };
}
// 其他文件类型用文件上传接口
const fileName = path.basename(mediaUrl);
const success = await sendFileToTarget(client, to, mediaUrl, fileName);
if (success && text) {
await sendToTarget(client, to, [{ type: "text", data: { text } }]);
}
return { channel: "qq", sent: success };
} catch (err) {
console.error("[QQ] sendMedia error:", err);
return { channel: "qq", sent: false, error: String(err) };
}
}
5. 功能三:多媒体消息全类型支持
5.1 完整的类型定义
在 types.ts 中定义所有 OneBot11 消息段类型(文本、图片、表情、语音、视频、@、回复、JSON卡片、文件、戳一戳、骰子、猜拳、音乐分享、合并转发等),完整代码见第10节。
5.2 完整的 Client API
在 client.ts 中封装所有 OneBot11 API,包括:
- 消息接口:发送私聊/群聊消息、撤回消息、获取消息、合并转发
- 好友操作:好友列表、好友赞、处理好友请求
- 群操作:群信息、群成员、踢人、禁言、设置管理员等
- 文件操作:上传群文件、上传私聊文件、获取图片/语音/文件信息
- 系统接口:登录信息、版本信息、运行状态
- NapCat 扩展:戳一戳、设置头像、设置签名、消息表情回复、标记已读、聊天历史、AI语音等
完整代码见第10节。
6. 功能四:主动发送消息
6.1 为什么需要主动发送?
OpenClaw 的 Agent 是被动响应的——只有收到用户消息时才会回复。但很多场景需要主动发送消息:
- 定时问候(早安、晚安)
- 服务器告警(端口被扫描、磁盘满)
- 新邮件通知
- 任务进度报告
6.2 实现原理
NapCat 的 OneBot11 WebSocket 接口是双向的——不仅可以接收消息事件,还可以主动调用 API 发送消息。我们只需要建立一个独立的 WebSocket 连接,发送 send_private_msg 请求即可。
6.3 创建发送工具
首先初始化项目:
mkdir -p /root/qq-tools
cd /root/qq-tools
npm init -y
npm install ws
创建 /root/qq-tools/send-message.js:
#!/usr/bin/env node
const WebSocket = require('ws');
const NAPCAT_WS_URL = 'ws://127.0.0.1:3001';
const ACCESS_TOKEN = 'your_token_here'; // 替换为你的 NapCat token
function sendQQMessage(target, message) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(NAPCAT_WS_URL, {
headers: { 'Authorization': 'Bearer ' + ACCESS_TOKEN }
});
const timeout = setTimeout(() => {
ws.close();
reject(new Error('发送超时'));
}, 10000);
ws.on('open', () => {
let action, params;
if (target.startsWith('group:')) {
const groupId = parseInt(target.replace('group:', ''), 10);
action = 'send_group_msg';
params = {
group_id: groupId,
message: [{ type: 'text', data: { text: message } }]
};
} else {
const userId = parseInt(target, 10);
action = 'send_private_msg';
params = {
user_id: userId,
message: [{ type: 'text', data: { text: message } }]
};
}
ws.send(JSON.stringify({
action: action,
params: params,
echo: 'send_' + Date.now()
}));
});
ws.on('message', (data) => {
const response = JSON.parse(data.toString());
if (response.echo && response.echo.startsWith('send_')) {
clearTimeout(timeout);
if (response.status === 'ok') {
resolve(response.data);
} else {
reject(new Error(response.message || '发送失败'));
}
ws.close();
}
});
ws.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
}
// CLI 入口
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('用法: node send-message.js <QQ号或group:群号> <消息内容>');
process.exit(1);
}
const [target, ...messageParts] = args;
sendQQMessage(target, messageParts.join(' '))
.then(() => { console.log('发送成功'); process.exit(0); })
.catch((err) => { console.error('发送失败:', err.message); process.exit(1); });
}
module.exports = { sendQQMessage };
6.4 测试主动发送
# 发送私聊消息
node /root/qq-tools/send-message.js 你的QQ号 "Hello! 这是一条主动发送的消息"
# 发送群消息
node /root/qq-tools/send-message.js group:群号 "这是一条群消息"
如果一切正常,你会立即在 QQ 上收到消息。这条消息完全绕过了 OpenClaw Agent,直接通过 NapCat API 发送。
7. 功能五:定时任务与随机问候
7.1 方案选择
定时任务有两种实现方式:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Linux crontab | 简单可靠,不依赖 OpenClaw | 功能单一 |
| OpenClaw Cron API | 可以调用 Agent 生成动态内容 | 依赖 Gateway 运行 |
推荐使用 Linux crontab,因为更稳定可靠。
7.2 简化版定时任务工具
创建 /root/qq-tools/simple-cron.js:
#!/usr/bin/env node
const SEND_SCRIPT = '/root/qq-tools/send-message.js';
const QQ_USER = '你的QQ号';
function execSync(command) {
return require('child_process').execSync(command, { encoding: 'utf-8' });
}
// 添加每日定时任务
function addDailyTask(hour, minute, message) {
try {
const cronExpr = hour + ' ' + minute + ' * * *';
const jobId = 'daily_' + hour + '_' + minute + '_' + Date.now();
let currentCrontab = '';
try {
currentCrontab = execSync('crontab -l 2>/dev/null').toString().trim();
} catch (e) {
currentCrontab = '# QQ定时任务';
}
const taskLine = cronExpr + ' node ' + SEND_SCRIPT + ' ' + QQ_USER
+ " '" + message + "' # " + jobId;
const newCrontab = currentCrontab + '\n' + taskLine;
execSync('echo "' + newCrontab.replace(/"/g, '\\"') + '" > /tmp/new_cron.txt');
execSync('crontab /tmp/new_cron.txt');
console.log('每日任务创建成功: ' + hour + ':' + minute + ' -> ' + message);
return { success: true, jobId };
} catch (error) {
console.error('创建失败:', error.message);
return { success: false };
}
}
// 列出所有任务
function listTasks() {
try {
console.log('=== 当前定时任务 ===');
const result = execSync('crontab -l 2>/dev/null').toString();
const lines = result.trim().split('\n').filter(
line => line.trim() && !line.startsWith('#')
);
if (lines.length === 0) {
console.log('(无任务)');
} else {
lines.forEach((line, i) => console.log((i+1) + '. ' + line));
}
} catch (e) {
console.log('(无任务)');
}
}
// 删除任务
function removeTask(jobId) {
const result = execSync('crontab -l').toString();
const lines = result.split('\n').filter(line => !line.includes(jobId));
execSync('echo "' + lines.join('\n') + '" | crontab -');
console.log('任务已删除: ' + jobId);
}
// CLI
const args = process.argv.slice(2);
const cmd = args[0];
if (cmd === 'add-daily') {
addDailyTask(parseInt(args[1]), parseInt(args[2]), args.slice(3).join(' '));
} else if (cmd === 'list') {
listTasks();
} else if (cmd === 'remove') {
removeTask(args[1]);
} else if (cmd === 'examples') {
addDailyTask(8, 0, '早上好!新的一天开始了');
addDailyTask(12, 0, '该吃午饭了!记得休息一下');
addDailyTask(22, 0, '晚安!早点休息哦');
console.log('示例任务创建完成');
} else {
console.log('用法:');
console.log(' node simple-cron.js add-daily <时> <分> <消息>');
console.log(' node simple-cron.js list');
console.log(' node simple-cron.js remove <jobId>');
console.log(' node simple-cron.js examples');
}
module.exports = { addDailyTask, listTasks, removeTask };
7.3 使用示例
# 创建每天早上 8:00 的问候
node /root/qq-tools/simple-cron.js add-daily 8 0 "早上好!新的一天开始了"
# 创建每天晚上 22:00 的晚安
node /root/qq-tools/simple-cron.js add-daily 22 0 "晚安!早点休息"
# 一键创建示例任务(早安、午餐、晚安)
node /root/qq-tools/simple-cron.js examples
# 查看所有任务
node /root/qq-tools/simple-cron.js list
# 删除指定任务
node /root/qq-tools/simple-cron.js remove daily_8_0_1234567890
7.4 高级:随机问候(模拟真人)
固定时间的问候太机械了,真人不会每天准时 8:00 说早安。我们可以实现随机问候——每天生成 15-25 个随机时间点,发送不同的问候语。
创建 /root/qq-tools/random-greetings.js:
#!/usr/bin/env node
const SEND_SCRIPT = '/root/qq-tools/send-message.js';
const QQ_USER = '你的QQ号';
const MIN_GREETINGS = 15;
const MAX_GREETINGS = 25;
const START_HOUR = 7;
const END_HOUR = 23;
// 问候语库(按时间段分类)
const greetingTemplates = {
morning: [
"早安!新的一天开始了,记得吃早餐哦",
"早上好呀!今天也要元气满满!",
"起床啦!美好的一天从现在开始!",
],
noon: [
"中午好!吃饭了吗?",
"午休时间到了,记得休息一下",
"午餐时间到!美食在向你招手",
],
afternoon: [
"下午好!工作累了吗?休息一下",
"下午茶时间到!",
"下午好!记得多喝水",
],
evening: [
"晚上好呀!今天过得怎么样?",
"晚饭吃了吗?不要饿肚子哦",
"晚上好!放松一下自己吧",
],
random: [
"突然想你了,就给你发个消息",
"在干嘛呀?",
"嘿!今天一切都好吗?",
"今天也要开心哦!",
]
};
// 根据当前时间生成问候语
function generateGreeting() {
const hour = new Date().getHours();
let category;
if (hour >= 7 && hour < 10) category = 'morning';
else if (hour >= 10 && hour < 14) category = 'noon';
else if (hour >= 14 && hour < 18) category = 'afternoon';
else category = 'evening';
// 30% 概率使用随机问候
if (Math.random() < 0.3) category = 'random';
const templates = greetingTemplates[category];
return templates[Math.floor(Math.random() * templates.length)];
}
// 生成随机时间点
function generateRandomTimes(count) {
const times = [];
const startMin = START_HOUR * 60;
const endMin = END_HOUR * 60 + 59;
while (times.length < count) {
const randomMin = Math.floor(Math.random() * (endMin - startMin + 1)) + startMin;
const h = Math.floor(randomMin / 60);
const m = randomMin % 60;
const timeStr = h + ':' + m.toString().padStart(2, '0');
if (!times.includes(timeStr)) times.push(timeStr);
}
return times.sort((a, b) => {
const [ha, ma] = a.split(':').map(Number);
const [hb, mb] = b.split(':').map(Number);
return ha * 60 + ma - (hb * 60 + mb);
});
}
// 生成今天的问候任务并写入 crontab
function createGreetingTasks() {
const count = Math.floor(Math.random() * (MAX_GREETINGS - MIN_GREETINGS + 1)) + MIN_GREETINGS;
const times = generateRandomTimes(count);
const { execSync } = require('child_process');
// 获取当前 crontab,移除旧的问候任务
let currentCrontab = '';
try {
currentCrontab = execSync('crontab -l 2>/dev/null').toString();
} catch (e) {
currentCrontab = '# 定时任务';
}
const lines = currentCrontab.split('\n').filter(line =>
!line.includes('# greeting_') && line.trim() && !line.startsWith('#')
);
// 添加新的问候任务
const jobId = 'greeting_' + Date.now();
times.forEach(time => {
const [hour, minute] = time.split(':').map(Number);
const message = generateGreeting();
const cronJob = minute + ' ' + hour + ' * * * node ' + SEND_SCRIPT
+ ' ' + QQ_USER + " '" + message.replace(/'/g, "\\'") + "' # " + jobId;
lines.push(cronJob);
});
// 写入 crontab
const newCrontab = lines.join('\n');
execSync('echo "' + newCrontab.replace(/"/g, '\\"') + '" | crontab -');
console.log('随机问候任务创建成功!共 ' + times.length + ' 个时间点');
console.log('时间点: ' + times.join(', '));
}
// CLI
const cmd = process.argv[2];
if (cmd === 'generate') {
createGreetingTasks();
} else if (cmd === 'test') {
const msg = generateGreeting();
console.log('测试问候: ' + msg);
require('child_process').execSync(
'node ' + SEND_SCRIPT + ' ' + QQ_USER + " '" + msg + "'"
);
} else if (cmd === 'preview') {
const count = Math.floor(Math.random() * (MAX_GREETINGS - MIN_GREETINGS + 1)) + MIN_GREETINGS;
const times = generateRandomTimes(count);
console.log('预览今天的 ' + times.length + ' 个问候时间点:');
times.forEach(t => console.log(' ' + t));
} else {
console.log('用法:');
console.log(' node random-greetings.js generate # 生成今天的问候任务');
console.log(' node random-greetings.js test # 测试发送一条问候');
console.log(' node random-greetings.js preview # 预览随机时间');
}
7.5 每天自动刷新随机问候
将随机问候的生成也加入 crontab,每天凌晨自动刷新:
# 每天凌晨 0:05 重新生成随机问候时间
echo "5 0 * * * node /root/qq-tools/random-greetings.js generate" | crontab -
# 或者手动执行一次
node /root/qq-tools/random-greetings.js generate
8. 功能六:服务器事件监听与告警
8.1 功能说明
创建一个后台服务,持续监控服务器状态,当检测到异常时自动通过 QQ 发送告警:
- 端口监控:检测 SSH、HTTP 等端口的活动连接
- 磁盘监控:磁盘使用率超过阈值时告警
- 内存监控:内存使用率超过阈值时告警
- 文件监控:监听指定文件变化(如新邮件)
8.2 实现代码
创建 /root/qq-tools/event-monitor.js:
#!/usr/bin/env node
const fs = require('fs');
const { exec } = require('child_process');
const { sendQQMessage } = require('./send-message.js');
// 配置
const CONFIG = {
qq_user: '你的QQ号',
check_interval: 60000, // 检查间隔 60 秒
monitored_ports: [22, 80, 443], // 监听的端口
disk_warning_threshold: 90, // 磁盘使用率告警阈值 %
memory_warning_threshold: 90, // 内存使用率告警阈值 %
};
// 状态记录(避免重复告警)
const state = {
last_port_access: {},
last_disk_warning: 0,
last_memory_warning: 0,
};
// 检查端口访问
function checkPortAccess() {
CONFIG.monitored_ports.forEach(port => {
exec('ss -tn | grep :' + port + ' | grep ESTAB', (error, stdout) => {
if (stdout && stdout.trim()) {
const connections = stdout.trim().split('\n');
const now = Date.now();
// 每 5 分钟最多通知一次
if (!state.last_port_access[port] || now - state.last_port_access[port] > 300000) {
const ips = [...new Set(connections.map(line => {
const match = line.match(/(\d+\.\d+\.\d+\.\d+)/);
return match ? match[1] : 'unknown';
}))];
sendQQMessage(CONFIG.qq_user,
'端口 ' + port + ' 检测到 ' + connections.length + ' 个活动连接\n来源: ' + ips.join(', ')
).catch(err => console.error('发送失败:', err));
state.last_port_access[port] = now;
}
}
});
});
}
// 检查磁盘空间
function checkDiskSpace() {
exec("df -h / | tail -1 | awk '{print $5}' | sed 's/%//'", (error, stdout) => {
if (!error && stdout) {
const usage = parseInt(stdout.trim(), 10);
const now = Date.now();
if (usage >= CONFIG.disk_warning_threshold && now - state.last_disk_warning > 3600000) {
sendQQMessage(CONFIG.qq_user, '磁盘空间警告!使用率: ' + usage + '%')
.catch(err => console.error('发送失败:', err));
state.last_disk_warning = now;
}
}
});
}
// 检查内存使用
function checkMemoryUsage() {
exec("free | grep Mem | awk '{print ($3/$2) * 100.0}'", (error, stdout) => {
if (!error && stdout) {
const usage = parseFloat(stdout.trim());
const now = Date.now();
if (usage >= CONFIG.memory_warning_threshold && now - state.last_memory_warning > 3600000) {
sendQQMessage(CONFIG.qq_user, '内存使用警告!使用率: ' + usage.toFixed(1) + '%')
.catch(err => console.error('发送失败:', err));
state.last_memory_warning = now;
}
}
});
}
// 启动监控
function startMonitoring() {
console.log('[事件监听] 服务启动');
console.log('[事件监听] 监听端口:', CONFIG.monitored_ports.join(', '));
sendQQMessage(CONFIG.qq_user, '事件监听服务已启动,正在监控系统状态...')
.catch(err => console.error('启动通知发送失败:', err));
setInterval(() => {
checkPortAccess();
checkDiskSpace();
checkMemoryUsage();
}, CONFIG.check_interval);
}
// 优雅退出
process.on('SIGINT', () => {
sendQQMessage(CONFIG.qq_user, '事件监听服务已停止')
.then(() => process.exit(0))
.catch(() => process.exit(0));
});
if (require.main === module) {
startMonitoring();
}
8.3 部署为系统服务
创建 systemd 服务文件 /etc/systemd/system/qq-event-monitor.service:
[Unit]
Description=QQ Event Monitor Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/qq-tools
ExecStart=/usr/bin/node /root/qq-tools/event-monitor.js
Restart=always
RestartSec=10
StandardOutput=append:/var/log/qq-event-monitor.log
StandardError=append:/var/log/qq-event-monitor.log
[Install]
WantedBy=multi-user.target
启用服务:
sudo systemctl daemon-reload
sudo systemctl enable qq-event-monitor.service
sudo systemctl start qq-event-monitor.service
sudo systemctl status qq-event-monitor.service
9. 踩坑记录与解决方案
9.1 文件发送报 Invalid URL
现象:OpenClaw 日志中出现 Error: Invalid URL,URL 类似 http://127.0.0.1:18790%2Ftest.txt
原因:对文件路径使用了 encodeURIComponent(),导致 / 被编码为 %2F
解决:直接拼接路径,不要编码:
// 错误写法
const url = FILE_SERVER_BASE_URL + "/" + encodeURIComponent(relativePath);
// 正确写法
const url = FILE_SERVER_BASE_URL + relativePath;
9.2 NapCat 无法访问文件服务器
现象:NapCat 日志报连接 127.0.0.1:18790 失败
原因:NapCat 在 Docker 容器中,127.0.0.1 指向容器自身,不是宿主机
解决:使用宿主机的 Docker 网桥 IP:
# 查看 Docker 网桥 IP
ip addr show docker0 | grep inet
# 输出类似:inet 172.17.0.1/16
# 在 channel.ts 中使用这个 IP
const FILE_SERVER_BASE_URL = "http://172.17.0.1:18790";
9.3 发送文件时附带多余消息
现象:AI 发送文件后,还会额外发一条 “I didn’t receive any text in your message”
原因:NapCat 发送文件后会产生一个空的消息回执事件,OpenClaw 将其当作用户消息处理
解决:在消息接收处添加空消息过滤:
// 忽略空消息(系统消息、回执等)
if (!text || text.trim() === "") {
console.log("[QQ] Ignoring empty message event");
return;
}
9.4 图片消息被当作空消息
现象:用户发送图片后,AI 回复"没收到文本"
原因:图片消息的 raw_message 为空,OpenClaw 认为没有收到内容
解决:为媒体消息添加描述性文本(见第4.2节)
9.5 base64 编码的文件用户看不懂
现象:AI 发送的文件内容是一堆 base64 编码的字符
原因:使用了 data: URL 发送文件
解决:在 sendMedia 中拒绝 data: URL,强制使用 HTTP URL:
if (mediaUrl.startsWith("data:")) {
return { channel: "qq", sent: false,
error: "Data URLs not supported. Save file and use HTTP URL." };
}
10. 完整代码汇总
10.1 QQ 插件文件结构
~/.openclaw/extensions/qq/src/
|- channel.ts # 主逻辑:消息收发、文件服务器集成
|- client.ts # OneBot11 WebSocket 客户端
|- config.ts # 配置 Schema
|- file-server.ts # HTTP 文件服务器
|- runtime.ts # 运行时管理
|- types.ts # OneBot11 类型定义
10.2 工具脚本文件结构
/root/qq-tools/
|- package.json # Node.js 依赖(ws)
|- send-message.js # 主动发送 QQ 消息
|- simple-cron.js # 定时任务管理
|- random-greetings.js # 随机问候生成
|- event-monitor.js # 事件监听与告警
10.3 OpenClaw 配置
~/.openclaw/openclaw.json 中的关键配置:
{
"plugins": {
"entries": {
"qq": { "enabled": true }
}
},
"channels": {
"qq": {
"enabled": true,
"wsUrl": "ws://127.0.0.1:3001",
"accessToken": "your_napcat_token"
}
},
"gateway": {
"port": 18789,
"controlUi": {
"allowInsecureAuth": true
}
}
}
完整的源代码文件内容较长,建议直接参考本文各章节中的代码片段进行实现。如需完整文件,可以在评论区留言。
11. 总结
通过本教程,我们在基础文字聊天的基础上,实现了以下高级功能:
| 功能 | 状态 | 关键技术 |
|---|---|---|
| 文件发送 | 已实现 | HTTP 文件服务器 + OneBot11 upload_private_file |
| 图片发送 | 已实现 | image 消息段 + 文件服务器 URL |
| 多媒体支持 | 已实现 | 完整的 OneBot11 消息段类型定义 |
| 接收图片 | 已实现 | 媒体消息描述性文本 + 空消息过滤 |
| 主动发消息 | 已实现 | 独立 WebSocket 连接 + send_private_msg |
| 定时任务 | 已实现 | Linux crontab + send-message.js |
| 随机问候 | 已实现 | 随机时间生成 + 分时段问候语库 |
| 事件告警 | 已实现 | 系统状态轮询 + 频率控制 + systemd 服务 |
核心架构要点:
- 文件发送的关键是内置 HTTP 文件服务器,解决 Docker 容器无法访问宿主机文件的问题
- 主动发送的关键是直接连接 NapCat 的 WebSocket API,绕过 OpenClaw Agent 的被动响应限制
- 定时任务的关键是利用 Linux crontab 的稳定性,配合 send-message.js 实现定时发送
- 事件告警的关键是后台轮询 + 频率控制,避免消息轰炸
后续可以继续扩展的方向:
- 接入邮件通知(IMAP 监听新邮件)
- 接入 GitHub Webhooks(代码提交通知)
- 接入天气 API(每天早上发送天气预报)
- 接入 RSS 订阅(新闻推送)
- AI 生成动态问候内容(结合 OpenClaw Agent)
作者说:从最初只能发文字,到现在能发文件、发图片、主动问候、服务器告警……一步步把 QQ 机器人打造成了真正的 AI 助手。希望这篇教程对你有帮助!如果有问题欢迎评论区交流。
相关文章:
- 上一篇:《【全网首发!】OpenClaw 接入 QQ 个人号完整教程》
- OpenClaw 官方文档
- NapCat OneBot11 API 文档
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)