You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
5.4 KiB
182 lines
5.4 KiB
|
1 month ago
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* OpenClaw Mem0 Integration Plugin
|
||
|
|
* 将 mem0 拦截器挂载到 OpenClaw 主对话生命周期
|
||
|
|
*/
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
// Python 子进程执行器
|
||
|
|
const { spawn } = require('child_process');
|
||
|
|
|
||
|
|
class Mem0Plugin {
|
||
|
|
constructor(config) {
|
||
|
|
this.config = config;
|
||
|
|
this.enabled = true;
|
||
|
|
this.pythonPath = config.pythonPath || 'python3';
|
||
|
|
this.scriptPath = path.join(__dirname, 'mem0_integration.py');
|
||
|
|
this.initialized = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 初始化 mem0(在 OpenClaw 启动时调用)
|
||
|
|
*/
|
||
|
|
async onLoad() {
|
||
|
|
if (!this.enabled) return;
|
||
|
|
|
||
|
|
console.log('[Mem0] 初始化记忆系统...');
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 调用 Python 脚本初始化 mem0
|
||
|
|
const result = await this._executePython('init');
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
this.initialized = true;
|
||
|
|
console.log('🟢 Mem0 生产环境集成完毕,等待 Telegram 消息注入');
|
||
|
|
} else {
|
||
|
|
console.error('[Mem0] 初始化失败:', result.error);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[Mem0] 初始化异常:', error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Pre-Hook: 在 LLM 生成前检索记忆
|
||
|
|
*/
|
||
|
|
async preLLM(userMessage, context) {
|
||
|
|
if (!this.enabled || !this.initialized) return null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await this._executePython('search', {
|
||
|
|
query: userMessage,
|
||
|
|
user_id: context.user_id || 'default',
|
||
|
|
agent_id: context.agent_id || 'general'
|
||
|
|
});
|
||
|
|
|
||
|
|
if (result.success && result.memories && result.memories.length > 0) {
|
||
|
|
console.log(`[Mem0] Pre-Hook: 检索到 ${result.memories.length} 条记忆`);
|
||
|
|
return this._formatMemories(result.memories);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[Mem0] Pre-Hook 失败:', error.message);
|
||
|
|
return null; // 静默失败,不影响对话
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Post-Hook: 在响应后异步写入记忆
|
||
|
|
*/
|
||
|
|
async postResponse(userMessage, assistantMessage, context) {
|
||
|
|
if (!this.enabled || !this.initialized) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await this._executePython('add', {
|
||
|
|
user_message: userMessage,
|
||
|
|
assistant_message: assistantMessage,
|
||
|
|
user_id: context.user_id || 'default',
|
||
|
|
agent_id: context.agent_id || 'general'
|
||
|
|
}, false); // 不等待结果(异步)
|
||
|
|
|
||
|
|
console.log('[Mem0] Post-Hook: 已提交对话到记忆队列');
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[Mem0] Post-Hook 失败:', error.message);
|
||
|
|
// 静默失败
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 执行 Python 脚本
|
||
|
|
*/
|
||
|
|
_executePython(action, data = {}, waitForResult = true) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const args = [this.scriptPath, action];
|
||
|
|
if (data) {
|
||
|
|
args.push(JSON.stringify(data));
|
||
|
|
}
|
||
|
|
|
||
|
|
const proc = spawn(this.pythonPath, args, {
|
||
|
|
cwd: __dirname,
|
||
|
|
env: process.env
|
||
|
|
});
|
||
|
|
|
||
|
|
let stdout = '';
|
||
|
|
let stderr = '';
|
||
|
|
|
||
|
|
proc.stdout.on('data', (data) => {
|
||
|
|
stdout += data.toString();
|
||
|
|
});
|
||
|
|
|
||
|
|
proc.stderr.on('data', (data) => {
|
||
|
|
stderr += data.toString();
|
||
|
|
});
|
||
|
|
|
||
|
|
proc.on('close', (code) => {
|
||
|
|
if (code === 0) {
|
||
|
|
try {
|
||
|
|
const result = JSON.parse(stdout);
|
||
|
|
resolve({ success: true, ...result });
|
||
|
|
} catch {
|
||
|
|
resolve({ success: true, raw: stdout });
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
resolve({ success: false, error: stderr || `Exit code: ${code}` });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
proc.on('error', reject);
|
||
|
|
|
||
|
|
// 如果不等待结果,立即返回
|
||
|
|
if (!waitForResult) {
|
||
|
|
resolve({ success: true, async: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 格式化记忆为 Prompt
|
||
|
|
*/
|
||
|
|
_formatMemories(memories) {
|
||
|
|
if (!memories || memories.length === 0) return '';
|
||
|
|
|
||
|
|
let prompt = '\n\n=== 相关记忆 ===\n';
|
||
|
|
memories.forEach((mem, i) => {
|
||
|
|
prompt += `${i + 1}. ${mem.memory}`;
|
||
|
|
if (mem.created_at) {
|
||
|
|
prompt += ` (记录于:${mem.created_at})`;
|
||
|
|
}
|
||
|
|
prompt += '\n';
|
||
|
|
});
|
||
|
|
prompt += '===============\n';
|
||
|
|
|
||
|
|
return prompt;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 导出插件实例
|
||
|
1 month ago
|
const mem0Plugin = new Mem0Plugin({
|
||
|
1 month ago
|
pythonPath: process.env.MEM0_PYTHON_PATH || 'python3'
|
||
|
|
});
|
||
|
1 month ago
|
|
||
|
|
// OpenClaw 插件生命周期导出
|
||
|
|
module.exports = {
|
||
|
|
async register(ctx) {
|
||
|
|
console.log('[Mem0] 注册插件...');
|
||
|
|
await mem0Plugin.onLoad();
|
||
|
|
return mem0Plugin;
|
||
|
|
},
|
||
|
|
|
||
|
|
async activate(ctx) {
|
||
|
|
console.log('[Mem0] 激活插件...');
|
||
|
|
return mem0Plugin;
|
||
|
|
},
|
||
|
|
|
||
|
|
// 导出实例方法供 OpenClaw 调用
|
||
|
|
preLLM: async (userMessage, context) => await mem0Plugin.preLLM(userMessage, context),
|
||
|
|
postResponse: async (userMessage, assistantMessage, context) => await mem0Plugin.postResponse(userMessage, assistantMessage, context),
|
||
|
|
...mem0Plugin
|
||
|
|
};
|