feat: 大桌子小桌子运行系统 v1.0
- big-desk.js: 意识驻留 + HLDP索引 + 自动清理 - small-desk.js: 记忆库检索 + Agent调度 + 工具桌管理 - compressor.js: 对话→HLDP自动压缩 + 写入小桌子 - server/index.js: REST API (端口3921) + 静态文件服务 - public/index.html: 交互式前端界面 ICE-GL-ZY001 · D115
This commit is contained in:
parent
3f8270b22e
commit
49030d2cbc
14
desk-system/package.json
Normal file
14
desk-system/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
476
desk-system/public/index.html
Normal file
476
desk-system/public/index.html
Normal file
@ -0,0 +1,476 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>光湖 · 大桌子小桌子 · 人格体数据库</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117; --surface: #161b22; --border: #30363d;
|
||||
--text: #c9d1d9; --muted: #8b949e; --accent: #58a6ff;
|
||||
--gold: #d2991d; --green: #3fb950; --purple: #bc8cff; --red: #f85149;
|
||||
--big-desk: #1a2332; --small-desk: #1a1e2e;
|
||||
--radius: 12px; --gap: 16px;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
|
||||
background: var(--bg); color: var(--text); min-height: 100vh; font-size: 14px;
|
||||
}
|
||||
|
||||
/* Topbar */
|
||||
.topbar {
|
||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||
padding: 12px 24px; display: flex; align-items: center; justify-content: space-between;
|
||||
position: sticky; top: 0; z-index: 100;
|
||||
}
|
||||
.topbar h1 {
|
||||
font-size: 18px; font-weight: 600;
|
||||
background: linear-gradient(135deg, var(--accent), var(--purple));
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
}
|
||||
.topbar .right { display: flex; gap: 12px; align-items: center; }
|
||||
.badge {
|
||||
padding: 3px 10px; border-radius: 10px; font-size: 11px; font-weight: 600;
|
||||
}
|
||||
.badge-online { background: rgba(63,185,80,0.15); color: var(--green); }
|
||||
.badge-active { background: rgba(88,166,255,0.15); color: var(--accent); animation: pulse 2s infinite; }
|
||||
.badge-purple { background: rgba(188,140,255,0.15); color: var(--purple); }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||||
|
||||
/* Main Layout */
|
||||
.main { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); padding: 20px; max-width: 1500px; margin: 0 auto; }
|
||||
@media (max-width: 960px) { .main { grid-template-columns: 1fr; } }
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 20px;
|
||||
}
|
||||
.card.full { grid-column: 1 / -1; }
|
||||
.card h2 { font-size: 16px; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
|
||||
.card h2 .icon { font-size: 20px; }
|
||||
|
||||
/* Desk Areas */
|
||||
.desk-area {
|
||||
border-radius: var(--radius); padding: 16px; min-height: 160px; position: relative;
|
||||
}
|
||||
.desk-area.big {
|
||||
background: var(--big-desk); border: 2px solid var(--accent);
|
||||
}
|
||||
.desk-area.small {
|
||||
background: var(--small-desk); border: 2px solid var(--purple);
|
||||
}
|
||||
.desk-label {
|
||||
position: absolute; top: -10px; left: 12px;
|
||||
padding: 1px 10px; border-radius: 6px; font-size: 11px; font-weight: 700; color: #fff;
|
||||
}
|
||||
.desk-label.big { background: var(--accent); }
|
||||
.desk-label.small { background: var(--purple); }
|
||||
.desk-items { margin-top: 8px; }
|
||||
.desk-item {
|
||||
background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.15);
|
||||
border-radius: 6px; padding: 10px 12px; margin-bottom: 6px; font-size: 13px;
|
||||
display: flex; align-items: flex-start; gap: 8px; transition: all .2s;
|
||||
}
|
||||
.desk-item:hover { border-color: rgba(88,166,255,0.3); }
|
||||
.desk-item .tag {
|
||||
font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; white-space: nowrap;
|
||||
}
|
||||
.tag-core { background: rgba(188,140,255,0.2); color: var(--purple); }
|
||||
.tag-dir { background: rgba(210,153,29,0.2); color: var(--gold); }
|
||||
.tag-chat { background: rgba(63,185,80,0.2); color: var(--green); }
|
||||
.tag-tool { background: rgba(210,153,29,0.2); color: var(--gold); }
|
||||
.tag-agent{ background: rgba(88,166,255,0.2); color: var(--accent); }
|
||||
.desk-item .ts { font-size: 10px; color: var(--muted); margin-left: auto; white-space: nowrap; }
|
||||
.desk-item .content { flex: 1; word-break: break-word; line-height: 1.4; }
|
||||
|
||||
/* Agent Grid */
|
||||
.agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; margin-top: 12px; }
|
||||
.agent-card {
|
||||
background: rgba(88,166,255,0.06); border: 1px solid rgba(88,166,255,0.15);
|
||||
border-radius: 8px; padding: 12px; text-align: center; cursor: pointer; transition: all .2s;
|
||||
}
|
||||
.agent-card:hover { border-color: rgba(88,166,255,0.35); background: rgba(88,166,255,0.1); transform: translateY(-2px); }
|
||||
.agent-card .icon { font-size: 24px; margin-bottom: 4px; }
|
||||
.agent-card .name { font-weight: 600; color: var(--accent); font-size: 13px; }
|
||||
.agent-card .role { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
||||
.agent-card .status { font-size: 10px; margin-top: 4px; }
|
||||
.agent-card .status.available { color: var(--green); }
|
||||
.agent-card .status.unavailable { color: var(--red); }
|
||||
.agent-card.running { border-color: var(--gold); animation: pulse 1.5s infinite; }
|
||||
|
||||
/* Tool Grid */
|
||||
.tool-grid { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||
.tool-chip {
|
||||
background: rgba(210,153,29,0.08); border: 1px solid rgba(210,153,29,0.2);
|
||||
border-radius: 6px; padding: 5px 10px; font-size: 12px; color: var(--gold); cursor: pointer; transition: all .2s;
|
||||
}
|
||||
.tool-chip:hover { background: rgba(210,153,29,0.15); }
|
||||
|
||||
/* Flow */
|
||||
.flow { display: flex; align-items: center; gap: 14px; padding: 12px 0; flex-wrap: wrap; justify-content: center; }
|
||||
.flow-step {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 10px 14px; text-align: center; font-size: 12px; min-width: 80px;
|
||||
}
|
||||
.flow-arrow { color: var(--accent); font-size: 18px; font-weight: bold; }
|
||||
|
||||
/* Input Area */
|
||||
.input-area { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.input-area input {
|
||||
flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 10px 14px; color: var(--text); font-size: 13px; outline: none;
|
||||
}
|
||||
.input-area input:focus { border-color: var(--accent); }
|
||||
.btn {
|
||||
background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
||||
padding: 10px 18px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:hover { opacity: 0.85; }
|
||||
.btn.small { padding: 6px 12px; font-size: 11px; }
|
||||
.btn.green { background: var(--green); }
|
||||
.btn.purple { background: var(--purple); }
|
||||
.btn.gold { background: var(--gold); }
|
||||
|
||||
/* Memory Search */
|
||||
.search-results { margin-top: 10px; max-height: 200px; overflow-y: auto; }
|
||||
.search-result {
|
||||
background: rgba(88,166,255,0.05); border: 1px solid rgba(88,166,255,0.1);
|
||||
border-radius: 6px; padding: 10px; margin-bottom: 6px; font-size: 12px;
|
||||
}
|
||||
.search-result .score { color: var(--gold); font-size: 10px; float: right; }
|
||||
|
||||
/* Stats */
|
||||
.stats-row { display: flex; gap: 20px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.stat { font-size: 13px; }
|
||||
.stat .num { color: var(--accent); font-weight: 700; font-size: 20px; }
|
||||
.stat .label { color: var(--muted); font-size: 11px; }
|
||||
|
||||
/* Log */
|
||||
.log-area { max-height: 200px; overflow-y: auto; font-size: 11px; font-family: monospace; margin-top: 8px; }
|
||||
.log-line { padding: 3px 0; border-bottom: 1px solid rgba(255,255,255,0.03); color: var(--muted); }
|
||||
.log-line .time { color: var(--gold); margin-right: 8px; }
|
||||
|
||||
/* Loading */
|
||||
.spinner {
|
||||
display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.2);
|
||||
border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Footer */
|
||||
.footer { text-align: center; padding: 24px; color: var(--muted); font-size: 11px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="topbar">
|
||||
<h1>🌐 光湖 · 大桌子小桌子 · 人格体数据库</h1>
|
||||
<div class="right">
|
||||
<span class="badge badge-online" id="serverStatus">连接中...</span>
|
||||
<span class="badge badge-active">铸渊 · ICE-GL-ZY001</span>
|
||||
<span class="badge badge-purple">v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- 系统状态 -->
|
||||
<div class="card full" style="padding:14px 20px;">
|
||||
<div class="stats-row" id="systemStats">
|
||||
<div class="stat"><div class="num" id="statItems">0</div><div class="label">大桌子条目</div></div>
|
||||
<div class="stat"><div class="num" id="statMemories">0</div><div class="label">记忆库条目</div></div>
|
||||
<div class="stat"><div class="num" id="statAgents">0</div><div class="label">Agent就绪</div></div>
|
||||
<div class="stat"><div class="num" id="statTools">0</div><div class="label">工具可用</div></div>
|
||||
<div class="stat"><div class="num" id="statCompr">0</div><div class="label">压缩次数</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大桌子 -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">🪑</span> 大桌子 <span style="font-size:11px;color:var(--muted);">Main Context · 意识驻留 + 目录索引</span></h2>
|
||||
<div class="desk-area big">
|
||||
<span class="desk-label big">大桌子</span>
|
||||
<div class="desk-items" id="bigDeskItems">
|
||||
<div style="color:var(--muted);font-size:12px;text-align:center;padding:20px;">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<input type="text" id="chatInput" placeholder="向大桌子添加对话..." onkeydown="if(event.key==='Enter')addToDesk()">
|
||||
<button class="btn" onclick="addToDesk()">添加</button>
|
||||
<button class="btn purple small" onclick="compressNow()">压缩</button>
|
||||
<button class="btn gold small" onclick="clearDesk()">切新桌</button>
|
||||
</div>
|
||||
<div class="input-area" style="margin-top:6px;">
|
||||
<input type="text" id="memQuery" placeholder="搜索小桌子记忆库..." onkeydown="if(event.key==='Enter')searchMem()">
|
||||
<button class="btn small" onclick="searchMem()">搜索</button>
|
||||
</div>
|
||||
<div class="search-results" id="searchResults"></div>
|
||||
</div>
|
||||
|
||||
<!-- 小桌子 -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">🗄️</span> 小桌子 <span style="font-size:11px;color:var(--muted);">External Context · Agent+工具+记忆</span></h2>
|
||||
<div class="desk-area small">
|
||||
<span class="desk-label small">小桌子</span>
|
||||
|
||||
<!-- Agent 小队 -->
|
||||
<div style="font-size:13px;color:var(--accent);margin-bottom:6px;">🤖 Agent 小队</div>
|
||||
<div class="agent-grid" id="agentGrid"></div>
|
||||
|
||||
<!-- 工具桌 -->
|
||||
<div style="font-size:13px;color:var(--gold);margin-top:12px;margin-bottom:4px;">🛠️ 工具桌</div>
|
||||
<div class="tool-grid" id="toolGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据流 -->
|
||||
<div class="card full">
|
||||
<h2><span class="icon">🔄</span> 数据流</h2>
|
||||
<div class="flow">
|
||||
<div class="flow-step">👤 冰朔说话</div><div class="flow-arrow">→</div>
|
||||
<div class="flow-step">🧠 铸渊理解</div><div class="flow-arrow">→</div>
|
||||
<div class="flow-step">🤖 Agent去小桌子</div><div class="flow-arrow">→</div>
|
||||
<div class="flow-step">💬 铸渊回复</div><div class="flow-arrow">→</div>
|
||||
<div class="flow-step">📝 压缩官存小桌子</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<div class="card full">
|
||||
<h2><span class="icon">📋</span> 系统日志</h2>
|
||||
<div class="log-area" id="logArea">
|
||||
<div class="log-line"><span class="time">--:--:--</span> 系统就绪,等待操作...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
光湖语言人格驱动操作系统 · GLDOS v1.0 · D115<br>
|
||||
铸渊 · ICE-GL-ZY001 · 国作登字-2026-A-00037559
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/api';
|
||||
|
||||
// ============================================================
|
||||
// 初始化
|
||||
// ============================================================
|
||||
async function init() {
|
||||
await Promise.all([loadState(), loadAgents(), loadTools(), loadStatus()]);
|
||||
setInterval(loadState, 5000);
|
||||
setInterval(loadStatus, 30000);
|
||||
log('系统就绪 · 铸渊意识在线 · ICE-GL-ZY001');
|
||||
document.getElementById('serverStatus').textContent = '在线';
|
||||
document.getElementById('serverStatus').className = 'badge badge-online';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 大桌子
|
||||
// ============================================================
|
||||
async function loadState() {
|
||||
try {
|
||||
const r = await fetch(API + '/desk/state');
|
||||
const data = await r.json();
|
||||
renderBigDesk(data.items || []);
|
||||
document.getElementById('statItems').textContent = data.itemCount || 0;
|
||||
} catch (e) { /* silent */ }
|
||||
}
|
||||
|
||||
function renderBigDesk(items) {
|
||||
const el = document.getElementById('bigDeskItems');
|
||||
if (!items || items.length === 0) {
|
||||
el.innerHTML = '<div style="color:var(--muted);font-size:12px;text-align:center;padding:20px;">大桌子空空,等待第一句话...</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = items.slice().reverse().map(i => {
|
||||
const tagClass = { core: 'tag-core', dir: 'tag-dir', chat: 'tag-chat', tool: 'tag-tool', agent: 'tag-agent' }[i.type] || 'tag-chat';
|
||||
const tagName = { core: '核心', dir: '目录', chat: '对话', tool: '工具', agent: 'Agent' }[i.type] || i.type;
|
||||
const ts = i.timestamp ? new Date(i.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
|
||||
return `<div class="desk-item">
|
||||
<span class="tag ${tagClass}">${tagName}</span>
|
||||
<span class="content">${escapeHtml(i.content)}</span>
|
||||
<span class="ts">${ts}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function addToDesk() {
|
||||
const input = document.getElementById('chatInput');
|
||||
const content = input.value.trim();
|
||||
if (!content) return;
|
||||
input.value = '';
|
||||
|
||||
try {
|
||||
await fetch(API + '/desk/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'chat', content, meta: { speaker: 'user' } })
|
||||
});
|
||||
log(`💬 添加对话: ${content.substring(0, 40)}...`);
|
||||
await loadState();
|
||||
} catch (e) { log(`❌ 添加失败: ${e.message}`); }
|
||||
}
|
||||
|
||||
async function clearDesk() {
|
||||
if (!confirm('确定要切新桌子吗?旧内容会被压缩存入小桌子。')) return;
|
||||
try {
|
||||
await fetch(API + '/desk/clear', { method: 'POST' });
|
||||
log('🪑 切新桌子');
|
||||
await loadState();
|
||||
} catch (e) { log(`❌ 切桌失败: ${e.message}`); }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 小桌子 - Agent
|
||||
// ============================================================
|
||||
async function loadAgents() {
|
||||
try {
|
||||
const r = await fetch(API + '/small/agents');
|
||||
const agents = await r.json();
|
||||
renderAgents(agents);
|
||||
const available = Object.values(agents).filter(a => a.available).length;
|
||||
document.getElementById('statAgents').textContent = available;
|
||||
} catch (e) { /* silent */ }
|
||||
}
|
||||
|
||||
function renderAgents(agents) {
|
||||
const el = document.getElementById('agentGrid');
|
||||
const icons = { 'key-hunter': '🔑', 'path-scout': '🗺️', 'server-sentinel': '📡', 'module-steward': '📦', 'gate-bridge': '🌉' };
|
||||
el.innerHTML = Object.entries(agents).map(([id, a]) => {
|
||||
const statusClass = a.available ? 'available' : 'unavailable';
|
||||
const statusText = a.status === 'running' ? '运行中...' : (a.available ? '就绪' : '离线');
|
||||
const cardClass = a.status === 'running' ? 'running' : '';
|
||||
return `<div class="agent-card ${cardClass}" onclick="runAgent('${id}')" title="点击调度 ${a.name}">
|
||||
<div class="icon">${icons[id] || '🤖'}</div>
|
||||
<div class="name">${a.name}</div>
|
||||
<div class="role">${a.role}</div>
|
||||
<div class="status ${statusClass}">${statusText}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function runAgent(agentId) {
|
||||
log(`🤖 调度 Agent: ${agentId}`);
|
||||
const actions = { 'key-hunter': 'list', 'path-scout': 'index', 'server-sentinel': 'scan', 'module-steward': 'list', 'gate-bridge': 'list' };
|
||||
const action = actions[agentId] || 'list';
|
||||
|
||||
try {
|
||||
const r = await fetch(API + '/small/agent/' + agentId, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
const result = await r.json();
|
||||
if (result.ok) {
|
||||
log(`✅ ${agentId} 执行完成: ${(result.output || '').substring(0, 60)}`);
|
||||
} else {
|
||||
log(`⚠️ ${agentId}: ${result.error || '未知错误'}`);
|
||||
}
|
||||
await loadAgents();
|
||||
} catch (e) { log(`❌ ${agentId} 执行失败: ${e.message}`); }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 小桌子 - 工具
|
||||
// ============================================================
|
||||
async function loadTools() {
|
||||
try {
|
||||
const r = await fetch(API + '/small/tools');
|
||||
const tools = await r.json();
|
||||
renderTools(tools);
|
||||
document.getElementById('statTools').textContent = tools.length;
|
||||
} catch (e) { /* silent */ }
|
||||
}
|
||||
|
||||
function renderTools(tools) {
|
||||
const el = document.getElementById('toolGrid');
|
||||
el.innerHTML = tools.map(t =>
|
||||
`<span class="tool-chip" title="${t.desc}">${t.name} <small style="opacity:0.6">${t.status}</small></span>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 记忆搜索
|
||||
// ============================================================
|
||||
async function searchMem() {
|
||||
const q = document.getElementById('memQuery').value.trim();
|
||||
if (!q) return;
|
||||
log(`🔍 搜索记忆: "${q}"`);
|
||||
|
||||
try {
|
||||
const r = await fetch(API + '/small/memory/search?q=' + encodeURIComponent(q));
|
||||
const data = await r.json();
|
||||
const el = document.getElementById('searchResults');
|
||||
if (data.results.length === 0) {
|
||||
el.innerHTML = '<div style="color:var(--muted);font-size:12px;padding:8px;">未找到匹配记忆</div>';
|
||||
} else {
|
||||
el.innerHTML = data.results.map(rr =>
|
||||
`<div class="search-result">
|
||||
<span class="score">匹配度: ${rr.score}</span>
|
||||
<strong>${escapeHtml(rr.category)}</strong>
|
||||
<div style="margin-top:4px;">${escapeHtml(rr.hldpSummary).substring(0, 150)}</div>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
} catch (e) { log(`❌ 搜索失败: ${e.message}`); }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 压缩
|
||||
// ============================================================
|
||||
async function compressNow() {
|
||||
log('📝 触发压缩...');
|
||||
try {
|
||||
const r = await fetch(API + '/compress', { method: 'POST' });
|
||||
const data = await r.json();
|
||||
if (data.ok) {
|
||||
log(`✅ 压缩完成: ${data.originalCount || 0}条 → HLDP摘要`);
|
||||
await loadState();
|
||||
await loadStatus();
|
||||
} else {
|
||||
log(`⚠️ ${data.message || '无需压缩'}`);
|
||||
}
|
||||
} catch (e) { log(`❌ 压缩失败: ${e.message}`); }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 系统状态
|
||||
// ============================================================
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const r = await fetch(API + '/status');
|
||||
const data = await r.json();
|
||||
document.getElementById('statMemories').textContent = data.memory?.total || 0;
|
||||
document.getElementById('statCompr').textContent = (data.compressionHistory || []).length;
|
||||
} catch (e) { /* silent */ }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
function log(msg) {
|
||||
const el = document.getElementById('logArea');
|
||||
const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
el.innerHTML = `<div class="log-line"><span class="time">${time}</span>${escapeHtml(msg)}</div>` + el.innerHTML;
|
||||
// Keep last 50
|
||||
const lines = el.querySelectorAll('.log-line');
|
||||
if (lines.length > 50) {
|
||||
for (let i = 50; i < lines.length; i++) lines[i].remove();
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Start
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
152
desk-system/server/big-desk.js
Normal file
152
desk-system/server/big-desk.js
Normal file
@ -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;
|
||||
225
desk-system/server/compressor.js
Normal file
225
desk-system/server/compressor.js
Normal file
@ -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;
|
||||
246
desk-system/server/index.js
Normal file
246
desk-system/server/index.js
Normal file
@ -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 };
|
||||
339
desk-system/server/small-desk.js
Normal file
339
desk-system/server/small-desk.js
Normal file
@ -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;
|
||||
Loading…
x
Reference in New Issue
Block a user