小白龙 Bailongma - 初始提交
自主操作员与思考搭档系统。 包含 orchestrator-v2 多Agent编排层、后台意识引擎、记忆系统、ACUI 组件。
This commit is contained in:
205
orchestrator-v2/session-store.js
Normal file
205
orchestrator-v2/session-store.js
Normal file
@@ -0,0 +1,205 @@
|
||||
const initSqlJs = require('sql.js');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
class SessionStore {
|
||||
constructor(dbDir) {
|
||||
this.dbDir = dbDir;
|
||||
this.dbPath = path.join(dbDir, 'sessions.sqlite');
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!fs.existsSync(this.dbDir)) fs.mkdirSync(this.dbDir, { recursive: true });
|
||||
const SQL = await initSqlJs();
|
||||
if (fs.existsSync(this.dbPath)) {
|
||||
const buf = fs.readFileSync(this.dbPath);
|
||||
this.db = new SQL.Database(buf);
|
||||
} else {
|
||||
this.db = new SQL.Database();
|
||||
}
|
||||
this.db.run('PRAGMA journal_mode=WAL');
|
||||
this.db.run('PRAGMA foreign_keys=ON');
|
||||
this.db.run("CREATE TABLE IF NOT EXISTS sessions (" +
|
||||
"id TEXT PRIMARY KEY, parent_id TEXT, status TEXT DEFAULT 'created', " +
|
||||
"task TEXT, context TEXT, result TEXT, summary TEXT, " +
|
||||
"token_count INTEGER DEFAULT 0, is_compressed INTEGER DEFAULT 0, " +
|
||||
"events TEXT DEFAULT '[]', " +
|
||||
"created_at TEXT DEFAULT (datetime('now')), " +
|
||||
"updated_at TEXT DEFAULT (datetime('now')), " +
|
||||
"FOREIGN KEY (parent_id) REFERENCES sessions(id))");
|
||||
this.db.run("CREATE TABLE IF NOT EXISTS messages (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, " +
|
||||
"role TEXT NOT NULL, content TEXT, tool_calls TEXT, tool_call_id TEXT, " +
|
||||
"token_count INTEGER DEFAULT 0, " +
|
||||
"created_at TEXT DEFAULT (datetime('now')), " +
|
||||
"FOREIGN KEY (session_id) REFERENCES sessions(id))");
|
||||
this._save();
|
||||
}
|
||||
|
||||
_save() {
|
||||
const data = this.db.export();
|
||||
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||
fs.writeFileSync(this.dbPath, buf);
|
||||
}
|
||||
|
||||
createSession(id, task, context, parentId) {
|
||||
if (parentId === undefined) parentId = null;
|
||||
this.db.run('INSERT OR REPLACE INTO sessions (id, parent_id, task, context, status) VALUES (?, ?, ?, ?, ?)',
|
||||
[id, parentId, task, JSON.stringify(context || {}), 'created']);
|
||||
this._save();
|
||||
}
|
||||
|
||||
getSession(id) {
|
||||
const stmt = this.db.exec('SELECT rowid, * FROM sessions WHERE id = ?', [id]);
|
||||
if (!stmt.length || !stmt[0].values.length) return null;
|
||||
const cols = stmt[0].columns;
|
||||
const vals = stmt[0].values[0];
|
||||
return cols.reduce(function(o, c, i) { o[c] = vals[i]; return o; }, {});
|
||||
}
|
||||
|
||||
updateSession(id, updates) {
|
||||
var allowed = ['status','task','context','result','summary','token_count','is_compressed','parent_id'];
|
||||
var sets = [];
|
||||
var params = [];
|
||||
for (var key in updates) {
|
||||
if (allowed.indexOf(key) >= 0) {
|
||||
sets.push(key + ' = ?');
|
||||
params.push(typeof updates[key] === 'object' ? JSON.stringify(updates[key]) : updates[key]);
|
||||
}
|
||||
}
|
||||
if (!sets.length) return;
|
||||
sets.push("updated_at = datetime('now')");
|
||||
params.push(id);
|
||||
this.db.run('UPDATE sessions SET ' + sets.join(', ') + ' WHERE id = ?', params);
|
||||
this._save();
|
||||
}
|
||||
|
||||
updateStatus(id, status, result) {
|
||||
if (result) {
|
||||
this.db.run("UPDATE sessions SET status = ?, result = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
[status, JSON.stringify(result), id]);
|
||||
} else {
|
||||
this.db.run("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
[status, id]);
|
||||
}
|
||||
this._save();
|
||||
}
|
||||
|
||||
deleteSession(id) {
|
||||
this.db.run('DELETE FROM messages WHERE session_id = ?', [id]);
|
||||
this.db.run('DELETE FROM sessions WHERE id = ?', [id]);
|
||||
this._save();
|
||||
}
|
||||
|
||||
listSessions(limit, offset) {
|
||||
if (limit === undefined) limit = 20;
|
||||
if (offset === undefined) offset = 0;
|
||||
var stmt = this.db.exec('SELECT id, parent_id, status, task, summary, token_count, is_compressed, created_at, updated_at FROM sessions ORDER BY created_at DESC LIMIT ' + limit + ' OFFSET ' + offset);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
getSessionChain(id) {
|
||||
var chain = [];
|
||||
var current = this.getSession(id);
|
||||
while (current) {
|
||||
chain.unshift(current);
|
||||
if (current.parent_id) current = this.getSession(current.parent_id);
|
||||
else break;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
appendEvent(id, event) {
|
||||
var row = this.db.exec('SELECT events FROM sessions WHERE id = ?', [id]);
|
||||
if (row.length > 0) {
|
||||
var events = JSON.parse(row[0].values[0][0] || '[]');
|
||||
events.push(event);
|
||||
this.db.run("UPDATE sessions SET events = ?, updated_at = datetime('now') WHERE id = ?", [JSON.stringify(events), id]);
|
||||
this._save();
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(sessionId, role, content, extras) {
|
||||
if (!extras) extras = {};
|
||||
this.db.run('INSERT INTO messages (session_id, role, content, tool_calls, tool_call_id, token_count) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[sessionId, role, content, extras.toolCalls ? JSON.stringify(extras.toolCalls) : null, extras.toolCallId || null, extras.tokenCount || 0]);
|
||||
this._save();
|
||||
if (extras.tokenCount) {
|
||||
this.db.run("UPDATE sessions SET token_count = token_count + ?, updated_at = datetime('now') WHERE id = ?", [extras.tokenCount, sessionId]);
|
||||
this._save();
|
||||
}
|
||||
}
|
||||
|
||||
getMessages(sessionId, limit) {
|
||||
if (limit === undefined) limit = 100;
|
||||
var stmt = this.db.exec('SELECT id, role, content, tool_calls, token_count, created_at FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?', [sessionId, limit]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
countMessages(sessionId) {
|
||||
var stmt = this.db.exec('SELECT COUNT(*) as cnt FROM messages WHERE session_id = ?', [sessionId]);
|
||||
return stmt.length ? stmt[0].values[0][0] : 0;
|
||||
}
|
||||
|
||||
getTotalTokenCount(sessionId) {
|
||||
var stmt = this.db.exec('SELECT SUM(token_count) as total FROM messages WHERE session_id = ?', [sessionId]);
|
||||
return (stmt.length && stmt[0].values[0][0]) ? stmt[0].values[0][0] : 0;
|
||||
}
|
||||
|
||||
searchSessions(query, limit) {
|
||||
if (limit === undefined) limit = 10;
|
||||
var likeQ = '%' + query + '%';
|
||||
var stmt = this.db.exec('SELECT id, task, summary, status, created_at FROM sessions WHERE task LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT ?', [likeQ, likeQ, limit]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
searchMessages(query, limit) {
|
||||
if (limit === undefined) limit = 20;
|
||||
var likeQ = '%' + query + '%';
|
||||
var stmt = this.db.exec('SELECT session_id, role, content FROM messages WHERE content LIKE ? ORDER BY id DESC LIMIT ?', [likeQ, limit]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
compressSession(id, summary) {
|
||||
this.db.run("UPDATE sessions SET is_compressed = 1, summary = ?, status = 'compressed', updated_at = datetime('now') WHERE id = ?", [summary, id]);
|
||||
this._save();
|
||||
}
|
||||
|
||||
getCompressibleSessions(threshold) {
|
||||
if (threshold === undefined) threshold = 100;
|
||||
var stmt = this.db.exec("SELECT s.id, s.task, COUNT(m.id) as msg_count, s.created_at FROM sessions s LEFT JOIN messages m ON m.session_id = s.id WHERE s.is_compressed = 0 AND s.status = 'completed' GROUP BY s.id HAVING msg_count > ? ORDER BY msg_count DESC", [threshold]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
getStats() {
|
||||
var stmt = this.db.exec("SELECT COUNT(*) as total_sessions, SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) as failed, SUM(token_count) as total_tokens, SUM(CASE WHEN is_compressed=1 THEN 1 ELSE 0 END) as compressed FROM sessions");
|
||||
if (!stmt.length) return {};
|
||||
var vals = stmt[0].values[0];
|
||||
var cols = stmt[0].columns;
|
||||
return cols.reduce(function(o, c, i) { o[c] = vals[i]; return o; }, {});
|
||||
}
|
||||
|
||||
searchEverything(query, limit) {
|
||||
if (limit === undefined) limit = 10;
|
||||
var results = { sessions: [], messages: [] };
|
||||
try { results.sessions = this.searchSessions(query, limit); results.messages = this.searchMessages(query, limit * 2); } catch (e) {}
|
||||
return results;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.db) { this._save(); this.db.close(); }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SessionStore;
|
||||
Reference in New Issue
Block a user