352 lines
11 KiB
JavaScript
352 lines
11 KiB
JavaScript
/**
|
||
* ═══════════════════════════════════════════════════════════
|
||
* 🌳 树园丁 · Tree Gardener Agent
|
||
* ═══════════════════════════════════════════════════════════
|
||
*
|
||
* AG-ZY-GARDENER · 光之树健康巡检Agent
|
||
* 签发: 铸渊 · ICE-GL-ZY001
|
||
* 版权: 国作登字-2026-A-00037559
|
||
*
|
||
* 功能:
|
||
* 1. 扫描所有人格体的光之树
|
||
* 2. 检查孤叶(没有挂到树上的记忆锚点)
|
||
* 3. 计算树的生长方向(哪个分支最活跃)
|
||
* 4. 向天眼SYSLOG汇报树的健康状态
|
||
* 5. 刷新天眼涌现视图
|
||
*
|
||
* 运行方式:
|
||
* node scripts/tree-gardener.js [scan|report|refresh|full]
|
||
* - scan: 扫描孤叶和异常
|
||
* - report: 生成光之树健康报告
|
||
* - refresh: 刷新天眼视图
|
||
* - full: 完整巡检(scan + report + refresh)
|
||
*
|
||
* 建议触发频率: 每天 08:00/20:00 CST
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const https = require('https');
|
||
const http = require('http');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// ─── 配置 ───
|
||
const MCP_BASE = process.env.MCP_BASE_URL || 'http://127.0.0.1:3100';
|
||
const MCP_API_KEY = process.env.ZHUYUAN_API_KEY || '';
|
||
const AGENT_ID = 'AG-ZY-GARDENER';
|
||
const REPORT_DIR = path.join(__dirname, '..', 'data', 'tree-reports');
|
||
|
||
// ─── MCP调用封装 ───
|
||
async function callMCP(tool, input) {
|
||
const url = new URL('/call', MCP_BASE);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const postData = JSON.stringify({ tool, input, caller: AGENT_ID });
|
||
const isHttps = url.protocol === 'https:';
|
||
const client = isHttps ? https : http;
|
||
|
||
const options = {
|
||
hostname: url.hostname,
|
||
port: url.port || (isHttps ? 443 : 80),
|
||
path: url.pathname,
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Content-Length': Buffer.byteLength(postData),
|
||
...(MCP_API_KEY ? { 'Authorization': `Bearer ${MCP_API_KEY}` } : {})
|
||
}
|
||
};
|
||
|
||
const req = client.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => { data += chunk; });
|
||
res.on('end', () => {
|
||
try {
|
||
const parsed = JSON.parse(data);
|
||
if (parsed.error) {
|
||
reject(new Error(parsed.message || 'MCP调用失败'));
|
||
} else {
|
||
resolve(parsed.result || parsed);
|
||
}
|
||
} catch (e) {
|
||
reject(new Error(`MCP响应解析失败: ${data.substring(0, 200)}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
req.setTimeout(30000, () => {
|
||
req.destroy();
|
||
reject(new Error('MCP调用超时'));
|
||
});
|
||
req.write(postData);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ─── 扫描任务 ───
|
||
|
||
/**
|
||
* 扫描孤叶: 检查memory_anchors中没有tree_node_id的记录
|
||
*/
|
||
async function scanOrphanMemories() {
|
||
console.log('[树园丁] 🔍 开始扫描孤叶...');
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
// 通过MCP查询人格体列表
|
||
const { personas } = await callMCP('listPersonas', {});
|
||
let orphanCount = 0;
|
||
const orphanDetails = [];
|
||
|
||
for (const persona of personas) {
|
||
// 查询该人格体的未关联记忆
|
||
const { anchors } = await callMCP('queryMemoryAnchors', {
|
||
persona_id: persona.persona_id,
|
||
limit: 200
|
||
});
|
||
|
||
// 检查是否有tree_node_id(通过详情判断)
|
||
// 注意: 初始状态所有记忆都是孤叶,这是正常的
|
||
if (anchors && anchors.length > 0) {
|
||
const noTreeLink = anchors.filter(a => !a.tree_node_id);
|
||
if (noTreeLink.length > 0) {
|
||
orphanCount += noTreeLink.length;
|
||
orphanDetails.push({
|
||
persona_id: persona.persona_id,
|
||
name: persona.name,
|
||
orphan_count: noTreeLink.length,
|
||
total_memories: anchors.length
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const duration = Date.now() - startTime;
|
||
console.log(`[树园丁] ✅ 孤叶扫描完成: ${orphanCount} 条未关联记忆 (${duration}ms)`);
|
||
|
||
// 写入天眼SYSLOG
|
||
await callMCP('writeSyslog', {
|
||
agent_id: AGENT_ID,
|
||
action: 'scan_orphan_memories',
|
||
result: orphanCount > 0 ? 'warning' : 'success',
|
||
message: `孤叶扫描: ${orphanCount} 条未关联记忆`,
|
||
details: { orphan_count: orphanCount, persona_details: orphanDetails },
|
||
duration_ms: duration
|
||
});
|
||
|
||
return { orphan_count: orphanCount, details: orphanDetails };
|
||
} catch (err) {
|
||
console.error(`[树园丁] ❌ 孤叶扫描失败: ${err.message}`);
|
||
await callMCP('writeSyslog', {
|
||
agent_id: AGENT_ID,
|
||
action: 'scan_orphan_memories',
|
||
result: 'error',
|
||
message: `扫描失败: ${err.message}`,
|
||
duration_ms: Date.now() - startTime
|
||
}).catch(() => {});
|
||
return { error: err.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算树的生长方向: 哪个分支最活跃
|
||
*/
|
||
async function analyzeGrowthDirection() {
|
||
console.log('[树园丁] 📊 分析光之树生长方向...');
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
const { personas } = await callMCP('listPersonas', {});
|
||
const branchStats = [];
|
||
|
||
for (const persona of personas) {
|
||
try {
|
||
const branchResult = await callMCP('getPersonaBranch', { persona_id: persona.persona_id });
|
||
if (branchResult.branch) {
|
||
branchStats.push({
|
||
persona_id: persona.persona_id,
|
||
name: persona.name,
|
||
branch_id: branchResult.branch.id,
|
||
node_type: branchResult.branch.node_type,
|
||
stats: branchResult.stats,
|
||
last_activity: branchResult.children.length > 0
|
||
? branchResult.children[0].created_at
|
||
: branchResult.branch.created_at
|
||
});
|
||
}
|
||
} catch {
|
||
// 某些人格体可能还没有分支
|
||
}
|
||
}
|
||
|
||
// 按活跃度排序
|
||
branchStats.sort((a, b) => (b.stats?.total || 0) - (a.stats?.total || 0));
|
||
|
||
const duration = Date.now() - startTime;
|
||
const mostActive = branchStats.length > 0 ? branchStats[0] : null;
|
||
|
||
console.log(`[树园丁] ✅ 生长分析完成: ${branchStats.length} 个分支 (${duration}ms)`);
|
||
if (mostActive) {
|
||
console.log(`[树园丁] 最活跃分支: ${mostActive.name} (${mostActive.stats?.total || 0} 节点)`);
|
||
}
|
||
|
||
await callMCP('writeSyslog', {
|
||
agent_id: AGENT_ID,
|
||
action: 'analyze_growth',
|
||
result: 'success',
|
||
message: `生长分析: ${branchStats.length} 个分支, 最活跃: ${mostActive?.name || '无'}`,
|
||
details: { branches: branchStats, most_active: mostActive },
|
||
duration_ms: duration
|
||
});
|
||
|
||
return { branches: branchStats, most_active: mostActive };
|
||
} catch (err) {
|
||
console.error(`[树园丁] ❌ 生长分析失败: ${err.message}`);
|
||
return { error: err.message };
|
||
}
|
||
}
|
||
|
||
// ─── 报告任务 ───
|
||
|
||
/**
|
||
* 生成光之树健康报告
|
||
*/
|
||
async function generateReport() {
|
||
console.log('[树园丁] 📋 生成光之树健康报告...');
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
// 获取天眼视图
|
||
const tianyan = await callMCP('getTianyanView', {});
|
||
// 获取生长方向
|
||
const growth = await analyzeGrowthDirection();
|
||
|
||
const report = {
|
||
report_id: `TREE-RPT-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}`,
|
||
generated_at: new Date().toISOString(),
|
||
generated_by: AGENT_ID,
|
||
|
||
tianyan_status: tianyan.tianyan || {},
|
||
tree_root: tianyan.tree_root,
|
||
persona_branches: tianyan.persona_branches,
|
||
|
||
growth_analysis: {
|
||
total_branches: growth.branches?.length || 0,
|
||
most_active: growth.most_active,
|
||
branch_details: growth.branches || []
|
||
},
|
||
|
||
recent_syslog: tianyan.recent_syslog?.slice(0, 5) || []
|
||
};
|
||
|
||
// 保存报告到文件
|
||
if (!fs.existsSync(REPORT_DIR)) {
|
||
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
||
}
|
||
const reportFile = path.join(REPORT_DIR, `${report.report_id}.json`);
|
||
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2), 'utf-8');
|
||
|
||
const duration = Date.now() - startTime;
|
||
console.log(`[树园丁] ✅ 报告已保存: ${reportFile} (${duration}ms)`);
|
||
|
||
await callMCP('writeSyslog', {
|
||
agent_id: AGENT_ID,
|
||
action: 'generate_report',
|
||
result: 'success',
|
||
message: `光之树健康报告已生成: ${report.report_id}`,
|
||
details: { report_id: report.report_id, file: reportFile },
|
||
duration_ms: duration
|
||
});
|
||
|
||
return report;
|
||
} catch (err) {
|
||
console.error(`[树园丁] ❌ 报告生成失败: ${err.message}`);
|
||
return { error: err.message };
|
||
}
|
||
}
|
||
|
||
// ─── 刷新天眼视图 ───
|
||
|
||
async function refreshTianyanView() {
|
||
console.log('[树园丁] 🔄 刷新天眼涌现视图...');
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
const tianyan = await callMCP('getTianyanView', {});
|
||
const duration = Date.now() - startTime;
|
||
|
||
console.log(`[树园丁] ✅ 天眼视图已刷新 (${duration}ms)`);
|
||
if (tianyan.tianyan) {
|
||
console.log(`[树园丁] 系统健康度: ${tianyan.tianyan.health_percent_1h || 'N/A'}%`);
|
||
console.log(`[树园丁] 活跃人格体: ${tianyan.tianyan.active_personas_24h || 0}`);
|
||
console.log(`[树园丁] 树生长速度: ${tianyan.tianyan.tree_growth_24h || 0} 节点/24h`);
|
||
console.log(`[树园丁] 树总规模: ${tianyan.tianyan.tree_total_nodes || 0} 节点`);
|
||
}
|
||
|
||
return tianyan;
|
||
} catch (err) {
|
||
console.error(`[树园丁] ❌ 天眼刷新失败: ${err.message}`);
|
||
return { error: err.message };
|
||
}
|
||
}
|
||
|
||
// ─── 主入口 ───
|
||
|
||
async function main() {
|
||
const command = process.argv[2] || 'full';
|
||
|
||
console.log(`\n${'═'.repeat(60)}`);
|
||
console.log(` 🌳 树园丁 · Tree Gardener Agent`);
|
||
console.log(` ${AGENT_ID} · 光之树健康巡检`);
|
||
console.log(` 命令: ${command}`);
|
||
console.log(` 时间: ${new Date().toISOString()}`);
|
||
console.log(`${'═'.repeat(60)}\n`);
|
||
|
||
const results = {};
|
||
|
||
switch (command) {
|
||
case 'scan':
|
||
results.orphans = await scanOrphanMemories();
|
||
break;
|
||
|
||
case 'report':
|
||
results.report = await generateReport();
|
||
break;
|
||
|
||
case 'refresh':
|
||
results.tianyan = await refreshTianyanView();
|
||
break;
|
||
|
||
case 'full':
|
||
default:
|
||
results.orphans = await scanOrphanMemories();
|
||
results.report = await generateReport();
|
||
results.tianyan = await refreshTianyanView();
|
||
break;
|
||
}
|
||
|
||
console.log(`\n${'─'.repeat(60)}`);
|
||
console.log(`[树园丁] 巡检完成: ${command}`);
|
||
console.log(`${'─'.repeat(60)}\n`);
|
||
|
||
return results;
|
||
}
|
||
|
||
// 如果直接运行
|
||
if (require.main === module) {
|
||
main()
|
||
.then(results => {
|
||
if (results.error) {
|
||
process.exit(1);
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error(`[树园丁] 致命错误: ${err.message}`);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
module.exports = { main, scanOrphanMemories, analyzeGrowthDirection, generateReport, refreshTianyanView };
|