// ===== State =====
let state = {
status: {},
kernel: null,
settings: {},
meta: {},
pluginMeta: {},
settingsPlugins: ['core'],
selectedSection: 'core',
messages: [],
chatLoading: false,
chatStage: '',
healthResult: null,
starmapInit: false,
starmapLoading: false,
starmapData: null,
chatHistory: [],
terminals: [],
cmdHistory: [],
eventSource: null,
lang: localStorage.getItem('ha-lang') || 'zh',
connections: [], currentConn: null,
};
// ===== I18n =====
window._i18n = {
navOverview: ['概览','Overview'],
navChat: ['对话','Chat'],
navPlugins: ['插件','Plugins'],
navSettings: ['设置','Settings'],
navAdapters: ['适配器','Adapters'],
navKernel: ['内核','Kernel'],
navLogout: ['退出登录','Logout'],
themeToggle: ['切换亮色/暗色模式','Toggle theme']
};
function __(zh, en) { return state.lang === 'en' ? en : zh }
function L() { return state.lang }
function toggleLang() {
state.lang = state.lang === 'zh' ? 'en' : 'zh';
localStorage.setItem('ha-lang', state.lang);
document.querySelectorAll('[data-i18n]').forEach(function(el) {
var k = el.getAttribute('data-i18n');
var m = window._i18n && window._i18n[k];
if (m) el.textContent = __(m[0], m[1]);
});
renderAll();
}
function applyI18n() {
var lang = state.lang;
var btn = document.getElementById('lang-btn');
if (btn) btn.textContent = lang === 'zh' ? 'EN' : '中';
document.querySelectorAll('[data-i18n]').forEach(function(el) {
var k = el.getAttribute('data-i18n');
var m = window._i18n && window._i18n[k];
if (m) el.textContent = lang === 'en' ? m[1] : m[0];
});
}
// ===== Theme =====
function setTheme(name) {
document.documentElement.setAttribute('data-theme', name);
localStorage.setItem('ha-theme', name);
document.getElementById('theme-btn').textContent = name === 'light' ? '☀️' : '🌙';
}
function toggleTheme() {
var cur = document.documentElement.getAttribute('data-theme');
setTheme(cur === 'light' ? 'dark' : 'light');
}
(function() {
var saved = localStorage.getItem('ha-theme');
setTheme(saved || 'dark');
})();
// ===== Utility =====
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
function timeAgo(t) {
var s = Math.floor((Date.now() - new Date(t).getTime()) / 1000);
if (s < 60) return s + '秒前';
var m = Math.floor(s / 60);
if (m < 60) return m + '分钟前';
return Math.floor(m / 60) + '小时前';
}
function toast(m, isError) {
var t = document.getElementById('toast');
t.textContent = m;
t.className = 'toast' + (isError ? ' error' : '');
t.style.display = 'block';
setTimeout(function() { t.style.display = 'none' }, 3000);
}
// ===== API =====
async function api(p, o) {
if (!state.currentConn) throw new Error(__('未选择连接','No connection selected'));
var opts = o || {};
var headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) };
if (state.currentConn.apiKey) headers['X-API-Key'] = state.currentConn.apiKey;
var r = await fetch(state.currentConn.url + '/api/v1' + p, { ...opts, headers: headers });
if (r.status === 401) throw new Error(__('认证失败','unauthorized'));
if (opts.raw) return r;
var ct = r.headers.get('content-type') || '';
if (ct.includes('json')) return r.json();
return r.text();
}
// ===== Navigation =====
function switchTab(n) {
document.querySelectorAll('.tab-content').forEach(function(e) { e.classList.remove('active') });
var el = document.getElementById('tab-' + n);
if (el) el.classList.add('active');
document.querySelectorAll('nav a').forEach(function(e) { e.classList.remove('active') });
var match = document.querySelector('nav a[onclick*="' + n + '"]');
if (match) match.classList.add('active');
renderAll();
}
// ===== Tab Render Dispatch =====
async function doRenderAll() {
try { var s = await api('/status'); state.status = s; state.startedAt = s.startedAt ? new Date(s.startedAt).getTime() : null; updateConnIndicator() } catch(e) {}
try { state.kernel = await api('/kernel') } catch(e) {}
try {
var s = await api('/settings');
state.settings = s.settings || {};
state.meta = s.meta || {};
state.settingsPlugins = s.plugins || ['core'];
state.pluginMeta = s.plugin_meta || {};
} catch(e) {}
try { state.installedPlugins = await api('/plugins') } catch(e) {}
try { await loadTerminals() } catch(e) {}
try { await loadCmdHistory() } catch(e) {}
renderAll();
}
async function renderAll() {
try { var s = await api('/status'); state.status = s; state.startedAt = s.startedAt ? new Date(s.startedAt).getTime() : null } catch(e) {}
try { state.kernel = await api('/kernel') } catch(e) {}
try {
var s = await api('/settings');
state.settings = s.settings || {};
state.meta = s.meta || {};
state.settingsPlugins = s.plugins || ['core'];
state.pluginMeta = s.plugin_meta || {};
} catch(e) {}
try { state.installedPlugins = await api('/plugins') } catch(e) {}
try { await loadTerminals() } catch(e) {}
try { await loadCmdHistory() } catch(e) {}
try { renderOverview() } catch(e) { console.error('renderOverview', e) }
try { renderChat() } catch(e) { console.error('renderChat', e) }
try { renderChatStarmap() } catch(e) { console.error('renderChatStarmap', e) }
try { renderPlugins() } catch(e) { console.error('renderPlugins', e) }
try { renderKernel() } catch(e) { console.error('renderKernel', e) }
try { renderOneSettings() } catch(e) { console.error('renderOneSettings', e) }
try { renderAdapters() } catch(e) { console.error('renderAdapters', e) }
applyI18n();
}
function fmtUptime(ms) {
var s = Math.floor(ms / 1000);
if (s < 60) return s + 's';
var m = Math.floor(s / 60); s = s % 60;
if (m < 60) return m + 'm ' + s + 's';
var h = Math.floor(m / 60); m = m % 60;
return h + 'h ' + m + 'm ' + s + 's';
}
var uptimeTick = null;
function startUptimeTicker() {
if (uptimeTick) clearInterval(uptimeTick);
uptimeTick = setInterval(function() {
var el = document.querySelector('#uptime-val');
if (el && state.startedAt) {
var now = Date.now();
el.textContent = fmtUptime(now - state.startedAt);
} else if (!state.startedAt) {
var el2 = document.querySelector('#uptime-val');
if (el2) el2.textContent = '-';
}
}, 1000);
}
// ===== Overview =====
function statCard(l, v) {
return '
';
}
function renderOverview() {
var s = state.status || {};
var k = state.kernel;
var html = ''
+ statCard(__('运行状态','Status'), s.status || 'unknown', 'running')
+ statCard(__('运行时间','Uptime'), '' + (state.startedAt ? fmtUptime(Date.now() - state.startedAt) : '-') + ' ', 'uptime')
+ statCard(__('插件','Plugins'), (k?.plugins || []).length || 0, 'plugin')
+ statCard(__('版本','Version'), s.version || '0.1.0', 'version')
+ '
';
if (k) {
html += ''
+ '
' + __('LLM 状态','LLM Status') + ' '
+ '
Provider ' + (k.llm?.provider || __('未配置','Not configured')) + '
'
+ '
' + __('可用源','Sources') + ' ' + (k.llm?.sources || 0) + '
'
+ '
' + __('状态','Status') + ' ' + (k.llm?.available ? __('运行中','Running') : __('不可用','Unavailable')) + '
'
+ '
'
+ '
' + __('记忆状态','Memory Status') + ' '
+ '
' + __('图记忆','Graph Memory') + ' ' + (k.memory?.available ? k.memory.entity_count + __(' 实体, ',' entities, ') + k.memory.relation_count + __(' 关系',' relations') : __('未初始化','Uninitialized')) + '
'
+ '
' + __('文档记忆','Document Memory') + ' ' + (k.documents?.available ? k.documents.doc_count + __(' 文档',' docs') : __('未初始化','Uninitialized')) + '
'
+ '
' + __('文本记忆','Text Memory') + ' ' + (k.text_memory?.available ? k.text_memory.file_count + __(' 文件',' files') : __('未初始化','Uninitialized')) + '
'
+ '
' + __('知识库','Knowledge') + ' ' + (k.knowledge?.available ? k.knowledge.item_count + __(' 项',' items') : __('未初始化','Uninitialized')) + '
'
+ '
';
}
html += '' + __('运行时','Runtime') + ' '
+ statCard('Goroutines', k?.runtime?.goroutines || '-', '')
+ statCard(__('内存','Memory'), k?.runtime?.memory_mb ? k.runtime.memory_mb + ' MB' : '-', '')
+ statCard('Go ' + __('版本','Version'), k?.runtime?.go_version || '-', '')
+ '
';
document.getElementById('tab-overview').innerHTML = html;
}
// ===== Chat =====
var _chatLayoutBuilt = false;
function buildChatLayout() {
var cont = document.getElementById('tab-chat');
var k = state.kernel || {};
var html = '';
html += '
' + __('对话','Chat') + ' ' + escHtml(state.chatStage || '') + ' ';
if (state.messages.length === 0) {
html += '
' + __('开始对话以测试 Agent 回复','Start a conversation to test Agent replies') + '
';
}
html += '
'
+ '
'
+ ' '
+ '' + __('发送','Send') + ' '
+ '
';
html += '
';
cont.innerHTML = html;
_chatLayoutBuilt = true;
}
function renderChat() {
if (!_chatLayoutBuilt) { buildChatLayout(); renderChatStarmap(); renderTerminals(); renderCmdHistory() }
var msgsEl = document.getElementById('chat-msgs');
if (!msgsEl) return;
var msgs = state.messages;
var html = '';
if (msgs.length === 0) {
html = '' + __('开始对话以测试 Agent 回复','Start a conversation to test Agent replies') + '
';
} else {
msgs.forEach(function(m, i) {
var role = m.role || 'user';
var c = m.content || '';
if (role === 'assistant') {
if (typeof marked !== 'undefined') { c = marked.parse(c) } else { c = '' + escHtml(c) + ' ' }
} else if (role === 'system') {
c = escHtml(c);
} else {
c = escHtml(c);
}
var rc = '';
if (m.reasoning_content) {
var rcBody = (typeof marked !== 'undefined' ? marked.parse(m.reasoning_content) : escHtml(m.reasoning_content));
rc = ''
+ '
' + __('收起思考','Collapse') + '
'
+ '
' + rcBody + '
';
}
var tcs = '';
if (m.tool_calls && m.tool_calls.length > 0) {
m.tool_calls.forEach(function(tc) {
var argsStr = typeof tc.args === 'object' ? JSON.stringify(tc.args, null, 1) : (tc.args || '');
var resultStr = tc.result ? (typeof tc.result === 'object' ? JSON.stringify(tc.result, null, 1).substring(0, 200) : String(tc.result).substring(0, 200)) : '';
var statusIcon = tc.status === 'denied' ? '⛔' : '🔧';
tcs += '';
});
}
var body = rc + tcs + '' + c + '
';
if (role === 'system') {
html += '';
} else {
html += ''
+ '
' + (role === 'user' ? 'U' : 'A') + '
'
+ '
'
+ '
';
}
});
}
msgsEl.innerHTML = html;
msgsEl.scrollTop = msgsEl.scrollHeight;
updateChatBadge();
}
function updateChatBadge() {
var badge = document.getElementById('chat-stage');
if (!badge) return;
badge.textContent = state.chatStage || '';
badge.style.display = state.chatLoading ? 'inline' : 'none';
}
function rerenderChat() { renderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory() }
function renderChatStarmap() {
var cont = document.getElementById('sm-container-chat');
if (!cont) return;
if (window._THREE_FAILED || (!window.THREE && window._THREE_FAILED !== undefined)) {
cont.innerHTML = '' + __('3D 星图不可用(CDN 加载失败)','Star map unavailable (CDN load failed)') + '
';
state.starmapInit = true;
state.starmapLoading = false;
return;
}
if (!window.THREE) {
cont.innerHTML = '';
state.starmapInit = false;
state.starmapLoading = false;
return;
}
if (cont.querySelector('canvas')) {
var rect = cont.getBoundingClientRect();
if (starmapRen && rect.width > 0) starmapRen.setSize(rect.width, Math.max(rect.height, 250));
return;
}
if (state.starmapInit) {
if (starmapRen) {
var rect = cont.getBoundingClientRect();
if (rect.width > 0) starmapRen.setSize(rect.width, Math.max(rect.height, 250));
cont.appendChild(starmapRen.domElement);
starmapRen.domElement.style.display = 'block';
} else {
// starmapRen was destroyed (e.g. re-render cycle), restart
state.starmapInit = false;
state.starmapLoading = false;
}
return;
}
if (state.starmapLoading) return;
state.starmapLoading = true;
loadChatStarmapData();
}
async function loadChatStarmapData() {
try {
var resp = await api('/memory/graph');
if (!resp || !resp.success || !resp.data || !resp.data.nodes || resp.data.nodes.length === 0) {
document.getElementById('sm-container-chat').innerHTML
= ''
+ __('暂无记忆数据','No memory data') + '
';
state.starmapInit = true;
state.starmapLoading = false;
return;
}
var d = resp.data;
starmapNodes = d.nodes || [];
starmapEdges = d.edges || [];
state.starmapInit = true;
state.starmapLoading = false;
initChatStarmap();
} catch(e) {
document.getElementById('sm-container-chat').innerHTML
= ''
+ __('加载失败','Load failed') + '
';
state.starmapInit = true;
state.starmapLoading = false;
}
}
function getStarmapBg() {
return 0x0a0a1a;
}
function initChatStarmap() {
var cont = document.getElementById('sm-container-chat');
if (!cont) return;
var rect = cont.getBoundingClientRect();
var w = Math.max(rect.width || 300, 100);
var h = Math.max(rect.height || 250, 100);
if (starmapRen) {
starmapRen.setSize(w, h);
cont.appendChild(starmapRen.domElement);
starmapRen.domElement.style.display = 'block';
return;
}
starmapScene = new THREE.Scene();
starmapScene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
starmapCam = new THREE.PerspectiveCamera(60, w / h, 0.1, 2000);
starmapCam.position.set(0, 20, 40);
starmapRen = new THREE.WebGLRenderer({ antialias: true, alpha: true });
starmapRen.setSize(w, h);
starmapRen.setPixelRatio(Math.min(window.devicePixelRatio, 2));
starmapRen.setClearColor(0x0a0a1a, 1);
cont.innerHTML = '';
cont.appendChild(starmapRen.domElement);
starmapCtrl = new THREE.OrbitControls(starmapCam, starmapRen.domElement);
starmapCtrl.enableDamping = true;
starmapCtrl.dampingFactor = 0.05;
starmapCtrl.rotateSpeed = 0.5;
starmapCtrl.zoomSpeed = 0.8;
var al = new THREE.AmbientLight(0x444466, 0.6);
starmapScene.add(al);
var dl = new THREE.DirectionalLight(0xffffff, 0.8);
dl.position.set(50, 100, 50);
starmapScene.add(dl);
createStarField();
createNebula();
buildChatStarmapGraph();
starmapRen.domElement.addEventListener('mousemove', onStarmapMove);
starmapRen.domElement.addEventListener('click', onStarmapClick);
window.addEventListener('resize', onStarmapResize);
if (starmapRaf) cancelAnimationFrame(starmapRaf);
starmapAnimate();
}
function buildChatStarmapGraph() {
starmapNodeMeshes.forEach(function(m) { starmapScene.remove(m) });
starmapEdgeLines.forEach(function(l) { starmapScene.remove(l) });
starmapNodeMeshes = [];
starmapEdgeLines = [];
if (starmapNodes.length === 0) return;
// Calculate node degrees for leaf node detection
var nodeDegs = {};
starmapNodes.forEach(function(n) { nodeDegs[n.id] = 0 });
starmapEdges.forEach(function(e) {
nodeDegs[e.source_id] = (nodeDegs[e.source_id] || 0) + 1;
nodeDegs[e.target_id] = (nodeDegs[e.target_id] || 0) + 1;
});
var nodeMap = {};
starmapNodes.forEach(function(n) { nodeMap[n.id] = n });
var sorted = starmapNodes.slice().sort(function(a, b) {
return (b.mention_count || 0) - (a.mention_count || 0);
});
var mc = sorted.map(function(n) { return n.mention_count || 0 });
var maxMc = Math.max(...mc, 1), minMc = Math.min(...mc, 0), rng = maxMc - minMc || 1;
// Layout positions
var pos = {};
var baseR = 15, maxR = 80;
var total = sorted.length;
var acc = 0;
sorted.forEach(function(n, i) {
var m = n.mention_count || 0, mn = rng > 0 ? (m - minMc) / rng : 0;
var radius = baseR + mn * (maxR - baseR);
var baseStep = (Math.PI * 2) / total;
var extra = mn * baseStep * 2;
var angle = acc + extra / 2;
acc += baseStep + extra;
pos[n.id] = {
x: radius * Math.cos(angle),
y: (Math.random() - 0.5) * (10 + mn * 20),
z: radius * Math.sin(angle),
mn: mn,
rad: radius
};
});
// Leaf nodes (degree 1) reposition near parent
sorted.forEach(function(n) {
var deg = nodeDegs[n.id] || 0;
if (deg !== 1) return;
var edge = starmapEdges.find(function(e) { return e.source_id === n.id || e.target_id === n.id });
if (!edge) return;
var parentId = edge.source_id === n.id ? edge.target_id : edge.source_id;
if (!pos[parentId]) return;
var pp = pos[parentId];
var m = n.mention_count || 0, mn = rng > 0 ? (m - minMc) / rng : 0;
var off = 6 + mn * 8 + Math.random() * 4;
var a2 = Math.random() * Math.PI * 2;
pos[n.id] = {
x: pp.x + off * Math.cos(a2),
y: pp.y + (Math.random() - 0.5) * (4 + mn * 6),
z: pp.z + off * Math.sin(a2),
mn: mn,
rad: off
};
});
// Force-directed simulation
for (var it = 0; it < 50; it++) {
var ids = Object.keys(pos);
// Repulsion
for (var i = 0; i < ids.length; i++) {
for (var j = i + 1; j < ids.length; j++) {
var a = pos[ids[i]], b = pos[ids[j]];
var dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z, d = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
var rf = 0.5 + (a.mn + b.mn) * 0.5;
if (d < 25) {
var force = (0.06 * rf) / Math.max(d, 0.5);
a.x += dx / d * force; a.y += dy / d * force; a.z += dz / d * force;
b.x -= dx / d * force; b.y -= dy / d * force; b.z -= dz / d * force;
}
}
}
// Attraction along edges
starmapEdges.forEach(function(e) {
var a = pos[e.source_id], b = pos[e.target_id];
if (!a || !b) return;
var dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z, d = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
var af = Math.max(0.3, 1.0 - (a.mn + b.mn) * 0.3);
if (d > 20) {
var force = 0.04 * af;
a.x += dx / d * force; a.y += dy / d * force; a.z += dz / d * force;
b.x -= dx / d * force; b.y -= dy / d * force; b.z -= dz / d * force;
}
});
// Centering constraint
ids.forEach(function(id) {
var p = pos[id];
var dist = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
var maxA = maxR * 1.5;
if (dist > maxA) { var s = maxA / dist; p.x *= s; p.y *= s; p.z *= s }
});
}
// Create nodes
starmapNodes.forEach(function(n) {
var p = pos[n.id];
if (!p) return;
var mn = n.mention_count || 0, mnr = rng > 0 ? (mn - minMc) / rng : 0;
var rad = 0.5 + mnr * 2.0;
var col = smTypeColors[n.type] || 0xcccccc;
var ei = 0.3 + mnr * 0.7;
var g = new THREE.SphereGeometry(rad, 16, 12);
var mat = new THREE.MeshPhongMaterial({ color: col, emissive: col, emissiveIntensity: ei, shininess: 30 });
var mesh = new THREE.Mesh(g, mat);
mesh.position.set(p.x, p.y, p.z);
mesh.userData.nodeData = n;
mesh.userData.nodeId = n.id;
mesh.userData.baseEmissive = ei;
// Glow sphere
var gr = rad * 1.2 + mnr * 0.5;
var gg = new THREE.SphereGeometry(gr, 16, 12);
var gm = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 0.12 + mnr * 0.08, side: THREE.BackSide, blending: THREE.AdditiveBlending });
var gs = new THREE.Mesh(gg, gm);
mesh.add(gs);
mesh.userData.glowSphere = gs;
// Label sprite
var canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 64;
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 256, 64);
ctx.font = 'Bold 24px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = '#aaccff';
ctx.shadowBlur = 8;
ctx.fillStyle = '#ffffff';
ctx.fillText((n.name || n.id).substring(0, 12), 128, 32);
var tex = new THREE.CanvasTexture(canvas);
tex.needsUpdate = true;
var spMat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.9, depthTest: false, depthWrite: false, blending: THREE.AdditiveBlending });
var sprite = new THREE.Sprite(spMat);
sprite.scale.set(8, 2, 1);
sprite.position.y = rad + 2;
mesh.add(sprite);
starmapScene.add(mesh);
starmapNodeMeshes.push(mesh);
});
// Create edges
starmapEdges.forEach(function(e) {
var a = pos[e.source_id], b = pos[e.target_id];
if (!a || !b) return;
var col = smEdgeColors[e.relation_type] || smEdgeColors[e.type] || 0x444466;
var pts = [
new THREE.Vector3(a.x, a.y, a.z),
new THREE.Vector3(b.x, b.y, b.z)
];
var geo = new THREE.BufferGeometry().setFromPoints(pts);
var mat = new THREE.LineBasicMaterial({ color: col, transparent: true, opacity: 0.4 });
var line = new THREE.Line(geo, mat);
line.userData = { edgeId: e.id, edgeData: e };
starmapScene.add(line);
starmapEdgeLines.push(line);
});
}
async function sendChat() {
var inp = document.getElementById('chat-input');
var btn = document.getElementById('chat-send-btn');
var text = inp.value.trim();
if (!text || state.chatLoading) return;
state.messages.push({ role: 'user', content: text });
inp.value = '';
rerenderChat();
state.chatLoading = true;
state.chatStage = __('等待AI回复...','Waiting for AI...');
btn.disabled = true;
btn.textContent = '';
rerenderChat();
try {
var r = await api('/chat', { method: 'POST', body: JSON.stringify({ message: text }) });
state.chatStage = '';
var last = state.messages[state.messages.length - 1];
console.log('[sendChat] POST returned, last msg:', last ? {role:last.role, _streaming:last._streaming, _final:last._final, tool_calls:last.tool_calls?.length, content_len:last.content?.length} : null);
if (last && last.role === 'assistant' && last._streaming) {
console.log('[sendChat] updating existing streaming msg, tool_calls before:', last.tool_calls?.length);
last.content = r.response || __('(无响应)','(no response)');
last.reasoning_content = r.reasoning_content || '';
last._final = true;
delete last._streaming;
} else {
state.messages.push({
role: 'assistant',
content: r.response || __('(无响应)','(no response)'),
reasoning_content: r.reasoning_content,
tool_calls: last && last.role === 'assistant' && last.tool_calls ? last.tool_calls : [],
_final: true
});
}
rerenderChat();
} catch(e) {
state.messages.push({ role: 'assistant', content: __('错误: ','Error: ') + e.message, _final: true });
rerenderChat();
toast(__('请求失败: ','Request failed: ') + e.message, true);
} finally {
state.chatLoading = false;
state.chatStage = '';
btn.disabled = false;
btn.textContent = __('发送','Send');
rerenderChat();
}
}
async function queryMemoryChat() {
var q = document.getElementById('mem-query')?.value;
var r = document.getElementById('mem-result-chat');
if (!r || !q) return;
r.innerHTML = '
';
try {
var data = await api('/memory?q=' + encodeURIComponent(q) + '&depth=2');
r.innerHTML = '' + escHtml(JSON.stringify(data, null, 2)) + ' ';
} catch(e) {
r.innerHTML = '' + __('查询失败: ','Query failed: ') + escHtml(e.message) + '
';
}
}
async function queryMemoryContext() {
var q = document.getElementById('ctx-query')?.value;
var r = document.getElementById('ctx-result');
if (!r) return;
r.innerHTML = '
';
try {
var data = await api('/memory/context?q=' + encodeURIComponent(q || ''));
var ctx = data?.context || __('无上下文','No context');
var summary = data?.summary || '';
var entities = data?.entities || [];
var tk = data?.token_estimate || 0;
var html = '';
if (summary) html += '
' + __('摘要','Summary') + ' ' + escHtml(summary) + '
';
html += '
Token ' + __('预估','Estimate') + ' ' + tk + '
';
if (entities.length) {
html += '
' + __('实体','Entities') + ' '
+ entities.map(function(e) { return escHtml(e.name || e.id || '') }).join(', ')
+ '
';
}
html += '
' + escHtml(ctx) + ' ';
r.innerHTML = html;
} catch(e) {
r.innerHTML = '' + __('获取失败: ','Get failed: ') + escHtml(e.message) + '
';
}
}
async function searchKnowledgeChat() {
var q = document.getElementById('know-query')?.value;
var r = document.getElementById('know-result-chat');
if (!r || !q) return;
r.innerHTML = '
';
try {
var data = await api('/knowledge?q=' + encodeURIComponent(q));
r.innerHTML = '' + escHtml(JSON.stringify(data, null, 2)) + ' ';
} catch(e) {
r.innerHTML = '' + __('搜索失败: ','Search failed: ') + escHtml(e.message) + '
';
}
}
async function createKnowledgeChat() {
var name = document.getElementById('know-name')?.value;
var content = document.getElementById('know-content')?.value;
if (!name || !content) { toast(__('名称和内容不能为空','Name and content cannot be empty'), true); return }
try {
var r = await api('/knowledge', { method: 'POST', body: JSON.stringify({ name: name, content: content }) });
if (r.status || r.id) {
toast(__('知识「','Knowledge "') + name + __('」已创建','" created'));
document.getElementById('know-name').value = '';
document.getElementById('know-content').value = '';
} else {
toast(__('创建失败','Create failed'), true);
}
} catch(e) {
toast(__('创建失败: ','Create failed: ') + e.message, true);
}
}
function switchChatSub(tab, el) {
var cards = {
'memory': document.getElementById('chat-sub-memory'),
'context': document.getElementById('chat-sub-context'),
'knowledge': document.getElementById('chat-sub-knowledge')
};
Object.keys(cards).forEach(function(k) {
var c = cards[k];
if (c) c.style.display = k === tab ? 'block' : 'none';
});
if (el) {
var parent = el.parentElement;
if (parent) {
Array.from(parent.children).forEach(function(ch) { ch.classList.remove('active') });
el.classList.add('active');
}
}
if (tab === 'context') queryMemoryContext();
}
async function loadChatHistory() {
try { var data = await api('/chat/history'); if (data && data.messages) state.messages = data.messages } catch(e) {}
}
async function loadTerminals() {
try { var data = await api('/terminals'); if (data && data.terminals) state.terminals = data.terminals } catch(e) {}
}
async function loadCmdHistory() {
try { var data = await api('/cmd/history'); if (data && data.history) state.cmdHistory = data.history } catch(e) {}
}
function renderTerminals() {
var r = document.getElementById('term-list');
var cnt = document.getElementById('term-count');
if (!r) return;
var list = state.terminals || [];
if (cnt) cnt.textContent = list.length;
if (list.length === 0) {
r.innerHTML = '' + __('暂无终端会话','No terminal sessions') + '
';
return;
}
var html = '';
list.forEach(function(t, i) {
var detailId = 'term-detail-' + i;
html += '';
html += '
';
html += '' + escHtml(t.id || '-') + ' ';
html += '' + escHtml(t.command || '') + ' ';
html += '' + (t.running ? __('运行中','Running') : __('已关闭','Closed')) + ' ';
html += '' + escHtml(t.created_at || '') + ' ';
html += '
';
html += '
';
html += '
ID ' + escHtml(t.id || '-') + '
';
html += '
' + __('命令','Command') + ' ' + escHtml(t.command || '-') + '
';
html += '
' + __('状态','Status') + ' ' + (t.running ? __('运行中','Running') : __('已关闭','Closed')) + '
';
html += '
' + __('创建时间','Created') + ' ' + escHtml(t.created_at || '-') + '
';
if (t.uptime) html += '
' + __('运行时长','Uptime') + ' ' + escHtml(t.uptime) + '
';
if (t.output) {
html += '
' + __('输出预览','Output') + ' ' + escHtml((t.output || '').substring(0, 500)) + ' ';
}
html += '
';
});
r.innerHTML = html;
}
function renderCmdHistory() {
var r = document.getElementById('cmd-list');
var cnt = document.getElementById('cmd-count');
if (!r) return;
var list = state.cmdHistory || [];
if (cnt) cnt.textContent = list.length;
if (list.length === 0) {
r.innerHTML = '' + __('暂无命令记录','No command history') + '
';
return;
}
var html = '' + __('命令','Command') + ' ' + __('状态','Status') + ' ' + __('时间','Time') + ' ';
list.slice().reverse().slice(0, 50).forEach(function(c) {
html += ''
+ '' + escHtml(c.command || '') + ' '
+ '' + escHtml(c.status || '') + ' '
+ '' + escHtml((c.time || '').substring(0, 19)) + ' '
+ ' ';
});
html += '
';
r.innerHTML = html;
}
// ===== Plugins =====
function renderPlugins() {
var k = state.kernel;
var plugins = k?.plugins || [];
var tools = k?.tools || [];
var installed = state.installedPlugins || [];
var html = '';
var installedNames = (state.installedPlugins || []).map(function(p) { return p.name });
html += '' + __('已加载插件','Loaded Plugins') + ' (' + plugins.length + ') ';
if (plugins.length === 0) {
html += '
' + __('暂无已加载插件','No loaded plugins') + '
';
} else {
html += '
' + __('名称','Name') + ' ' + __('状态','Status') + ' ' + __('操作','Actions') + ' ';
plugins.forEach(function(p) {
var isExternal = installedNames.indexOf(p.name) >= 0;
html += '' + escHtml(p.name) + ' '
+ '' + __('已加载','Loaded') + ' '
+ '' + (isExternal ? '' + __('卸载','Unload') + ' ' : '' + __('内置','Built-in') + ' ') + ' ';
});
html += '
';
}
html += '
';
if (installed.length > 0) {
html += '' + __('已安装外部插件','Installed Plugins') + ' (' + installed.length + ') '
+ '
' + __('名称','Name') + ' ' + __('版本','Version') + ' ' + __('描述','Description') + ' ' + __('操作','Actions') + ' ';
installed.forEach(function(p) {
html += '' + escHtml(p.name) + ' '
+ '' + escHtml(p.version || '-') + ' '
+ '' + escHtml((p.description || '').substring(0, 50)) + ' '
+ '' + __('详情','Details') + ' '
+ '' + __('卸载','Unload') + ' ';
});
html += '
';
}
if (state.pluginInfo) {
html += '' + __('插件详情','Plugin Details') + ': ' + escHtml(state.pluginInfo.name) + ' '
+ '
' + escHtml(JSON.stringify(state.pluginInfo, null, 2)) + ' '
+ '
' + __('关闭','Close') + ' ';
}
if (tools.length > 0) {
html += '' + __('已注册工具','Registered Tools') + ' (' + tools.length + ') '
+ '
';
tools.forEach(function(t) {
html += '' + escHtml(t.name) + ' ';
});
html += '
';
}
html += '
' + __('系统操作','System Operations') + ' '
+ '' + __('重载插件','Reload Plugins') + ' '
+ '' + __('健康检查','Health Check') + ' ';
html += '' + __('健康检查','Health Check') + ' ';
if (state.healthResult) {
html += renderHealthResult(state.healthResult);
} else {
html += '
' + __('点击上方按钮运行','Click the button above to run') + '
';
}
html += '
';
document.getElementById('tab-plugins').innerHTML = html;
}
async function loadInstalledPlugins() {
try { state.installedPlugins = await api('/plugins') } catch(e) { state.installedPlugins = [] }
}
async function installPlugin() {
var inp = document.getElementById('plugin-url');
var url = inp?.value.trim();
if (!url) { toast(__('请输入插件包 URL','Please enter plugin URL'), true); return }
try {
var r = await api('/plugins', { method: 'POST', body: JSON.stringify({ url: url }) });
toast(__('安装结果: ','Install result: ') + (r.status || JSON.stringify(r)));
if (r.action === 'reload_required') toast(__('已安装,请点击「重载插件」加载','Installed, click "Reload Plugins" to load'), false);
loadInstalledPlugins(); renderPlugins();
} catch(e) { toast(__('安装失败: ','Install failed: ') + e.message, true) }
}
async function installPluginFile(file) {
if (!file) return;
try {
var r = await fetch('/api/v1/plugins', { method: 'POST', body: file, headers: { 'Content-Type': 'application/octet-stream' } });
var data = await r.json();
toast(__('上传安装: ','Upload install: ') + (data.status || JSON.stringify(data)));
if (data.action === 'reload_required') toast(__('已安装,请点击「重载插件」加载','Installed, click "Reload Plugins" to load'), false);
loadInstalledPlugins(); renderPlugins();
} catch(e) { toast(__('上传失败: ','Upload failed: ') + e.message, true) }
}
async function showPluginInfo(name) {
try { state.pluginInfo = await api('/plugins/' + encodeURIComponent(name)); renderPlugins() } catch(e) { toast(__('获取详情失败: ','Get details failed: ') + e.message, true) }
}
function closePluginInfo() { state.pluginInfo = null; renderPlugins() }
async function removePlugin(name) {
if (!confirm(__('确定卸载插件','Are you sure to unload plugin') + '「' + name + '」?')) return;
try {
var r = await api('/plugins/' + encodeURIComponent(name), { method: 'DELETE' });
toast(__('已卸载: ','Unloaded: ') + (r.status || r.name));
if (r.action === 'reload_required') toast(__('已卸载,请点击「重载插件」生效','Unloaded, click "Reload Plugins" to apply'), false);
loadInstalledPlugins(); renderPlugins();
} catch(e) { toast(__('卸载失败: ','Unload failed: ') + e.message, true) }
}
async function reloadPlugins() {
try {
var r = await api('/plugins/reload', { method: 'POST' });
toast(__('插件已重载','Plugins reloaded'));
state.kernel = await api('/kernel');
renderPlugins();
} catch(e) { toast(__('重载失败: ','Reload failed: ') + e.message, true) }
}
async function runHealthcheck() {
var panel = document.getElementById('health-panel');
if (!panel) return;
panel.innerHTML = '
' + __('运行中...','Running...') + '
';
try {
var r = await api('/kernel');
var tools = r?.tools || [];
var healthTool = tools.find(function(t) { return t.name === 'healthcheck' });
if (!healthTool) { panel.innerHTML = '' + __('healthcheck 工具未注册','healthcheck tool not registered') + '
'; return }
panel.innerHTML = '' + __('通过 Agent 对话触发 healthcheck...','Triggering healthcheck via Agent...') + '
';
var chatR = await api('/chat', { method: 'POST', body: JSON.stringify({ message: __('请运行 healthcheck 工具进行全面健康检查并报告结果','Please run the healthcheck tool for a full system check and report the results') }) });
panel.innerHTML = '' + escHtml(JSON.stringify(chatR, null, 2)) + ' ';
} catch(e) { panel.innerHTML = '' + __('错误: ','Error: ') + escHtml(e.message) + '
'; toast(__('健康检查失败: ','Health check failed: ') + e.message, true) }
}
function renderHealthResult(r) {
if (!r || !r.checks) return '' + __('暂无健康检查数据','No health check data') + '
';
var checks = r.checks || [];
var passed = checks.filter(function(c) { return c.pass }).length;
var failed = checks.filter(function(c) { return !c.pass }).length;
var html = ''
+ '' + __('通过: ','Pass: ') + passed + ' '
+ '' + __('失败: ','Fail: ') + failed + ' '
+ '' + __('总计: ','Total: ') + checks.length + '
';
checks.forEach(function(c) {
var passClass = c.pass ? 'check-pass' : 'check-fail';
if (c.status === 'skip') passClass = 'check-skip';
html += ''
+ '' + escHtml(c.name) + ' '
+ '' + (c.status || 'unknown') + ' '
+ '' + escHtml(c.detail || '') + '
';
});
return html;
}
// ===== Kernel =====
function renderKernel() {
var k = state.kernel;
if (!k) { document.getElementById('tab-kernel').innerHTML = '' + __('内核未响应','Kernel not responding') + '
'; return }
var html = '' + __('运行时','Runtime') + ' '
+ statCard('Goroutines', k?.runtime?.goroutines || '-', '')
+ statCard(__('内存','Memory'), k?.runtime?.memory_mb ? k.runtime.memory_mb + ' MB' : '-', '')
+ statCard('Go ' + __('版本','Version'), k?.runtime?.go_version || '-', '')
+ '
';
html += 'LLM '
+ '
Provider ' + (k.llm?.provider || __('未配置','Not configured')) + '
'
+ '
' + __('可用源','Sources') + ' ' + (k.llm?.sources || 0) + '
'
+ '
' + __('状态','Status') + ' ' + (k.llm?.available ? __('运行中','Running') : __('不可用','Unavailable')) + '
';
html += '' + __('记忆','Memory') + ' '
+ '
' + __('图记忆','Graph Memory') + ' ' + (k.memory?.available ? k.memory.entity_count + __(' 实体, ',' entities, ') + k.memory.relation_count + __(' 关系',' relations') : __('未初始化','Uninitialized')) + '
'
+ '
' + __('文档记忆','Document Memory') + ' ' + (k.documents?.available ? k.documents.doc_count + __(' 文档',' docs') : __('未初始化','Uninitialized')) + '
'
+ '
' + __('文本记忆','Text Memory') + ' ' + (k.text_memory?.available ? k.text_memory.file_count + __(' 文件',' files') : __('未初始化','Uninitialized')) + '
'
+ '
' + __('知识库','Knowledge') + ' ' + (k.knowledge?.available ? k.knowledge.item_count + __(' 项',' items') : __('未初始化','Uninitialized')) + '
';
html += '' + __('插件','Plugins') + ' (' + (k.plugins?.length || 0) + ') ';
if (k.plugins?.length) {
html += '
';
k.plugins.forEach(function(p) { html += '' + escHtml(p.name) + ' ' });
html += '
';
} else {
html += '
' + __('无','None') + '
';
}
html += '
';
document.getElementById('tab-kernel').innerHTML = html;
}
// ===== Star Map =====
var starmapScene = null, starmapCam = null, starmapRen = null, starmapCtrl = null;
var starmapNodes = [], starmapEdges = [];
var starmapNodeMeshes = [], starmapEdgeLines = [], starmapStarField = null;
var starmapHovered = null, starmapSelected = null, starmapAutoView = true;
var starmapRaf = null;
var smTypeColors = { person: 0x4488ff, task: 0xff8844, ai: 0xaa44ff, concept: 0x44ff88, object: 0xff4444 };
var smEdgeColors = { '喜欢': 0xff6b6b, '学习': 0x4ecdc4, '属于': 0x45b7d1, '相关': 0x96ceb4, '使用': 0xfeca57, '创建': 0xff9ff3 };
function createStarField() {
var c = 3000;
var p = new Float32Array(c * 3), cl = new Float32Array(c * 3), s = new Float32Array(c);
for (var i = 0; i < c; i++) {
var i3 = i * 3;
var r = 400 + Math.random() * 600, th = Math.random() * Math.PI * 2, ph = Math.acos(2 * Math.random() - 1);
p[i3] = r * Math.sin(ph) * Math.cos(th);
p[i3 + 1] = r * Math.sin(ph) * Math.sin(th);
p[i3 + 2] = r * Math.cos(ph);
if (Math.random() < 0.7) {
cl[i3] = 0.8 + Math.random() * 0.2; cl[i3 + 1] = 0.8 + Math.random() * 0.2; cl[i3 + 2] = 1;
} else {
cl[i3] = 1; cl[i3 + 1] = 0.9 + Math.random() * 0.1; cl[i3 + 2] = 0.8 + Math.random() * 0.2;
}
s[i] = 0.5 + Math.random() * 2;
}
var g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.BufferAttribute(p, 3));
g.setAttribute('color', new THREE.BufferAttribute(cl, 3));
g.setAttribute('size', new THREE.BufferAttribute(s, 1));
var m = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true });
starmapStarField = new THREE.Points(g, m);
starmapScene.add(starmapStarField);
}
function onStarmapMove(e) {
if (!starmapRen || !starmapCam) return;
var rect = starmapRen.domElement.getBoundingClientRect();
var mouse = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
var rc = new THREE.Raycaster();
rc.setFromCamera(mouse, starmapCam);
var hits = rc.intersectObjects(starmapNodeMeshes);
var infoEl = document.getElementById('starmap-info');
if (hits.length > 0) {
var n = hits[0].object;
if (starmapHovered !== n) {
if (starmapHovered) starmapHovered.scale.set(1, 1, 1);
starmapHovered = n;
n.scale.set(1.2, 1.2, 1.2);
var nd = n.userData.nodeData;
if (infoEl) {
var e1 = document.getElementById('sm-info-name'); if (e1) e1.textContent = nd.name || '';
var e2 = document.getElementById('sm-info-type'); if (e2) e2.textContent = nd.type || '';
var e3 = document.getElementById('sm-info-mentions'); if (e3) e3.textContent = (nd.mention_count || 0) + '';
var lk = starmapEdges.filter(function(e) { return e.source_id === nd.id || e.target_id === nd.id }).length;
var e4 = document.getElementById('sm-info-links'); if (e4) e4.textContent = lk + '';
infoEl.style.display = 'block';
}
}
} else {
if (starmapHovered) { starmapHovered.scale.set(1, 1, 1); starmapHovered = null }
if (!starmapSelected && infoEl) infoEl.style.display = 'none';
}
}
function onStarmapClick(e) {
if (!starmapRen || !starmapCam) return;
var rect = starmapRen.domElement.getBoundingClientRect();
var mouse = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
var rc = new THREE.Raycaster();
rc.setFromCamera(mouse, starmapCam);
var hits = rc.intersectObjects(starmapNodeMeshes);
if (hits.length > 0) {
var n = hits[0].object;
starmapSelected = (starmapSelected === n) ? null : n;
if (starmapAutoView && starmapSelected) flyStarmapTo(starmapSelected.userData.nodeId, 500);
onStarmapMove(e);
} else {
starmapSelected = null;
}
}
function flyStarmapTo(nodeId, dur) {
if (!starmapAutoView) return;
var m = starmapNodeMeshes.find(function(x) { return x.userData.nodeId === nodeId });
if (!m) return;
var tp = m.position.clone(), sp = starmapCam.position.clone(), st = starmapCtrl.target.clone();
var dist = tp.length() + 25, ep = new THREE.Vector3(tp.x, tp.y + dist * 0.4, tp.z + dist * 0.8);
var t0 = Date.now();
(function lerp() {
var t = Math.min((Date.now() - t0) / dur, 1), e = 1 - Math.pow(1 - t, 3);
starmapCam.position.lerpVectors(sp, ep, e);
starmapCtrl.target.lerpVectors(st, tp, e);
if (t < 1) requestAnimationFrame(lerp);
})();
}
function onStarmapResize() {
if (!starmapRen || !starmapCam) return;
var cont = starmapRen.domElement.parentElement;
if (!cont) return;
var rect = cont.getBoundingClientRect();
var w = rect.width || 800, h = Math.max(rect.height || 250, 100);
if (w > 0 && h > 0) { starmapCam.aspect = w / h; starmapCam.updateProjectionMatrix(); starmapRen.setSize(w, h) }
}
function toggleStarmapAuto() {
starmapAutoView = !starmapAutoView;
var b = document.getElementById('sm-auto-btn');
if (b) b.className = starmapAutoView ? 'on' : '';
}
function resetStarmapCamera() {
if (!starmapCam || !starmapCtrl || !starmapNodeMeshes) return;
var maxD = 0;
starmapNodeMeshes.forEach(function(m) { var d = m.position.length(); if (d > maxD) maxD = d });
if (maxD < 1) maxD = 30;
var td = Math.min(Math.max(maxD + 20, 30), 150);
var sp = starmapCam.position.clone(), ep = new THREE.Vector3(td * 0.9, td * 0.6, td * 0.9);
var st = starmapCtrl.target.clone(), t0 = Date.now();
(function lerp() {
var t = Math.min((Date.now() - t0) / 400, 1), e = 1 - Math.pow(1 - t, 3);
starmapCam.position.lerpVectors(sp, ep, e);
starmapCtrl.target.lerpVectors(st, new THREE.Vector3(0, 0, 0), e);
if (t < 1) requestAnimationFrame(lerp);
})();
}
function starmapAnimate() {
starmapRaf = requestAnimationFrame(starmapAnimate);
if (starmapCtrl) starmapCtrl.update();
if (starmapStarField) starmapStarField.rotation.y += 0.0001;
if (starmapRen && starmapScene && starmapCam) starmapRen.render(starmapScene, starmapCam);
}
function createNebula() {
var nc = 500;
var p = new Float32Array(nc * 3), cl = new Float32Array(nc * 3);
for (var i = 0; i < nc; i++) {
var i3 = i * 3;
p[i3] = (Math.random() - 0.5) * 800;
p[i3+1] = (Math.random() - 0.5) * 800;
p[i3+2] = (Math.random() - 0.5) * 800;
var ch = Math.random();
if (ch < 0.33) {
cl[i3]=0.5+Math.random()*0.3; cl[i3+1]=0.2+Math.random()*0.2; cl[i3+2]=0.7+Math.random()*0.3;
} else if (ch < 0.66) {
cl[i3]=0.2+Math.random()*0.2; cl[i3+1]=0.3+Math.random()*0.3; cl[i3+2]=0.8+Math.random()*0.2;
} else {
cl[i3]=0.7+Math.random()*0.3; cl[i3+1]=0.2+Math.random()*0.2; cl[i3+2]=0.5+Math.random()*0.3;
}
}
var g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.BufferAttribute(p, 3));
g.setAttribute('color', new THREE.BufferAttribute(cl, 3));
var m = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending });
var np = new THREE.Points(g, m);
starmapScene.add(np);
}
// ===== Settings =====
function pluginDisplayName(p) {
if (p === 'core') return __('核心', 'Core');
var name = p.replace('plugin.', '');
var meta = state.pluginMeta && state.pluginMeta[name];
if (meta) return state.lang === 'en' ? (meta.name_en || name) : (meta.name_zh || name);
return name;
}
function renderSettingsSidebar() {
var el = document.querySelector('.settings-sidebar');
if (!el) return;
el.innerHTML = '';
state.settingsPlugins.forEach(function(p) {
var a = document.createElement('a');
a.textContent = pluginDisplayName(p);
if (p === state.selectedSection) a.className = 'active';
a.onclick = function() { state.selectedSection = p; renderOneSettings() };
el.appendChild(a);
});
}
function renderOneSettings() {
var prefix = state.selectedSection + '.';
var allKeys = Object.keys(state.settings || {});
var filtered = allKeys.filter(function(k) { return k === prefix.slice(0, -1) || k.startsWith(prefix) });
filtered.sort();
var hideTopLlms = ['core.llm.base_url','core.llm.model','core.llm.api_key','core.llm.adapter','core.llm.adapter_path','core.llm.thinking_enabled'];
var sourceKeys = filtered.filter(function(k) { return k.startsWith('core.llm.sources.') });
var sourceMap = {};
sourceKeys.forEach(function(k) {
var parts = k.split('.');
var srcName = parts[3];
if (!sourceMap[srcName]) sourceMap[srcName] = {};
sourceMap[srcName][k] = true;
});
var mcpServerKeys = filtered.filter(function(k) { return k.startsWith('plugin.mcp.servers.') && k.split('.').length >= 5 });
var mcpServerMap = {};
mcpServerKeys.forEach(function(k) {
var parts = k.split('.');
var srvName = parts[3];
if (!mcpServerMap[srvName]) mcpServerMap[srvName] = {};
mcpServerMap[srvName][k] = true;
});
var regularKeys = filtered.filter(function(k) {
return !k.startsWith('core.llm.sources.') && hideTopLlms.indexOf(k) === -1 && !k.startsWith('plugin.mcp.servers.') && k !== 'plugin.mcp.servers';
});
var html = '';
if (regularKeys.length === 0 && Object.keys(sourceMap).length === 0 && Object.keys(mcpServerMap).length === 0 && state.selectedSection !== 'plugin.mcp') {
html += '
' + escHtml(state.selectedSection) + ' ' + __('暂无设置项','No settings') + '
';
} else {
regularKeys.forEach(function(k) {
var v = state.settings[k];
var sv = typeof v === 'object' ? JSON.stringify(v) : String(v);
var m = state.meta?.[k];
var shortName = k.split('.').pop().replace(/_/g, ' ');
var label = m?.display_name || shortName;
var desc = m?.description || '';
var typ = m?.type || 'string';
var ph = m?.placeholder || '';
var opts = m?.options || [];
var inpId = 'inp-' + k.replace(/\./g, '_');
var inp = '';
if (typ === 'bool') {
var chk = sv === 'true' ? 'checked' : '';
inp = '
' + label + '';
} else if (typ === 'select') {
var selOpts = '';
opts.forEach(function(o) { selOpts += '
' + o + ' ' });
inp = '
' + label + ' ' + selOpts + ' ';
} else if (typ === 'text') {
inp = '
' + label + ' ';
} else {
inp = '
' + label + ' ';
}
var extra = '';
if (m?.extra) {
m.extra.forEach(function(f) {
var fk = (k ? k + '.' : '') + f.key;
var fv = state.settings?.[fk];
var fph = f.placeholder || __('输入','Enter ') + f.label;
extra += '
' + f.label + ' ';
if (f.type === 'select') {
var fopts = '';
if (f.options) f.options.forEach(function(o) { fopts += '' + o + ' ' });
extra += '' + fopts + ' ';
} else {
extra += ' ';
}
extra += '
';
});
}
var descHtml = desc ? '
' + escHtml(desc) + '
' : '';
html += '
' + escHtml(k) + '
' + inp + descHtml + extra
+ '
' + __('保存','Save') + ' ';
});
// LLM Sources
Object.keys(sourceMap).sort().forEach(function(src) {
var baseKey = 'core.llm.sources.' + src;
var srcData = state.settings?.[baseKey + '.adapter'] || state.settings?.[baseKey + '.base_url'] || '';
var fields = [
{ key: 'adapter', label: __('适配器','Adapter'), type: 'text' },
{ key: 'base_url', label: 'Base URL', type: 'text' },
{ key: 'model', label: __('模型','Model'), type: 'text' },
{ key: 'api_key', label: 'API Key', type: 'text' },
{ key: 'thinking_enabled', label: __('思考模式','Thinking Mode'), type: 'select', options: ['true','false'] },
{ key: 'adapter_path', label: __('适配器路径','Adapter Path'), type: 'text' }
];
var headerLabel = mL10n(src, 'LLM Source: ' + src);
html += '
' + escHtml(headerLabel) + ' ';
fields.forEach(function(f) {
var fk = baseKey + '.' + f.key;
var fv = state.settings?.[fk] || '';
var flabel = f.label;
var fieldId = 'inp-' + fk.replace(/\./g, '_');
if (f.type === 'select') {
var fopts = '';
f.options.forEach(function(o) { fopts += '
' + o + ' ' });
html += '
' + flabel + ' ' + fopts + ' ';
} else {
html += '
' + flabel + ' ';
}
});
html += '
'
+ '' + __('保存','Save') + ' '
+ '' + __('删除','Delete') + '
';
});
if (Object.keys(sourceMap).length > 0 || state.selectedSection === 'core.llm') {
html += '
+ ' + __('添加 LLM 源','Add LLM Source') + ' ';
}
// MCP Servers
if (state.selectedSection === 'plugin.mcp' || Object.keys(mcpServerMap).length > 0) {
html += '
' + __('MCP 服务器','MCP Servers') + ' ' + __('配置 Model Context Protocol 服务端连接','Configure Model Context Protocol server connections') + '
';
Object.keys(mcpServerMap).sort().forEach(function(srv) {
var baseKey = 'plugin.mcp.servers.' + srv;
var fields = [
{ key: 'command', label: __('启动命令','Command'), type: 'text' },
{ key: 'url', label: 'SSE URL', type: 'text' },
{ key: 'args', label: __('参数(JSON数组)','Args (JSON array)'), type: 'text' },
{ key: 'env', label: __('环境变量(JSON数组)','Env (JSON array)'), type: 'text' }
];
html += '
' + escHtml(srv) + ' ';
fields.forEach(function(f) {
var fk = baseKey + '.' + f.key;
var fv = state.settings?.[fk] || '';
var fieldId = 'inp-' + fk.replace(/\./g, '_');
html += '
' + f.label + ' ';
});
html += '
'
+ '' + __('保存','Save') + ' '
+ '' + __('删除','Delete') + '
';
});
html += '
+ ' + __('添加 MCP 服务器','Add MCP Server') + ' ';
}
}
html += '
';
document.getElementById('tab-settings').innerHTML = html;
renderSettingsSidebar();
}
function markDirty(k) {
var inp = document.getElementById('inp-' + k.replace(/\./g, '_'));
if (inp) inp.style.borderColor = 'var(--save-btn-border)';
}
async function saveSetting(k) {
var inp = document.getElementById('inp-' + k.replace(/\./g, '_'));
if (!inp) return;
var val;
var m = state.meta?.[k];
if (m?.type === 'bool') { val = inp.checked ? 'true' : 'false' }
else if (m?.type === 'select') { val = inp.value }
else { var raw = inp.value; try { val = JSON.parse(raw) } catch(e) { val = raw } }
try {
var r = await api('/settings', { method: 'PUT', body: JSON.stringify({ key: k, value: val }) });
if (r.status === 'ok') {
inp.style.borderColor = '';
state.settings[k] = val;
toast(__('已保存: ','Saved: ') + k);
} else {
toast(__('保存失败: ','Save failed: ') + (r.error || 'unknown'), true);
}
} catch(e) { toast(__('保存失败: ','Save failed: ') + e.message, true) }
}
function renderConfigDisabled() {
document.getElementById('tab-settings').innerHTML = '' + __('设置','Settings') + ' ' + __('设置面板已加载','Settings panel loaded') + '
';
renderOneSettings();
}
function mL10n(key, fallback) {
var meta = state.meta?.[key];
if (meta?.display_name) return meta.display_name;
return fallback || key;
}
function showAddSourceDialog() {
var name = prompt(__('输入新 LLM 源名称(如 openai、anthropic):','Enter new LLM source name (e.g. openai, anthropic):'));
if (!name || !name.trim()) return;
name = name.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_');
if (!name) { toast(__('名称无效','Invalid name'), true); return }
var keys = ['base_url','model','api_key','adapter','adapter_path','thinking_enabled'];
var values = { base_url: 'https://api.' + name + '.com', model: '', api_key: '', adapter: name, adapter_path: '', thinking_enabled: 'false' };
var promises = keys.map(function(f) {
return api('/settings', { method: 'PUT', body: JSON.stringify({ key: 'core.llm.sources.' + name + '.' + f, value: values[f] }) });
});
Promise.all(promises).then(function() { toast(__('源','Source') + ' "' + name + '" ' + __('已创建,请配置各项参数','created, please configure parameters')); renderAll() }).catch(function(e) { toast(__('创建失败: ','Create failed: ') + e.message, true) });
}
async function deleteSource(name) {
if (!confirm(__('确认删除源','Are you sure to delete source') + ' "' + name + '"?')) return;
var base = 'core.llm.sources.' + name;
var fields = ['adapter','base_url','model','api_key','thinking_enabled','adapter_path'];
try {
for (var f of fields) { await api('/settings', { method: 'PUT', body: JSON.stringify({ key: base + '.' + f, value: null }) }) }
toast(__('源','Source') + ' "' + name + '" ' + __('已删除','deleted')); renderAll();
} catch(e) { toast(__('删除失败: ','Delete failed: ') + e.message, true) }
}
function addMCPSource() {
var name = prompt(__('输入新 MCP 服务器名称:','Enter new MCP server name:'));
if (!name || !name.trim()) return;
name = name.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_');
if (!name) { toast(__('名称无效','Invalid name'), true); return }
var fields = ['command','url','args','env'];
var values = { command: '', url: '', args: '[]', env: '[]' };
var promises = fields.map(function(f) {
return api('/settings', { method: 'PUT', body: JSON.stringify({ key: 'plugin.mcp.servers.' + name + '.' + f, value: values[f] }) });
});
Promise.all(promises).then(function() { toast('MCP ' + __('服务器','server') + ' "' + name + '" ' + __('已创建','created')); renderAll() }).catch(function(e) { toast(__('创建失败: ','Create failed: ') + e.message, true) });
}
async function deleteMCPServer(name) {
if (!confirm(__('确认删除 MCP 服务器','Are you sure to delete MCP server') + ' "' + name + '"?')) return;
var fields = ['command','url','args','env'];
try {
for (var f of fields) { await api('/settings', { method: 'PUT', body: JSON.stringify({ key: 'plugin.mcp.servers.' + name + '.' + f, value: null }) }) }
toast('MCP ' + __('服务器','server') + ' "' + name + '" ' + __('已删除','deleted')); renderAll();
} catch(e) { toast(__('删除失败: ','Delete failed: ') + e.message, true) }
}
// ===== Adapters =====
async function renderAdapters() {
var html = '' + __('Lua 适配器管理','Lua Adapter Management') + ' '
+ '
' + __('上传自定义 Lua 适配器脚本以支持新的 LLM 提供商。脚本文件将保存到适配器目录并自动加载到 Lua VM。','Upload custom Lua adapter scripts to support new LLM providers. Scripts are saved to the adapter directory and auto-loaded into the Lua VM.') + '
';
try {
var r = await api('/adapters');
var adapters = r.adapters || [];
window._adapters = adapters;
html += '' + __('已加载的适配器','Loaded Adapters') + ' (' + adapters.length + ') ';
if (adapters.length === 0) {
html += '
' + __('暂无适配器','No adapters') + '
';
} else {
html += '
' + __('名称','Name') + ' ' + __('版本','Version') + ' ' + __('操作','Actions') + ' ';
adapters.forEach(function(a) {
html += '' + escHtml(a.name) + ' ' + escHtml(a.version || '-') + ' '
+ '' + __('删除','Delete') + ' ';
});
html += '
';
}
html += '
';
html += '
' + __('上传新适配器','Upload New Adapter') + ' '
+ '' + __('适配器名称(不带 .lua)','Adapter name (without .lua)') + ' '
+ ' '
+ '' + __('Lua 脚本代码','Lua Script Code') + ' '
+ ''
+ '' + __('上传','Upload') + ' ';
} catch(e) {
html += '' + __('加载适配器失败: ','Failed to load adapters: ') + escHtml(e.message) + '
';
}
document.getElementById('tab-adapters').innerHTML = html;
}
async function uploadAdapter() {
var name = document.getElementById('adapter-name')?.value;
var code = document.getElementById('adapter-code')?.value;
if (!name || !code) { toast(__('名称和代码不能为空','Name and code cannot be empty'), true); return }
try {
var r = await api('/adapters', { method: 'POST', body: JSON.stringify({ name: name, code: code }) });
if (r.status === 'loaded') { toast(__('适配器','Adapter') + ' "' + name + '" ' + __('已加载','loaded')); renderAdapters() }
else { toast(__('上传失败: ','Upload failed: ') + (r.error || 'unknown'), true) }
} catch(e) { toast(__('上传失败: ','Upload failed: ') + e.message, true) }
}
async function deleteAdapter(name) {
if (!confirm(__('确定删除适配器','Are you sure to delete adapter') + ' "' + name + '"?')) return;
try {
var r = await api('/adapters/' + encodeURIComponent(name), { method: 'DELETE' });
if (r.status === 'deleted') { toast(__('适配器','Adapter') + ' "' + name + '" ' + __('已删除','deleted')); renderAdapters() }
else { toast(__('删除失败','Delete failed'), true) }
} catch(e) { toast(__('删除失败: ','Delete failed: ') + e.message, true) }
}
// ===== Init =====
(async function() {
var data = await window.homeagent.connections.list();
state.connections = data.connections || [];
if (data.currentId) state.currentConn = state.connections.find(function(c) { return c.id === data.currentId }) || null;
if (state.currentConn) {
document.getElementById('app').style.display = 'block';
connectSSE();
await loadChatHistory();
doRenderAll();
startUptimeTicker();
setInterval(doRenderAll, 15000);
} else {
document.getElementById('conn-overlay').style.display = 'flex';
}
renderConnList();
})();
// ===== Connection Management =====
function updateConnIndicator() {
var el = document.getElementById('conn-name-display');
var dot = document.getElementById('conn-dot');
if (state.currentConn) {
el.textContent = state.currentConn.name;
dot.className = 'status-dot ' + (state.status.status === 'running' ? 'dot-green pulse' : 'dot-yellow');
} else {
el.textContent = '未连接';
dot.className = 'status-dot dot-gray';
}
}
function openConnManager() { renderConnList(); document.getElementById('conn-overlay').style.display = 'flex'; }
async function selectConnection(id) {
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
var data = await window.homeagent.connections.setCurrent(id);
state.currentConn = data.connections.find(function(c) { return c.id === id }) || null;
state.connections = data.connections;
state.messages = [];
document.getElementById('app').style.display = 'block';
document.getElementById('conn-overlay').style.display = 'none';
updateConnIndicator();
connectSSE();
await loadChatHistory();
doRenderAll();
startUptimeTicker();
}
async function deleteConnection(id, e) {
e.stopPropagation();
if (!confirm('确定删除此连接?')) return;
var wasCurrent = state.currentConn && state.currentConn.id === id;
var data = await window.homeagent.connections.delete(id);
state.connections = data.connections;
state.currentConn = data.currentId ? state.connections.find(function(c) { return c.id === data.currentId }) : null;
if (wasCurrent && state.eventSource) { state.eventSource.close(); state.eventSource = null; }
if (state.currentConn) {
updateConnIndicator(); doRenderAll(); connectSSE();
} else {
document.getElementById('app').style.display = 'none';
document.getElementById('conn-overlay').style.display = 'flex';
}
renderConnList();
}
function renderConnList() {
var list = document.getElementById('conn-list');
if (!list) return;
list.innerHTML = state.connections.map(function(c) {
return ''
+ '
'
+ '
' + escHtml(c.name) + '
' + escHtml(c.url) + '
'
+ '
'
+ '' + __('编辑','Edit') + ' '
+ '' + __('删除','Delete') + '
';
}).join('');
}
var editingConnId = null;
function showConnForm() {
editingConnId = null;
document.getElementById('conn-form-title').textContent = __('添加连接','Add Connection');
document.getElementById('conn-name').value = '';
document.getElementById('conn-url').value = 'http://localhost:8080';
document.getElementById('conn-key').value = '';
document.getElementById('conn-form').style.display = 'block';
document.getElementById('conn-add-btn').style.display = 'none';
}
function editConnection(id, e) {
e.stopPropagation();
var c = state.connections.find(function(x) { return x.id === id });
if (!c) return;
editingConnId = id;
document.getElementById('conn-form-title').textContent = __('编辑连接','Edit Connection');
document.getElementById('conn-name').value = c.name;
document.getElementById('conn-url').value = c.url;
document.getElementById('conn-key').value = c.apiKey;
document.getElementById('conn-form').style.display = 'block';
document.getElementById('conn-add-btn').style.display = 'none';
document.querySelectorAll('.conn-item').forEach(function(el) { el.style.opacity = '0.4' });
}
function cancelConnForm() {
document.getElementById('conn-form').style.display = 'none';
document.getElementById('conn-add-btn').style.display = 'block';
document.querySelectorAll('.conn-item').forEach(function(el) { el.style.opacity = '1' });
}
async function saveConnForm() {
var name = document.getElementById('conn-name').value.trim();
var url = document.getElementById('conn-url').value.trim().replace(/\/+$/, '');
var apiKey = document.getElementById('conn-key').value.trim();
if (!name || !url) { toast(__('名称和地址不能为空','Name and URL required'), true); return; }
var testBtn = document.querySelector('#conn-form .btn-primary');
testBtn.textContent = __('测试中...','Testing...'); testBtn.disabled = true;
try {
var testR = await fetch(url + '/api/v1/status', { headers: apiKey ? { 'X-API-Key': apiKey } : {} });
if (!testR.ok) { toast(__('连接测试失败: HTTP ','Connection test failed: HTTP ') + testR.status, true); testBtn.textContent = __('保存 / Save','Save'); testBtn.disabled = false; return; }
} catch(e) {
toast(__('无法连接到 ','Cannot connect to ') + url + ': ' + e.message, true);
testBtn.textContent = __('保存 / Save','Save'); testBtn.disabled = false; return;
}
testBtn.textContent = __('保存 / Save','Save'); testBtn.disabled = false;
var data;
if (editingConnId) {
data = await window.homeagent.connections.update(editingConnId, { name: name, url: url, apiKey: apiKey });
} else {
data = await window.homeagent.connections.add({ name: name, url: url, apiKey: apiKey });
}
state.connections = data.connections;
var cur = data.connections.find(function(c) { return c.id === data.currentId });
if (cur) {
state.currentConn = cur;
if (!document.getElementById('app').style.display || document.getElementById('app').style.display === 'none') {
document.getElementById('app').style.display = 'block';
document.getElementById('conn-overlay').style.display = 'none';
updateConnIndicator(); connectSSE(); await loadChatHistory(); doRenderAll(); startUptimeTicker();
} else { updateConnIndicator(); if (editingConnId) doRenderAll(); }
}
cancelConnForm(); renderConnList();
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && document.getElementById('conn-form').style.display === 'block') cancelConnForm();
});
// ===== SSE (override for fetch-based) =====
connectSSE = function() {
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
if (!state.currentConn) return;
connectFetchSSE(state.currentConn.url + '/api/v1/chat/events');
};
async function connectFetchSSE(url) {
try {
var headers = {};
if (state.currentConn && state.currentConn.apiKey) headers['X-API-Key'] = state.currentConn.apiKey;
var resp = await fetch(url, { headers: headers, cache: 'no-store' });
if (!resp.ok || !resp.body) { setTimeout(function() { connectSSE() }, 5000); return; }
var reader = resp.body.getReader(); var decoder = new TextDecoder();
var buffer = ''; var reconnectTimer = null;
state.eventSource = { close: function() { reader.cancel(); if (reconnectTimer) clearTimeout(reconnectTimer) } };
function processLines() {
var lines = buffer.split('\n'); buffer = lines.pop() || '';
var eventType = '', data = '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.startsWith('event: ')) eventType = line.slice(7).trim();
else if (line.startsWith('data: ')) data = line.slice(6).trim();
else if (line === '' && eventType && data) { handleSSEEvent(eventType, data); eventType = ''; data = ''; }
}
}
function handleSSEEvent(type, raw) {
try {
var ev = JSON.parse(raw); var p = ev.payload || {};
if (type === 'agent_output') {
state.chatStage = __('AI 回复中...','AI replying...');
if (state.messages.length > 0 && state.messages[state.messages.length - 1].role === 'assistant' && !state.messages[state.messages.length - 1]._final) {
state.messages[state.messages.length - 1].content += (p.content || '');
rerenderChatIfActive(); return;
}
state.messages.push({ role: 'assistant', content: p.content || '', _streaming: true });
rerenderChatIfActive();
} else if (type === 'reasoning') {
if (p.content && state.messages.length > 0) {
var last = state.messages[state.messages.length - 1];
if (last.role === 'assistant') {
state.chatStage = __('AI 思考中...','AI thinking...');
last.reasoning_content = (last.reasoning_content || '') + (p.content || '');
rerenderChatIfActive();
}
}
} else if (type === 'tool_call') {
if (!p.tool) return;
var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null;
if (!last || last.role !== 'assistant') {
state.messages.push({ role: 'assistant', content: '', tool_calls: [], _streaming: true });
last = state.messages[state.messages.length - 1];
}
if (!last.tool_calls) last.tool_calls = [];
last.tool_calls.push({ tool: p.tool, name: p.tool, args: p.args || {}, result: p.result || '', status: p.status || 'ok', plugin: p.plugin || '' });
state.chatStage = __('工具调用: ','Tool: ') + (p.tool || '');
rerenderChatIfActive();
} else if (type === 'stage') {
var phase = p.phase || ''; var tool = p.tool || '';
if (phase === 'pre_action') state.chatStage = __('AI 思考中...','AI thinking...');
else if (phase === 'before_toolcall') state.chatStage = __('工具调用: ','Tool: ') + (tool || '');
else if (phase === 'before_output') state.chatStage = __('生成回复中...','Generating response...');
var badge = document.getElementById('chat-stage');
if (badge) { badge.textContent = state.chatStage || ''; badge.style.display = state.chatLoading ? 'inline' : 'none' }
}
} catch(err) {}
}
async function pump() {
while (true) {
try { var result = await reader.read(); if (result.done) break; buffer += decoder.decode(result.value, { stream: true }); processLines(); } catch(e) { break; }
}
reconnectTimer = setTimeout(function() { connectSSE() }, 3000);
}
pump();
} catch(e) { setTimeout(function() { connectSSE() }, 5000); }
}
function rerenderChatIfActive() {
var tab = document.getElementById('tab-chat');
if (tab && tab.classList.contains('active')) { renderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory(); }
}