本文摘要
详细介绍如何开发一个 OpenClaw 插件来实现会话历史记录功能。
OpenClaw 是一个强大的 AI agent 框架,支持自定义插件扩展功能。本文将介绍如何构建一个实用插件:会话历史记录器,将对话记录持久化到 SQLite,便于后续分析和搜索。
为什么需要这个插件?
默认情况下,AI agent 对话存在于内存中,最终会被压缩或丢失。为了调试、分析或知识提取,你需要持久化存储。会话记录器插件在压缩发生前捕获每条消息。
插件架构
OpenClaw 插件使用基于钩子的架构。我们需要的关键钩子:
// 注册钩子
api.on("before_compaction", async (event, ctx) => {
// 压缩前保存所有记录
await autoImport(event, ctx, "before_compaction");
});
api.on("after_compaction", async (event, ctx) => {
// 导入压缩期间生成的新记录
await autoImport(event, ctx, "after_compaction");
});
api.on("agent_end", async (event, ctx) => {
// agent 结束时的最终捕获
await autoImport(event, ctx, "agent_end");
});
增量导入策略
简单的方法是每次重新导入整个记录。相反,我们跟踪每个会话已导入的行数:
// 跟踪每个会话的已导入行数
const lineCounts = {};
function getImportedLines(db, sessionId) {
if (!lineCounts[sessionId]) {
const row = db.prepare(
"SELECT COUNT(*) as cnt FROM transcript_entries WHERE session_id = ?"
).get(sessionId);
lineCounts[sessionId] = row ? row.cnt : 0;
}
return lineCounts[sessionId];
}
这样,每次钩子调用只处理新条目——对于长时间运行的会话效率更高。
SQLite 表结构
简单但有效:
CREATE TABLE IF NOT EXISTS transcript_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
kind TEXT DEFAULT "message",
content TEXT,
model TEXT,
timestamp TEXT,
created_at TEXT DEFAULT (datetime("now"))
);
CREATE INDEX IF NOT EXISTS idx_session_id
ON transcript_entries(session_id);
CREATE INDEX IF NOT EXISTS idx_timestamp
ON transcript_entries(timestamp);
关键经验
- 钩子时机很重要 —
before_compaction至关重要,因为这是在内容被摘要之前捕获完整对话的最后机会。 - 增量导入 防止重复数据,减少每次钩子触发时的 I/O。
- 良好的日志 — 当零条记录被导入时,记录路径和现有记录数以便快速诊断问题。
agent_end钩子 作为安全网,捕获不触发压缩的会话。
这个插件已在生产环境运行数天,可靠地捕获对话历史,即使经历多次压缩周期。SQLite 数据库使得用标准 SQL 查询历史对话变得简单。
Full-Stack Developer with 10+ years of experience, specializing in QT C++ desktop application development and AI Agent systems.





