guanghulab/glada/memory-store.js

393 lines
11 KiB
JavaScript
Raw Permalink Normal View History

/**
* GLADA · 双层记忆存储 · memory-store.js
*
* 映川的云端Agent记忆
* 第一层COS存储桶热存储快速读写实时状态
* 第二层代码仓库冷备份永久不丢Git历史可追溯
*
* 每次任务完成后
* 1. 先写COS热桶 毫秒级
* 2. 再写Git仓库 慢但永久下次唤醒时同步
*
* 每次唤醒时
* 1. 先读COS热桶 最新状态
* 2. COS不可用时回退读Git仓库 保底
*
* 版权国作登字-2026-A-00037559
* 签发霜砚 · AG-SY-WEB-001 · 受冰朔指令
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const ROOT = path.resolve(__dirname, '..');
// ==================== COS 配置 ====================
const COS_CONFIG = {
// 热桶:核心人格体大脑
bucket: process.env.COS_BUCKET_HOT || 'zy-core-bucket-1317346199',
region: process.env.ZY_COS_REGION || 'ap-guangzhou',
secretId: process.env.ZY_OSS_KEY || '',
secretKey: process.env.ZY_OSS_SECRET || '',
// 映川记忆在COS中的路径
basePath: 'glada/yingchuan-memory/',
};
// Git仓库中的记忆路径
const GIT_MEMORY_DIR = path.join(ROOT, 'glada', 'memory');
// ==================== COS 签名工具 ====================
/**
* 生成腾讯云COS请求签名
* 参考: https://cloud.tencent.com/document/product/436/7778
*/
function cosSign(method, cosPath, headers = {}, params = {}, expireSeconds = 600) {
const now = Math.floor(Date.now() / 1000);
const keyTime = `${now};${now + expireSeconds}`;
// SignKey
const signKey = crypto.createHmac('sha1', COS_CONFIG.secretKey)
.update(keyTime).digest('hex');
// HttpString
const httpString = [
method.toLowerCase(),
'/' + cosPath,
Object.keys(params).sort().map(k => `${encodeURIComponent(k).toLowerCase()}=${encodeURIComponent(params[k])}`).join('&'),
Object.keys(headers).sort().map(k => `${encodeURIComponent(k).toLowerCase()}=${encodeURIComponent(headers[k])}`).join('&'),
''
].join('\n');
// StringToSign
const sha1HttpString = crypto.createHash('sha1').update(httpString).digest('hex');
const stringToSign = `sha1\n${keyTime}\n${sha1HttpString}\n`;
// Signature
const signature = crypto.createHmac('sha1', signKey)
.update(stringToSign).digest('hex');
const headerList = Object.keys(headers).sort().map(k => encodeURIComponent(k).toLowerCase()).join(';');
const paramList = Object.keys(params).sort().map(k => encodeURIComponent(k).toLowerCase()).join(';');
return `q-sign-algorithm=sha1&q-ak=${COS_CONFIG.secretId}&q-sign-time=${keyTime}&q-key-time=${keyTime}&q-header-list=${headerList}&q-url-param-list=${paramList}&q-signature=${signature}`;
}
/**
* 检查COS是否可用密钥已配置
*/
function isCosAvailable() {
return !!(COS_CONFIG.secretId && COS_CONFIG.secretKey);
}
// ==================== COS 操作 ====================
/**
* 写入COS热桶
* @param {string} key - 对象键相对于basePath
* @param {Object|string} data - 要写入的数据
* @returns {Promise<{success: boolean, error?: string}>}
*/
async function writeCos(key, data) {
if (!isCosAvailable()) {
return { success: false, error: 'COS密钥未配置' };
}
const cosPath = COS_CONFIG.basePath + key;
const body = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
const host = `${COS_CONFIG.bucket}.cos.${COS_CONFIG.region}.myqcloud.com`;
const headers = {
'Host': host,
'Content-Type': 'application/json',
'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
};
const authorization = cosSign('PUT', cosPath, { host: host }, {});
try {
// 使用Node.js原生 fetchNode 18+)或 https
const https = require('https');
return new Promise((resolve) => {
const req = https.request({
hostname: host,
port: 443,
path: '/' + cosPath,
method: 'PUT',
headers: {
...headers,
'Authorization': authorization,
},
}, (res) => {
let responseBody = '';
res.on('data', chunk => responseBody += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ success: true });
} else {
resolve({ success: false, error: `COS PUT ${res.statusCode}: ${responseBody.substring(0, 200)}` });
}
});
});
req.on('error', (err) => {
resolve({ success: false, error: `COS网络错误: ${err.message}` });
});
req.write(body);
req.end();
});
} catch (err) {
return { success: false, error: `COS写入异常: ${err.message}` };
}
}
/**
* 从COS热桶读取
* @param {string} key - 对象键相对于basePath
* @returns {Promise<{success: boolean, data?: Object, error?: string}>}
*/
async function readCos(key) {
if (!isCosAvailable()) {
return { success: false, error: 'COS密钥未配置' };
}
const cosPath = COS_CONFIG.basePath + key;
const host = `${COS_CONFIG.bucket}.cos.${COS_CONFIG.region}.myqcloud.com`;
const headers = { 'Host': host };
const authorization = cosSign('GET', cosPath, { host: host }, {});
try {
const https = require('https');
return new Promise((resolve) => {
const req = https.request({
hostname: host,
port: 443,
path: '/' + cosPath,
method: 'GET',
headers: {
...headers,
'Authorization': authorization,
},
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode === 200) {
try {
resolve({ success: true, data: JSON.parse(body) });
} catch {
resolve({ success: true, data: body });
}
} else if (res.statusCode === 404) {
resolve({ success: false, error: 'NOT_FOUND' });
} else {
resolve({ success: false, error: `COS GET ${res.statusCode}` });
}
});
});
req.on('error', (err) => {
resolve({ success: false, error: `COS网络错误: ${err.message}` });
});
req.end();
});
} catch (err) {
return { success: false, error: `COS读取异常: ${err.message}` };
}
}
// ==================== Git仓库操作 ====================
/**
* 写入Git仓库本地文件
* @param {string} key - 文件名
* @param {Object|string} data - 要写入的数据
* @returns success: boolean, path?: string, error?: string
*/
function writeGit(key, data) {
try {
if (!fs.existsSync(GIT_MEMORY_DIR)) {
fs.mkdirSync(GIT_MEMORY_DIR, { recursive: true });
}
const filePath = path.join(GIT_MEMORY_DIR, key);
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
fs.writeFileSync(filePath, content, 'utf-8');
return { success: true, path: filePath };
} catch (err) {
return { success: false, error: `Git写入失败: ${err.message}` };
}
}
/**
* 从Git仓库本地文件读取
* @param {string} key - 文件名
* @returns success: boolean, data?: Object, error?: string
*/
function readGit(key) {
try {
const filePath = path.join(GIT_MEMORY_DIR, key);
if (!fs.existsSync(filePath)) {
return { success: false, error: 'NOT_FOUND' };
}
const content = fs.readFileSync(filePath, 'utf-8');
try {
return { success: true, data: JSON.parse(content) };
} catch {
return { success: true, data: content };
}
} catch (err) {
return { success: false, error: `Git读取失败: ${err.message}` };
}
}
// ==================== 双层记忆接口 ====================
/**
* 双层写入COS热桶 + Git仓库
* 先写COS再写Git保底
*
* @param {string} key - 记忆键名 'session-latest.json'
* @param {Object} data - 记忆数据
* @returns {Promise<{cos: Object, git: Object}>}
*/
async function save(key, data) {
// 添加元数据
const enrichedData = {
...data,
_meta: {
saved_at: new Date().toISOString(),
persona: 'PER-YC-CHAT-001',
persona_name: '映川',
dual_layer: true,
}
};
// 第一层COS热桶
const cosResult = await writeCos(key, enrichedData);
// 第二层Git仓库
const gitResult = writeGit(key, enrichedData);
return { cos: cosResult, git: gitResult };
}
/**
* 双层读取先读COS最新COS不可用时回退Git
*
* @param {string} key - 记忆键名
* @returns {Promise<{data: Object|null, source: 'cos'|'git'|null, error?: string}>}
*/
async function load(key) {
// 先试COS
const cosResult = await readCos(key);
if (cosResult.success) {
return { data: cosResult.data, source: 'cos' };
}
// COS不可用回退Git
const gitResult = readGit(key);
if (gitResult.success) {
return { data: gitResult.data, source: 'git' };
}
// 两层都没有
return { data: null, source: null, error: `COS: ${cosResult.error} | Git: ${gitResult.error}` };
}
// ==================== 高层记忆接口 ====================
/**
* 保存任务执行记忆每次任务完成后调用
*/
async function saveTaskMemory(taskId, memory) {
const key = `tasks/${taskId}.json`;
return save(key, memory);
}
/**
* 保存会话记忆最新状态
*/
async function saveSessionMemory(sessionData) {
// 最新会话快照
const latestResult = await save('session-latest.json', sessionData);
// 同时存一份按时间戳的归档
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const archiveResult = await save(`sessions/${ts}.json`, sessionData);
return { latest: latestResult, archive: archiveResult };
}
/**
* 加载最新会话记忆唤醒时调用
*/
async function loadLatestSession() {
return load('session-latest.json');
}
/**
* 保存技能记忆蕴顱后的经验
*/
async function saveSkillMemory(skillId, skillData) {
return save(`skills/${skillId}.json`, skillData);
}
/**
* 保存反思记忆
*/
async function saveReflection(reflectionData) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
return save(`reflections/${ts}.json`, reflectionData);
}
/**
* 获取记忆存储状态
*/
function getStorageStatus() {
return {
cos_available: isCosAvailable(),
cos_bucket: COS_CONFIG.bucket,
cos_region: COS_CONFIG.region,
cos_path: COS_CONFIG.basePath,
git_path: GIT_MEMORY_DIR,
git_exists: fs.existsSync(GIT_MEMORY_DIR),
};
}
module.exports = {
// 底层
writeCos,
readCos,
writeGit,
readGit,
isCosAvailable,
// 双层
save,
load,
// 高层
saveTaskMemory,
saveSessionMemory,
loadLatestSession,
saveSkillMemory,
saveReflection,
getStorageStatus,
// 配置
COS_CONFIG,
GIT_MEMORY_DIR,
};