guanghulab/glada/memory-store.js
2026-05-10 13:12:44 +08:00

393 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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,
};