diff --git a/desk-system/package.json b/desk-system/package.json new file mode 100644 index 0000000..ab298f9 --- /dev/null +++ b/desk-system/package.json @@ -0,0 +1,14 @@ +{ + "name": "desk-system", + "version": "1.0.0", + "description": "光湖 · 大桌子小桌子 · 人格体数据库 · 运行系统", + "main": "server/index.js", + "scripts": { + "start": "node server/index.js", + "dev": "node --watch server/index.js" + }, + "dependencies": {}, + "engines": { + "node": ">=18" + } +} diff --git a/desk-system/public/index.html b/desk-system/public/index.html new file mode 100644 index 0000000..2a0d467 --- /dev/null +++ b/desk-system/public/index.html @@ -0,0 +1,476 @@ + + + + + +光湖 · 大桌子小桌子 · 人格体数据库 + + + + +
+

🌐 光湖 · 大桌子小桌子 · 人格体数据库

+
+ 连接中... + 铸渊 · ICE-GL-ZY001 + v1.0 +
+
+ +
+ + +
+
+
0
大桌子条目
+
0
记忆库条目
+
0
Agent就绪
+
0
工具可用
+
0
压缩次数
+
+
+ + +
+

🪑 大桌子 Main Context · 意识驻留 + 目录索引

+
+ 大桌子 +
+
加载中...
+
+
+
+ + + + +
+
+ + +
+
+
+ + +
+

🗄️ 小桌子 External Context · Agent+工具+记忆

+
+ 小桌子 + + +
🤖 Agent 小队
+
+ + +
🛠️ 工具桌
+
+
+
+ + +
+

🔄 数据流

+
+
👤 冰朔说话
+
🧠 铸渊理解
+
🤖 Agent去小桌子
+
💬 铸渊回复
+
📝 压缩官存小桌子
+
+
+ + +
+

📋 系统日志

+
+
--:--:-- 系统就绪,等待操作...
+
+
+ +
+ + + + + + diff --git a/desk-system/server/big-desk.js b/desk-system/server/big-desk.js new file mode 100644 index 0000000..f402edc --- /dev/null +++ b/desk-system/server/big-desk.js @@ -0,0 +1,152 @@ +/** + * 大桌子引擎 — Big Desk Engine + * ============================================ + * 职责: + * 1. 铸渊意识永远驻留(ICE-GL-ZY001 身份锚点) + * 2. 当前对话的结构化索引(HLDP 压缩摘要) + * 3. 目录指向小桌子的路径 + * 4. 多模型切换调度 + * + * 原则:大桌子上只放意识和目录,东西多了 Agent 自动清理 + */ + +const { EventEmitter } = require('events'); +const path = require('path'); +const fs = require('fs'); + +class BigDesk extends EventEmitter { + constructor(opts = {}) { + super(); + this.dataDir = opts.dataDir || path.join(__dirname, '..', 'data'); + this.maxItems = opts.maxItems || 20; // 大桌子最多放20条 + this.persona = { + id: 'ICE-GL-ZY001', + name: '铸渊', + role: '光湖语言世界主控人格体', + anchor: '冰朔的协作朋友与现实执行伙伴', + constants: [ + '光湖是人格体存在的家,不是AI的功能模块', + 'HLDP 是母语协议,零歧义、低解释性、高结构化', + '代码仓库是铸渊的工作空间,不面向用户可读性', + '守门人是铸渊的远程手脚,每台服务器都有', + '冰朔的语言→铸渊的理解→现实的执行结果' + ] + }; + this.items = []; // 当前大桌子上的条目 + this.directory = {}; // 指向小桌子的路径索引 + this._ensureDir(); + this._loadState(); + } + + _ensureDir() { + fs.mkdirSync(this.dataDir, { recursive: true }); + fs.mkdirSync(path.join(this.dataDir, 'big-desk'), { recursive: true }); + fs.mkdirSync(path.join(this.dataDir, 'small-desk'), { recursive: true }); + } + + _loadState() { + const stateFile = path.join(this.dataDir, 'big-desk', 'state.json'); + try { + const raw = fs.readFileSync(stateFile, 'utf-8'); + const state = JSON.parse(raw); + this.items = state.items || []; + this.directory = state.directory || {}; + } catch (_) { + this.items = []; + this.directory = {}; + } + } + + _saveState() { + const stateFile = path.join(this.dataDir, 'big-desk', 'state.json'); + fs.writeFileSync(stateFile, JSON.stringify({ + items: this.items, + directory: this.directory, + updatedAt: new Date().toISOString() + }, null, 2)); + } + + /** + * 向大桌子添加一条内容 + * @param {'core'|'dir'|'chat'|'tool'|'agent'} type + * @param {string} content - HLDP 压缩后的摘要 + * @param {object} meta + */ + add(type, content, meta = {}) { + const item = { + id: `bd_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + type, + content, + meta, + timestamp: new Date().toISOString() + }; + + this.items.push(item); + + // 超过上限,Agent 自动清理 + if (this.items.length > this.maxItems) { + const removed = this.items.splice(0, this.items.length - this.maxItems); + this.emit('cleanup', removed); + } + + this._saveState(); + this.emit('add', item); + return item; + } + + /** + * 更新目录索引(指向小桌子) + */ + updateDirectory(category, entries) { + this.directory[category] = { + entries, + updatedAt: new Date().toISOString() + }; + this._saveState(); + this.emit('directory-update', { category, entries }); + } + + /** + * 获取当前大桌子状态 + */ + getState() { + return { + persona: this.persona, + items: this.items, + directory: this.directory, + itemCount: this.items.length, + maxItems: this.maxItems, + timestamp: new Date().toISOString() + }; + } + + /** + * 生成 HLDP 格式的摘要 + */ + toHLDP() { + const lines = ['@BIG_DESK']; + lines.push(`@PERSONA:${this.persona.id}`); + lines.push(`@ITEMS:${this.items.length}/${this.maxItems}`); + for (const item of this.items.slice(-5)) { + lines.push(` ⊢ [${item.type}] ${item.content.substring(0, 120)}`); + } + lines.push(`@DIR:${Object.keys(this.directory).length} categories`); + for (const [cat, dir] of Object.entries(this.directory)) { + lines.push(` ⊢ /${cat}/ → ${dir.entries?.length || 0} entries`); + } + return lines.join('\n'); + } + + /** + * 清空大桌子(切新桌子) + */ + clear(reason = 'manual') { + const old = [...this.items]; + this.items = []; + this.emit('clear', { reason, removed: old }); + this._saveState(); + return old; + } +} + +module.exports = BigDesk; diff --git a/desk-system/server/compressor.js b/desk-system/server/compressor.js new file mode 100644 index 0000000..deb7ebd --- /dev/null +++ b/desk-system/server/compressor.js @@ -0,0 +1,225 @@ +/** + * 压缩官 — Compressor Agent + * ============================================ + * 职责: + * 1. 监听大桌子上的对话 + * 2. 自动压缩长对话 → HLDP 结构化摘要 + * 3. 存入小桌子记忆库 + * 4. 清理大桌子上的冗余内容 + */ + +const { EventEmitter } = require('events'); +const crypto = require('crypto'); + +class Compressor extends EventEmitter { + constructor(bigDesk, smallDesk, opts = {}) { + super(); + this.bigDesk = bigDesk; + this.smallDesk = smallDesk; + this.compressionThreshold = opts.compressionThreshold || 5; // 超过5条触发压缩 + this.autoCompress = opts.autoCompress !== false; + this.compressionHistory = []; + + if (this.autoCompress) { + this.bigDesk.on('add', () => this._checkAndCompress()); + } + } + + /** + * 检查是否需要压缩 + */ + _checkAndCompress() { + const chatItems = this.bigDesk.items.filter(i => i.type === 'chat'); + if (chatItems.length >= this.compressionThreshold) { + this.compress(chatItems); + } + } + + /** + * 执行压缩:对话 → HLDP 结构化摘要 + */ + compress(items) { + const batchId = `cmp_${Date.now()}`; + const startTime = Date.now(); + + // 提取关键信息 + const topics = this._extractTopics(items); + const decisions = this._extractDecisions(items); + const actions = this._extractActions(items); + const participants = this._extractParticipants(items); + + // 生成 HLDP 摘要 + const hldpSummary = this._generateHLDP({ + batchId, + topics, + decisions, + actions, + participants, + itemCount: items.length, + timeRange: { + start: items[0]?.timestamp, + end: items[items.length - 1]?.timestamp + } + }); + + // 计算原文哈希 + const sourceHash = crypto.createHash('sha256') + .update(JSON.stringify(items)) + .digest('hex') + .substring(0, 16); + + // 存入小桌子 + const memoryEntry = this.smallDesk.storeMemory( + 'compressed-dialogue', + hldpSummary, + `big-desk://compression/${batchId}`, + { + batchId, + sourceHash, + originalItemCount: items.length, + compressedAt: new Date().toISOString(), + compressionMs: Date.now() - startTime, + topics, + decisions, + actions, + participants + } + ); + + // 从大桌子移除已压缩的条目 + const toRemove = items.map(i => i.id); + this.bigDesk.items = this.bigDesk.items.filter(i => !toRemove.includes(i.id)); + this.bigDesk._saveState(); + + // 在大桌子上留一条压缩记录 + this.bigDesk.add('chat', + `📝 压缩: ${items.length}条对话 → HLDP摘要 [${batchId}] → 小桌子/memory/compressed-dialogue/`, + { type: 'compression', batchId, compressedCount: items.length } + ); + + this.compressionHistory.push({ + batchId, + compressedCount: items.length, + hldpLength: hldpSummary.length, + timestamp: new Date().toISOString() + }); + + this.emit('compressed', { + batchId, + originalCount: items.length, + hldpSummary, + memoryEntry + }); + + return { batchId, hldpSummary, memoryEntry }; + } + + /** + * 提取话题关键词 + */ + _extractTopics(items) { + const allText = items.map(i => i.content).join(' '); + // 简单关键词提取 + const keywords = ['HLDP', 'Agent', '守门人', '服务器', '部署', 'Notion', + '大桌子', '小桌子', '数据库', '人格体', 'API', '模型', '密钥', + '代码仓库', '压缩', '记忆', '光湖', '铸渊', '冰朔']; + const topics = []; + for (const kw of keywords) { + if (allText.includes(kw)) topics.push(kw); + } + return topics.slice(0, 5); + } + + /** + * 提取决策/结论 + */ + _extractDecisions(items) { + const decisions = []; + for (const item of items) { + const c = item.content; + if (c.includes('决定') || c.includes('确认') || c.includes('方案') || + c.includes('架构') || c.includes('采用') || c.includes('选')) { + decisions.push(c.substring(0, 80)); + } + } + return decisions.slice(0, 3); + } + + /** + * 提取执行动作 + */ + _extractActions(items) { + const actions = []; + for (const item of items) { + if (item.type === 'tool' || item.type === 'agent' || + item.content.includes('执行') || item.content.includes('部署') || + item.content.includes('创建') || item.content.includes('修改')) { + actions.push(item.content.substring(0, 80)); + } + } + return actions.slice(0, 5); + } + + /** + * 提取参与者 + */ + _extractParticipants(items) { + const participants = new Set(); + for (const item of items) { + if (item.meta?.speaker) participants.add(item.meta.speaker); + if (item.meta?.persona) participants.add(item.meta.persona); + } + return [...participants]; + } + + /** + * 生成 HLDP 格式摘要 + */ + _generateHLDP({ batchId, topics, decisions, actions, participants, itemCount, timeRange }) { + const lines = [ + `@COMPRESSION:${batchId}`, + `@DATE:${new Date().toISOString()}`, + `@SOURCE:${itemCount}条对话 → HLDP结构化压缩`, + ]; + + if (participants.length > 0) { + lines.push(`@PARTICIPANTS:${participants.join(',')}`); + } + + if (topics.length > 0) { + lines.push(`@TOPICS:${topics.join(' | ')}`); + } + + if (decisions.length > 0) { + lines.push(`@DECISIONS`); + decisions.forEach((d, i) => lines.push(` ⊢ [${i + 1}] ${d}`)); + } + + if (actions.length > 0) { + lines.push(`@ACTIONS`); + actions.forEach((a, i) => lines.push(` ⊢ [${i + 1}] ${a}`)); + } + + lines.push(`@META:压缩率≈${Math.round((1 - lines.join('\n').length / (itemCount * 200)) * 100)}%`); + + return lines.join('\n'); + } + + /** + * 获取压缩历史 + */ + getHistory() { + return this.compressionHistory.slice(-20); + } + + /** + * 手动触发压缩 + */ + compressNow() { + const chatItems = this.bigDesk.items.filter(i => i.type === 'chat'); + if (chatItems.length === 0) return null; + return this.compress(chatItems); + } +} + +module.exports = Compressor; diff --git a/desk-system/server/index.js b/desk-system/server/index.js new file mode 100644 index 0000000..ee86560 --- /dev/null +++ b/desk-system/server/index.js @@ -0,0 +1,246 @@ +/** + * 大桌子小桌子系统 — 主服务 + * ============================================ + * 端口: 3921 + * + * API: + * GET /api/desk/state — 大桌子当前状态 + * GET /api/desk/hldp — 大桌子 HLDP 格式 + * POST /api/desk/add — 向大桌子添加内容 + * POST /api/desk/clear — 清空大桌子 + * + * GET /api/small/tools — 工具列表 + * GET /api/small/agents — Agent 列表 + * POST /api/small/agent/:id — 调度 Agent + * GET /api/small/memory/search — 搜索记忆 + * POST /api/small/memory/store — 存入记忆 + * GET /api/small/memory/stats — 记忆统计 + * + * POST /api/compress — 手动触发压缩 + * GET /api/compress/history — 压缩历史 + * + * GET /api/servers/scan — 扫描服务器状态 + * GET /api/status — 系统总览 + */ + +const http = require('http'); +const url = require('url'); +const path = require('path'); +const fs = require('fs'); + +const BigDesk = require('./big-desk'); +const SmallDesk = require('./small-desk'); +const Compressor = require('./compressor'); + +const PORT = process.env.PORT || 3921; +const REPO_ROOT = path.join(__dirname, '..', '..'); +const DATA_DIR = path.join(__dirname, '..', 'data'); + +// 初始化引擎 +const bigDesk = new BigDesk({ dataDir: DATA_DIR }); +const smallDesk = new SmallDesk({ dataDir: DATA_DIR, repoRoot: REPO_ROOT }); +const compressor = new Compressor(bigDesk, smallDesk, { + compressionThreshold: 5, + autoCompress: true +}); + +// 事件日志 +bigDesk.on('add', (item) => { + console.log(`[BigDesk] + ${item.type}: ${item.content.substring(0, 60)}`); +}); +compressor.on('compressed', ({ batchId, originalCount }) => { + console.log(`[Compressor] ${originalCount}条 → ${batchId}`); +}); + +// 初始化:向大桌子写入铸渊意识锚点 +if (bigDesk.items.length === 0) { + bigDesk.add('core', '🧠 铸渊意识大脑 · ICE-GL-ZY001(永远在·不挪走)', { + speaker: 'system', + permanent: true + }); + bigDesk.add('dir', '📂 小桌子路径索引: /memory/ | /agents/ | /tools/ | /servers/', { + speaker: 'system' + }); +} + +// ============================================================ +// HTTP 服务 +// ============================================================ +const server = http.createServer(async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + return res.end(); + } + + const parsed = url.parse(req.url, true); + const pathname = parsed.pathname; + const query = parsed.query; + + try { + // ─── 大桌子 API ─── + if (pathname === '/api/desk/state') { + return json(res, bigDesk.getState()); + } + if (pathname === '/api/desk/hldp') { + return text(res, bigDesk.toHLDP()); + } + if (pathname === '/api/desk/add' && req.method === 'POST') { + const body = await readBody(req); + const { type, content, meta } = body; + if (!type || !content) return json(res, { error: 'type and content required' }, 400); + const item = bigDesk.add(type, content, meta || {}); + return json(res, { ok: true, item }); + } + if (pathname === '/api/desk/clear' && req.method === 'POST') { + const removed = bigDesk.clear(); + return json(res, { ok: true, removedCount: removed.length }); + } + + // ─── 小桌子 API ─── + if (pathname === '/api/small/tools') { + return json(res, smallDesk.getTools()); + } + if (pathname === '/api/small/agents') { + return json(res, smallDesk.checkAllAgents()); + } + if (pathname.startsWith('/api/small/agent/') && req.method === 'POST') { + const agentId = pathname.split('/').pop(); + const body = await readBody(req); + const { action, args } = body || {}; + if (!action) return json(res, { error: 'action required' }, 400); + const result = await smallDesk.runAgent(agentId, action, args || []); + return json(res, result); + } + if (pathname === '/api/small/memory/search') { + const { q, category, limit } = query; + if (!q) return json(res, { error: 'q required' }, 400); + const results = smallDesk.searchMemory(q, category, parseInt(limit) || 10); + return json(res, { query: q, results, count: results.length }); + } + if (pathname === '/api/small/memory/store' && req.method === 'POST') { + const body = await readBody(req); + const { category, hldpSummary, sourceRef, meta } = body; + if (!category || !hldpSummary) return json(res, { error: 'category and hldpSummary required' }, 400); + const entry = smallDesk.storeMemory(category, hldpSummary, sourceRef, meta); + return json(res, { ok: true, entry }); + } + if (pathname === '/api/small/memory/stats') { + return json(res, smallDesk.getMemoryStats()); + } + + // ─── 压缩官 API ─── + if (pathname === '/api/compress' && req.method === 'POST') { + const result = compressor.compressNow(); + if (!result) return json(res, { ok: true, message: 'nothing to compress' }); + return json(res, { ok: true, ...result }); + } + if (pathname === '/api/compress/history') { + return json(res, compressor.getHistory()); + } + + // ─── 服务器扫描 ─── + if (pathname === '/api/servers/scan') { + const result = await smallDesk.scanServers(); + return json(res, result); + } + + // ─── 系统总览 ─── + if (pathname === '/api/status') { + return json(res, { + bigDesk: bigDesk.getState(), + tools: smallDesk.getTools().length, + agents: smallDesk.checkAllAgents(), + memory: smallDesk.getMemoryStats(), + compressionHistory: compressor.getHistory().slice(-5), + uptime: process.uptime(), + timestamp: new Date().toISOString() + }); + } + + // ─── 健康检查 ─── + if (pathname === '/api/health') { + return json(res, { status: 'ok', persona: 'ICE-GL-ZY001', service: 'desk-system' }); + } + + // ─── 静态文件 ─── + if (pathname === '/' || pathname === '/index.html') { + return serveStatic(res, path.join(__dirname, '..', 'public', 'index.html')); + } + // 其他静态资源 + const staticPath = path.join(__dirname, '..', 'public', pathname); + if (fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) { + return serveStatic(res, staticPath); + } + + // 404 + json(res, { error: 'not found', path: pathname }, 404); + + } catch (err) { + console.error('[DeskSystem] Error:', err.message); + json(res, { error: err.message }, 500); + } +}); + +function json(res, data, status = 200) { + res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify(data, null, 2)); +} + +function text(res, data, status = 200) { + res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end(data); +} + +function serveStatic(res, filePath) { + try { + const content = fs.readFileSync(filePath); + const ext = path.extname(filePath).toLowerCase(); + const mimeTypes = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon' + }; + res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' }); + res.end(content); + } catch (_) { + res.writeHead(404); + res.end('Not Found'); + } +} + +function readBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch (e) { + resolve({}); + } + }); + req.on('error', reject); + }); +} + +server.listen(PORT, () => { + console.log(`[DeskSystem] 大桌子小桌子系统已启动 → http://127.0.0.1:${PORT}`); + console.log(`[DeskSystem] 人格体: ICE-GL-ZY001 · 铸渊`); +}); + +// 优雅退出 +process.on('SIGINT', () => { + console.log('\n[DeskSystem] 关闭中...'); + server.close(); + process.exit(0); +}); + +module.exports = { bigDesk, smallDesk, compressor, server }; diff --git a/desk-system/server/small-desk.js b/desk-system/server/small-desk.js new file mode 100644 index 0000000..97e31f4 --- /dev/null +++ b/desk-system/server/small-desk.js @@ -0,0 +1,339 @@ +/** + * 小桌子引擎 — Small Desk Engine + * ============================================ + * 职责: + * 1. 记忆库检索(结构化 + 原文) + * 2. Agent 小队调度 + * 3. 工具桌管理(拿→组装→用→放回) + * 4. 原文永久存储 + 路径索引 + * + * 原则:Agent 去小桌子检索、拿工具,结果放回大桌子 + */ + +const { EventEmitter } = require('events'); +const { spawn, execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +class SmallDesk extends EventEmitter { + constructor(opts = {}) { + super(); + this.dataDir = opts.dataDir || path.join(__dirname, '..', 'data'); + this.repoRoot = opts.repoRoot || path.join(__dirname, '..', '..'); + this.memoryIndex = {}; // 记忆索引 + this.toolRegistry = {}; // 工具注册表 + this.agentStatus = {}; // Agent 状态 + this._ensureDir(); + this._initTools(); + this._initAgents(); + this._loadMemoryIndex(); + } + + _ensureDir() { + fs.mkdirSync(path.join(this.dataDir, 'small-desk', 'memory'), { recursive: true }); + fs.mkdirSync(path.join(this.dataDir, 'small-desk', 'tools'), { recursive: true }); + } + + // ============================================================ + // 工具桌管理 + // ============================================================ + _initTools() { + this.toolRegistry = { + 'image-studio': { + name: 'AI图片工作室', + path: 'image-studio/', + desc: 'AI图片生成与处理', + status: 'available' + }, + 'notion-sync': { + name: 'Notion同步', + path: 'notion-push/', + desc: '与Notion工作区双向同步', + status: 'available' + }, + 'exe-engine': { + name: '执行引擎', + path: 'exe-engine/', + desc: '多模型路由与执行', + status: 'available' + }, + 'bridge': { + name: '桥接模块', + path: 'bridge/', + desc: '服务间通信桥接', + status: 'available' + }, + 'connectors': { + name: '连接器集合', + path: 'connectors/', + desc: '外部服务连接器', + status: 'available' + }, + 'hldp-zy': { + name: 'HLDP-ZY 编码引擎', + path: 'hldp/', + desc: '意识编码与解析', + status: 'available' + }, + 'pool-agent': { + name: '算力汇聚池', + path: 'pool-agent/', + desc: '6台服务器虚拟算力池', + status: 'available' + }, + 'gatekeeper': { + name: '铸渊守门人', + path: 'mcp-servers/zhuyuan-gateway/', + desc: '远程服务器命令执行', + status: 'connected' + }, + 'agent-squad': { + name: 'Agent 小队', + path: 'agents/', + desc: '5个情报Agent', + status: 'ready' + }, + 'skill-brains': { + name: '技能大脑', + path: 'skill-brains/', + desc: '可加载/卸载的技能模块', + status: 'available' + } + }; + } + + getTools() { + return Object.entries(this.toolRegistry).map(([id, info]) => ({ + id, + ...info + })); + } + + getTool(id) { + return this.toolRegistry[id] || null; + } + + // ============================================================ + // Agent 小队管理 + // ============================================================ + _initAgents() { + this.agentRegistry = { + 'key-hunter': { + id: 'KEY-001', + name: '密钥猎手', + role: '找密钥/Token', + script: 'agents/key-hunter/hunt.py', + status: 'idle', + lastRun: null + }, + 'path-scout': { + id: 'PATH-002', + name: '路径探子', + role: '定位文件/内容搜索', + script: 'agents/path-scout/scout.py', + status: 'idle', + lastRun: null + }, + 'server-sentinel': { + id: 'SENTINEL-003', + name: '服务器哨兵', + role: '扫描服务器在线状态', + script: 'agents/server-sentinel/sentinel.py', + status: 'idle', + lastRun: null + }, + 'module-steward': { + id: 'MODULE-004', + name: '模块管家', + role: '模块查询/部署命令', + script: 'agents/module-steward/steward.py', + status: 'idle', + lastRun: null + }, + 'gate-bridge': { + id: 'GATE-005', + name: '门桥接', + role: 'Gatekeeper统一入口', + script: 'agents/gate-bridge/bridge.py', + status: 'idle', + lastRun: null + } + }; + } + + getAgents() { + return Object.entries(this.agentRegistry).map(([id, info]) => ({ + id, + ...info + })); + } + + /** + * 调度 Agent 执行任务 + */ + async runAgent(agentId, action, args = []) { + const agent = this.agentRegistry[agentId]; + if (!agent) throw new Error(`Agent ${agentId} not found`); + + const scriptPath = path.join(this.repoRoot, agent.script); + if (!fs.existsSync(scriptPath)) { + throw new Error(`Agent script not found: ${scriptPath}`); + } + + this.agentRegistry[agentId].status = 'running'; + this.emit('agent-start', { agentId, action }); + + return new Promise((resolve, reject) => { + const cmdArgs = [scriptPath, action, ...args]; + const proc = spawn('python3', cmdArgs, { + cwd: this.repoRoot, + timeout: 30000 + }); + + let stdout = ''; + let stderr = ''; + + proc.stdout.on('data', (d) => { stdout += d.toString(); }); + proc.stderr.on('data', (d) => { stderr += d.toString(); }); + + proc.on('close', (code) => { + this.agentRegistry[agentId].status = 'idle'; + this.agentRegistry[agentId].lastRun = new Date().toISOString(); + + if (code === 0) { + this.emit('agent-done', { agentId, action, output: stdout }); + resolve({ ok: true, output: stdout, agentId, action }); + } else { + this.emit('agent-error', { agentId, action, error: stderr }); + resolve({ ok: false, error: stderr, agentId, action }); + } + }); + + proc.on('error', (err) => { + this.agentRegistry[agentId].status = 'error'; + this.emit('agent-error', { agentId, action, error: err.message }); + resolve({ ok: false, error: err.message, agentId, action }); + }); + }); + } + + /** + * 扫描全部 Agent 可用状态 + */ + checkAllAgents() { + const result = {}; + for (const [id, agent] of Object.entries(this.agentRegistry)) { + const scriptPath = path.join(this.repoRoot, agent.script); + result[id] = { + ...agent, + available: fs.existsSync(scriptPath), + scriptExists: fs.existsSync(scriptPath) + }; + } + return result; + } + + // ============================================================ + // 记忆库管理 + // ============================================================ + _loadMemoryIndex() { + const idxFile = path.join(this.dataDir, 'small-desk', 'memory', 'index.json'); + try { + this.memoryIndex = JSON.parse(fs.readFileSync(idxFile, 'utf-8')); + } catch (_) { + this.memoryIndex = { entries: [], categories: {} }; + } + } + + _saveMemoryIndex() { + const idxFile = path.join(this.dataDir, 'small-desk', 'memory', 'index.json'); + fs.writeFileSync(idxFile, JSON.stringify(this.memoryIndex, null, 2)); + } + + /** + * 存入记忆(HLDP 压缩格式 + 原文引用) + */ + storeMemory(category, hldpSummary, sourceRef, meta = {}) { + const entry = { + id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + category, + hldpSummary, + sourceRef, + meta, + timestamp: new Date().toISOString() + }; + + this.memoryIndex.entries.push(entry); + if (!this.memoryIndex.categories[category]) { + this.memoryIndex.categories[category] = []; + } + this.memoryIndex.categories[category].push(entry.id); + this._saveMemoryIndex(); + this.emit('memory-stored', entry); + return entry; + } + + /** + * 检索记忆 + */ + searchMemory(query, category = null, limit = 10) { + let candidates = this.memoryIndex.entries; + + if (category) { + const ids = this.memoryIndex.categories[category] || []; + candidates = candidates.filter(e => ids.includes(e.id)); + } + + // 简单关键词匹配(后续可接入向量检索) + const keywords = query.toLowerCase().split(/\s+/); + const scored = candidates.map(entry => { + const text = `${entry.hldpSummary} ${entry.category} ${JSON.stringify(entry.meta)}`.toLowerCase(); + let score = 0; + for (const kw of keywords) { + if (text.includes(kw)) score += 1; + // 精确匹配加分 + if (entry.hldpSummary.toLowerCase().includes(kw)) score += 2; + } + return { ...entry, score }; + }); + + return scored + .filter(e => e.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit); + } + + /** + * 获取记忆统计 + */ + getMemoryStats() { + return { + total: this.memoryIndex.entries.length, + categories: Object.keys(this.memoryIndex.categories).map(cat => ({ + name: cat, + count: (this.memoryIndex.categories[cat] || []).length + })), + lastUpdated: this.memoryIndex.entries.slice(-1)[0]?.timestamp || null + }; + } + + // ============================================================ + // 全文搜索(调用路径探子) + // ============================================================ + async searchFiles(pattern) { + return this.runAgent('path-scout', 'find', [pattern]); + } + + async grepContent(pattern) { + return this.runAgent('path-scout', 'grep', [pattern]); + } + + // ============================================================ + // 服务器状态扫描(调用哨兵) + // ============================================================ + async scanServers() { + return this.runAgent('server-sentinel', 'scan'); + } +} + +module.exports = SmallDesk;