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:
冰朔 2026-05-29 00:20:02 +08:00
parent 3f8270b22e
commit 49030d2cbc
6 changed files with 1452 additions and 0 deletions

14
desk-system/package.json Normal file
View 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"
}
}

View 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>

View 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;

View 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
View 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 };

View 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;