前言

最近在做作业批改系统,需要实现老师通过机器人向学生发送成绩和通知的功能。本文记录如何将 OpenClaw 与 OneBot 协议对接,实现 QQ 私聊和群消息的自动发送。

背景

为什么选择 OneBot?

OpenClaw 原生支持 QQ 频道机器人,但频道机器人无法进入普通 QQ 群。为了覆盖更多使用场景,选择通过 OneBot 协议作为中转:

1
OpenClaw → OneBot 插件 → OneBot 实现(LLOneBot)→ QQ 群/好友

技术栈

  • OpenClaw: AI 网关,负责消息路由和 Agent 管理
  • OneBot 插件: @kirigaya/openclaw-onebot
  • OneBot 后端: LLOneBot(基于 NTQQ 的 OneBot 11 实现)

安装与配置

1. 安装 OneBot 插件

1
openclaw plugin install @kirigaya/openclaw-onebot --allow-dangerous

注意:该插件包含 child_process 调用,需要 --allow-dangerous 参数。

2. 配置 OneBot 连接

编辑 ~/.openclaw/openclaw.json,添加 onebot 通道配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"channels": {
"onebot": {
"type": "forward-websocket",
"host": "127.0.0.1",
"port": 3001,
"accessToken": "your-token",
"enabled": true,
"requireMention": true,
"renderMarkdownToPlain": true
}
}
}

3. 配置 Agent 绑定

将 onebot 通道绑定到指定 agent:

1
2
3
4
5
6
7
8
9
10
11
{
"bindings": [
{
"type": "route",
"agentId": "jarvis",
"match": {
"channel": "onebot"
}
}
]
}

4. 重启 Gateway

1
openclaw gateway restart

消息发送

私聊消息

通过 jarvis agent 发送私聊消息:

1
openclaw agent --agent jarvis -m "通过 onebot 给 QQ 891878708 发私聊消息:你好"

群消息

发送群消息:

1
openclaw agent --agent jarvis -m "通过 onebot 给群 875860223 发送群消息:大家好"

封装 Skill

为了便于复用,创建了 onebot-messenger skill:

1
2
skills/onebot-messenger/
└── SKILL.md

skill 中记录了完整的使用方法和作业系统集成示例:

  • 发送成绩通知
  • 群发作业提醒
  • 发送批改反馈

性能优化

问题(还没解决!!)

使用 openclaw agent 命令发送消息耗时约 1 分钟,主要卡在:

  1. 进程启动
  2. 插件重复加载
  3. WebSocket 连接初始化

解决方案

  1. 直接使用 HTTP API(推荐)

    • 绕过 OpenClaw 命令行开销
    • 响应时间 < 1 秒
  2. 编写常驻脚本

    • 保持 WebSocket 连接
    • 直接调用 OneBot 接口

总结

通过 OpenClaw + OneBot 的组合,可以实现:

  • ✅ QQ 私聊消息自动发送
  • ✅ QQ 群消息自动发送
  • ✅ 与作业系统深度集成
  • ⚠️ 需要优化性能(建议使用 HTTP API 直接调用)

后续计划:

  1. 编写常驻 Python 脚本提升发送速度
  2. 封装更多消息类型(图片、@成员等)
  3. 实现消息模板系统

参考


续:作业批改系统集成 RAG 与 QQ 消息发送

2026-04-22 更新

在完成了基础的 OneBot 消息发送功能后,我开始思考如何将其与作业批改系统的数据结合起来。目标是让老师能够通过自然语言指令,让 AI 自动查询作业数据并发送到 QQ 群。

需求分析

需要实现的功能:

  1. 数据向量化:将班级作业数据(成绩分布、学生学情、高频错题等)存入向量库
  2. RAG 检索:老师可以通过自然语言查询数据
  3. 自动发送:查询结果自动格式化为成绩单,通过 OneBot 发送到指定 QQ 群

技术方案设计

方案一:直接查询数据库

前端收集数据 → 后端查询数据库 → 格式化消息 → 调用 OpenClaw 发送

缺点

  • 老师需要明确知道作业 ID
  • 不够灵活,无法支持复杂查询

方案二:RAG 向量检索(最终选择)

1
2
3
PageFour 页面数据 → 导出文本 → 切割分块 → 向量化 → 存入 PostgreSQL + pgvector

老师提问 → RAG 检索 → 找到相关数据 → 生成回答/发送 QQ 群

优点

  • 支持自然语言查询:”3班 Java 作业成绩如何?”
  • 无需记住具体 ID
  • 可以组合多个条件查询

实现过程

1. 数据库设计

复用现有的 document_chunk 表存储仪表盘数据:

1
2
3
4
5
6
-- 添加元数据字段支持
ALTER TABLE document_chunk ADD COLUMN IF NOT EXISTS metadata JSONB;

-- 创建索引
CREATE INDEX idx_document_chunk_metadata_class_id ON document_chunk((metadata->>'classId'));
CREATE INDEX idx_document_chunk_metadata_type ON document_chunk((metadata->>'type'));

表结构说明:

  • document 表:存储文件元信息(原文件上传用)
  • document_chunk 表:存储切割后的文档块 + 向量(RAG 检索用)

2. 后端实现

接口层 (DashboardController.java):

1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/upload-to-rag")
public Result uploadToRag(@RequestBody DashboardUploadDTO data, HttpServletRequest request) {
// 检查今天是否已上传(防重复)
String docIdPrefix = "dashboard_" + data.getClassId();
if (vectorStoreService.existsToday(docIdPrefix)) {
return Result.error(409, "今天已上传过该班级数据");
}

// 执行上传
Map<String, Object> result = dashboardRagService.uploadDashboard(data);
return Result.success(result);
}

服务层 (DashboardRagServiceImpl.java):

核心逻辑:

  1. 格式化仪表盘数据为 Markdown 文本
  2. 使用 SmartChunkService 智能切割
  3. 生成 embedding 向量
  4. 存入数据库并附加元数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public Map<String, Object> uploadDashboard(DashboardUploadDTO data) {
String docId = "dashboard_" + data.getClassId() + "_" + today + "_" + timestamp;

// 1. 格式化数据
String content = formatDashboardContent(data);

// 2. 智能切割
List<DocumentChunk> chunks = chunkService.chunk(content, config);

// 3. 添加元数据标签
Map<String, Object> metadata = new HashMap<>();
metadata.put("type", "dashboard");
metadata.put("classId", data.getClassId());
metadata.put("className", data.getClassName());
metadata.put("date", today);
metadata.put("dataVersion", "v1");

chunks.forEach(chunk -> chunk.setMetadata(metadata));

// 4. 生成向量并保存
chunks.forEach(chunk -> {
float[] embedding = embeddingService.embed(chunk.getContent());
chunk.setEmbedding(embedding);
});

vectorStoreService.saveChunks(docId, chunks);

return Map.of("chunkCount", chunks.size(), "documentId", docId);
}

3. 前端实现

PageFour.vue 添加上传按钮:

1
2
3
4
5
6
7
8
9
10
<button 
class="upload-rag-btn"
@click="uploadToRag"
:disabled="!selectedClass || uploadingToRag || ragUploadedToday"
:class="{ 'uploaded': ragUploadedToday }"
>
<span v-if="uploadingToRag">⏳ 上传中...</span>
<span v-else-if="ragUploadedToday">✅ 今日已上传</span>
<span v-else>🚀 上传到知识库</span>
</button>

一键上传逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async uploadToRag() {
// 收集当前页面所有数据
const data = {
classId: this.selectedClass,
className: this.classList.find(c => c.id === this.selectedClass)?.name,
metrics: this.metrics,
scoreDistribution: this.scoreDistribution,
knowledgeMastery: this.knowledgeMastery,
frequentErrors: this.frequentErrors,
students: this.students,
exportTime: new Date().toISOString()
}

// 发送到后端处理
const response = await fetch(`${this.apiBaseUrl}/dashboard/upload-to-rag`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})

// 上传成功后,按钮显示"今日已上传"
this.ragUploadedToday = true
}

遇到的问题与解决方案

问题 1:格式化字符串类型不匹配

错误日志

1
java.util.IllegalFormatConversionException: f != java.lang.Integer

原因StudentOverviewDTO.avgScoreInteger 类型,但代码中使用了 %.1f 格式化。

解决

1
2
3
4
5
// 错误
sb.append(String.format("- %s: 平均分%.1f...", student.getName(), student.getAvgScore()));

// 正确
sb.append(String.format("- %s: 平均分%d...", student.getName(), student.getAvgScore()));

问题 2:是否需要额外存储文档块?

疑问:之前没有 pgvector 扩展时,需要额外存储文档块吗?

解答

  • 之前:只能用 embedding TEXT 字段存逗号分隔的向量,检索时全表扫描,Java 代码计算余弦相似度
  • 现在:PostgreSQL 安装了 pgvector 扩展后,有 embedding_vec 字段(vector 类型),支持 SQL 的 <=> 操作符直接做向量相似度搜索

document_chunk 表本身就存储了:

  • content:文档块文本内容
  • embedding_vec:向量(pgvector 专用)

不需要额外存储。

问题 3:Service 接口与实现分离

规范:项目采用接口 + 实现类的结构 (Service/Service/ServiceImpl/)

实现

1
2
3
4
Service/
├── DashboardRagService.java (接口)
└── ServiceImpl/
└── DashboardRagServiceImpl.java (实现)

优化点

  1. 防重复上传:每天每个班级只能上传一次,避免重复数据
  2. 元数据标签:包含 type、classId、className、date 等,便于精确检索
  3. 状态显示:前端按钮显示”今日已上传”,防止误操作

使用流程

  1. 老师进入 PageFour(教学数据中心)页面
  2. 选择班级,查看数据
  3. 点击”🚀 上传到知识库”按钮
  4. 数据自动格式化、切割、向量化、存入数据库
  5. 老师可以通过自然语言查询:
    • “3班有哪些学生需要关注?”
    • “把 Java 作业成绩单发到 3 班群”

后续计划

  1. 实现消息发送接口,打通 RAG → OneBot 的完整链路
  2. 支持更多数据类型(作业详情、错题分析等)
  3. 添加删除/更新知识库数据的功能

总结

通过将作业数据向量化存入 RAG,结合 OneBot 消息发送,实现了:

  • ✅ 自然语言查询班级数据
  • ✅ 一键上传数据到知识库
  • ✅ 防重复上传机制
  • ✅ 元数据标签支持精确检索

下一步是完成消息自动发送功能,实现”查询 → 生成消息 → 发送到 QQ 群”的完整闭环。

参考