2026-05-10 13:12:44 +08:00

272 lines
7.5 KiB
JavaScript
Raw Permalink 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.

/**
* ═══════════════════════════════════════════════════════════
* 🧠 铸渊运维守卫 · 永久记忆系统
* ═══════════════════════════════════════════════════════════
*
* 编号: ZY-OPS-MEM-001
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 存储层级:
* 1. 文件存储始终可用JSON文件
* 2. PostgreSQL可选连接到 age_os 数据库时启用)
*
* 记忆类型:
* - event: 巡检事件(健康检查、修复、告警)
* - repair: 修复记录
* - ticket: 工单记录
* - chat: 对话历史
* - config: 配置变更
*/
'use strict';
const fs = require('fs');
const path = require('path');
// ── 文件存储路径 ──────────────────────────
const MEMORY_DIR = process.env.OPS_MEMORY_DIR ||
path.join(__dirname, 'data');
const EVENTS_FILE = path.join(MEMORY_DIR, 'events.jsonl');
const TICKETS_FILE = path.join(MEMORY_DIR, 'tickets.json');
const STATS_FILE = path.join(MEMORY_DIR, 'stats.json');
// 确保目录存在
function ensureDir() {
try {
if (!fs.existsSync(MEMORY_DIR)) {
fs.mkdirSync(MEMORY_DIR, { recursive: true });
}
} catch { /* ignore if already exists */ }
}
// ── 事件日志(追加写入 JSONL ─────────────
function logEvent(type, data) {
ensureDir();
const entry = {
id: `EVT-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
type,
timestamp: new Date().toISOString(),
...data
};
try {
fs.appendFileSync(EVENTS_FILE, JSON.stringify(entry) + '\n');
} catch (err) {
console.error(`[Memory] 写入事件失败: ${err.message}`);
}
return entry;
}
/**
* 查询最近的事件
* @param {Object} filter - { type, since, limit }
*/
function queryEvents(filter = {}) {
const { type, since, limit = 50 } = filter;
try {
if (!fs.existsSync(EVENTS_FILE)) return [];
const lines = fs.readFileSync(EVENTS_FILE, 'utf-8')
.split('\n')
.filter(l => l.trim());
let events = [];
// 从末尾往前读,提高效率
for (let i = lines.length - 1; i >= 0 && events.length < limit * 2; i--) {
try {
events.push(JSON.parse(lines[i]));
} catch { /* skip malformed lines */ }
}
if (type) {
events = events.filter(e => e.type === type);
}
if (since) {
const sinceTime = new Date(since).getTime();
events = events.filter(e => new Date(e.timestamp).getTime() >= sinceTime);
}
return events.slice(0, limit);
} catch {
return [];
}
}
// ── 工单管理 ──────────────────────────────
function loadTickets() {
try {
if (!fs.existsSync(TICKETS_FILE)) return [];
return JSON.parse(fs.readFileSync(TICKETS_FILE, 'utf-8'));
} catch {
return [];
}
}
function saveTickets(tickets) {
ensureDir();
fs.writeFileSync(TICKETS_FILE, JSON.stringify(tickets, null, 2));
}
function createTicket(data) {
const tickets = loadTickets();
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const todayCount = tickets.filter(t => t.id.includes(dateStr)).length;
const ticket = {
id: `OPS-${dateStr}-${String(todayCount + 1).padStart(3, '0')}`,
title: data.title,
description: data.description,
severity: data.severity || 'medium',
category: data.category || 'unknown',
source: 'ops-agent',
direction: data.direction || '待判断',
status: 'open',
diagnosis: data.diagnosis || '',
suggestedFix: data.suggestedFix || '',
relatedService: data.relatedService || '',
autoRepairAttempts: data.autoRepairAttempts || 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
resolvedAt: null
};
tickets.push(ticket);
saveTickets(tickets);
// 同时记为事件
logEvent('ticket', { ticketId: ticket.id, title: ticket.title, severity: ticket.severity });
return ticket;
}
function updateTicket(ticketId, updates) {
const tickets = loadTickets();
const idx = tickets.findIndex(t => t.id === ticketId);
if (idx === -1) return null;
Object.assign(tickets[idx], updates, { updatedAt: new Date().toISOString() });
if (updates.status === 'resolved') {
tickets[idx].resolvedAt = new Date().toISOString();
}
saveTickets(tickets);
return tickets[idx];
}
function getOpenTickets() {
return loadTickets().filter(t => t.status === 'open');
}
/**
* 按服务+诊断查找处于 open 状态的工单(去重用)
* 严格匹配:必须同时提供 relatedService 与 diagnosis避免跨问题误合并
* @param {Object} key - { relatedService, diagnosis }
* @returns {Object|null}
*/
function findOpenTicketByKey(key = {}) {
const { relatedService, diagnosis } = key;
if (!relatedService || !diagnosis) return null;
const tickets = loadTickets();
return tickets.find(t =>
t.status === 'open' &&
t.relatedService === relatedService &&
t.diagnosis === diagnosis
) || null;
}
function getAllTickets(limit = 100) {
return loadTickets().slice(-limit);
}
// ── 统计数据 ──────────────────────────────
function loadStats() {
try {
if (!fs.existsSync(STATS_FILE)) return getDefaultStats();
return JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'));
} catch {
return getDefaultStats();
}
}
function saveStats(stats) {
ensureDir();
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2));
}
function getDefaultStats() {
return {
totalChecks: 0,
totalRepairs: 0,
totalTickets: 0,
totalChats: 0,
lastQuickCheck: null,
lastDeepCheck: null,
lastDailyReport: null,
uptimeStart: new Date().toISOString(),
issuesByCategory: {},
repairSuccessRate: { success: 0, fail: 0 }
};
}
function incrementStat(key, amount = 1) {
const stats = loadStats();
if (typeof stats[key] === 'number') {
stats[key] += amount;
} else {
stats[key] = amount;
}
saveStats(stats);
return stats;
}
function updateStatField(key, value) {
const stats = loadStats();
stats[key] = value;
saveStats(stats);
return stats;
}
// ── 上下文构建(给 LLM 提供历史记忆) ────────
function buildMemoryContext() {
const stats = loadStats();
const recentEvents = queryEvents({ limit: 10 });
const openTickets = getOpenTickets();
let context = '## 运维守卫记忆\n';
context += `- 启动时间: ${stats.uptimeStart}\n`;
context += `- 总巡检次数: ${stats.totalChecks}\n`;
context += `- 总修复次数: ${stats.totalRepairs}\n`;
context += `- 开放工单数: ${openTickets.length}\n`;
if (recentEvents.length > 0) {
context += '\n## 最近事件\n';
for (const evt of recentEvents.slice(0, 5)) {
context += `- [${evt.timestamp}] ${evt.type}: ${evt.summary || evt.title || JSON.stringify(evt).slice(0, 100)}\n`;
}
}
if (openTickets.length > 0) {
context += '\n## 开放工单\n';
for (const t of openTickets) {
context += `- ${t.id}: ${t.title} (${t.severity}) — ${t.direction}\n`;
}
}
return context;
}
module.exports = {
logEvent,
queryEvents,
createTicket,
updateTicket,
getOpenTickets,
findOpenTicketByKey,
getAllTickets,
loadStats,
incrementStat,
updateStatField,
buildMemoryContext,
MEMORY_DIR
};