diff --git a/cmd/gui/renderer/app.js b/cmd/gui/renderer/app.js
index e48a734..b5dbb50 100644
--- a/cmd/gui/renderer/app.js
+++ b/cmd/gui/renderer/app.js
@@ -1,225 +1,63 @@
// ===== State =====
let state = {
- connections: [], currentConn: null,
- status: {}, kernel: null,
- settings: {}, meta: {}, pluginMeta: {}, settingsPlugins: ['core'],
+ status: {},
+ kernel: null,
+ settings: {},
+ meta: {},
+ pluginMeta: {},
+ settingsPlugins: ['core'],
selectedSection: 'core',
- messages: [], chatLoading: false, chatStage: '',
- installedPlugins: [], pluginInfo: null,
- startedAt: null, uptimeTick: null, sidebarRefreshTick: null, eventSource: null,
- _chatHistoryLoaded: false, _starmapData: null,
+ 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,
+
};
-// ===== Markdown Renderer (lightweight, no dependencies) =====
-function renderMarkdown(t) {
- if (!t) return '';
- let s = String(t)
- .replace(/&/g,'&').replace(//g,'>');
- // code blocks (fenced)
- s = s.replace(/```(\w*)\n([\s\S]*?)```/g, '
$2 ');
- // inline code
- s = s.replace(/`([^`]+)`/g, '$1');
- // headers
- s = s.replace(/^### (.+)$/gm, '$1 ');
- s = s.replace(/^## (.+)$/gm, '$1 ');
- s = s.replace(/^# (.+)$/gm, '$1 ');
- // bold & italic
- s = s.replace(/\*\*\*(.+?)\*\*\*/g, '$1 ');
- s = s.replace(/\*\*(.+?)\*\*/g, '$1 ');
- s = s.replace(/\*(.+?)\*/g, '$1 ');
- // links
- s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ');
- // images
- s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, ' ');
- // blockquote
- s = s.replace(/^> (.+)$/gm, '$1 ');
- // horizontal rule
- s = s.replace(/^---$/gm, ' ');
- // unordered list
- s = s.replace(/^[\s]*[-*] (.+)$/gm, '$1 ');
- s = s.replace(/(.*<\/li>\n?)+/g, '');
- // ordered list
- s = s.replace(/^[\s]*\d+\. (.+)$/gm, ' $1 ');
- // paragraphs: double newlines
- s = s.replace(/\n\n/g, '');
- s = '
' + s + '
';
- // clean nested ps from lists
- s = s.replace(/<\/p>\n?/g, '').replace(/<\/ul>\n?/g, '
');
- s = s.replace(/<\/p>\n?/g, ' ').replace(/<\/li>\n?/g, '
');
- s = s.replace(/<\/p>/g, '');
- return s;
+// ===== 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();
}
-// ===== Connection Management =====
-async function initApp() {
- const data = await window.homeagent.connections.list();
- state.connections = data.connections || [];
- if (data.currentId) state.currentConn = state.connections.find(c => c.id === data.currentId) || null;
- if (state.currentConn) {
- document.getElementById('app').style.display = 'block';
- document.getElementById('conn-overlay').style.display = 'none';
- updateConnIndicator();
- await renderAll();
- startUptimeTicker(); startSidebarRefresh(); connectSSE();
- } else {
- document.getElementById('conn-overlay').style.display = 'flex';
- }
- renderConnList();
-}
-
-function updateConnIndicator() {
- const el = document.getElementById('conn-name-display');
- const dot = document.getElementById('conn-dot');
- if (state.currentConn) {
- el.textContent = state.currentConn.name;
- dot.className = 'status-dot ' + (state.status.status === 'running' ? 'dot-green' : '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) {
- disconnectSSE();
- const data = await window.homeagent.connections.setCurrent(id);
- state.currentConn = data.connections.find(c => c.id === id) || null;
- state.connections = data.connections;
- state.messages = []; state._chatHistoryLoaded = false;
- document.getElementById('app').style.display = 'block';
- document.getElementById('conn-overlay').style.display = 'none';
- updateConnIndicator();
- await renderAll();
- startUptimeTicker(); startSidebarRefresh(); connectSSE();
-}
-
-async function deleteConnection(id, e) {
- e.stopPropagation();
- if (!confirm('确定删除此连接?')) return;
- const wasCurrent = state.currentConn && state.currentConn.id === id;
- const data = await window.homeagent.connections.delete(id);
- state.connections = data.connections;
- state.currentConn = data.currentId ? state.connections.find(c => c.id === data.currentId) : null;
- if (wasCurrent) { disconnectSSE(); if (state.sidebarRefreshTick) { clearInterval(state.sidebarRefreshTick); state.sidebarRefreshTick = null; } }
- if (state.currentConn) {
- updateConnIndicator(); await renderAll(); startSidebarRefresh(); connectSSE();
- } else {
- document.getElementById('app').style.display = 'none'; document.getElementById('conn-overlay').style.display = 'flex';
- }
- renderConnList();
-}
-
-function renderConnList() {
- document.getElementById('conn-list').innerHTML = state.connections.map(c =>
- '
'
- + '
'
- + '
' + escHtml(c.name) + '
' + escHtml(c.url) + '
'
- + '
'
- + '编辑 '
- + '删除
'
- ).join('');
-}
-
-let editingConnId = null;
-
-function showConnForm() {
- editingConnId = null;
- document.getElementById('conn-form-title').textContent = '添加连接 / Add Connection';
- document.getElementById('conn-name').value = 'My HomeAgent';
- 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();
- const c = state.connections.find(x => 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(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(el => el.style.opacity = '1');
-}
-
-async function saveConnForm() {
- const name = document.getElementById('conn-name').value.trim();
- const url = document.getElementById('conn-url').value.trim().replace(/\/+$/, '');
- const apiKey = document.getElementById('conn-key').value.trim();
- if (!name || !url) { toast('名称和地址不能为空', true); return; }
- // test connection before saving
- const testBtn = document.querySelector('#conn-form .btn-primary');
- testBtn.textContent = '测试中...'; testBtn.disabled = true;
- try {
- const testR = await fetch(url + '/api/v1/status', {
- headers: apiKey ? { 'X-API-Key': apiKey } : {}
- });
- if (!testR.ok) { toast('连接测试失败: HTTP ' + testR.status, true); testBtn.textContent = '保存 / Save'; testBtn.disabled = false; return; }
- } catch(e) {
- toast('无法连接到 ' + url + ': ' + e.message, true);
- testBtn.textContent = '保存 / Save'; testBtn.disabled = false; return;
- }
- testBtn.textContent = '保存 / Save'; testBtn.disabled = false;
- let data;
- if (editingConnId) {
- data = await window.homeagent.connections.update(editingConnId, { name, url, apiKey });
- } else {
- data = await window.homeagent.connections.add({ name, url, apiKey });
- }
- state.connections = data.connections;
- const curId = data.currentId;
- const cur = data.connections.find(c => c.id === curId);
- 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(); await renderAll(); startUptimeTicker(); startSidebarRefresh(); connectSSE();
- } else { updateConnIndicator(); if (editingConnId) await renderAll(); }
- }
- cancelConnForm(); renderConnList();
-}
-
-document.addEventListener('keydown', function(e) {
- if (e.key === 'Escape' && document.getElementById('conn-form').style.display === 'block') cancelConnForm();
-});
-
-// ===== API Client =====
-async function api(path, opts = {}) {
- if (!state.currentConn) throw new Error('No connection selected');
- const headers = { 'Content-Type': 'application/json', ...opts.headers };
- if (state.currentConn.apiKey) headers['X-API-Key'] = state.currentConn.apiKey;
- const url = state.currentConn.url + '/api/v1' + path;
- const res = await fetch(url, { ...opts, headers });
- if (res.status === 401) throw new Error('unauthorized');
- if (opts.raw) return res;
- const ct = res.headers.get('content-type') || '';
- if (ct.includes('json')) return res.json();
- return res.text();
-}
-
-// ===== Toast =====
-function toast(msg, isError) {
- const t = document.getElementById('toast');
- t.textContent = msg; t.className = 'toast' + (isError ? ' error' : ''); t.style.display = 'block';
- setTimeout(function() { t.style.display = 'none' }, 3000);
-}
-
-// ===== Utility =====
-function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
-function fmtUptime(ms) {
- const s = Math.floor(ms / 1000);
- if (s < 60) return s + 's'; const m = Math.floor(s / 60); s = s % 60;
- if (m < 60) return m + 'm ' + s + 's'; const h = Math.floor(m / 60); m = m % 60;
- return h + 'h ' + m + 'm ' + s + 's';
+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 =====
@@ -228,61 +66,1563 @@ function setTheme(name) {
localStorage.setItem('ha-theme', name);
document.getElementById('theme-btn').textContent = name === 'light' ? '☀️' : '🌙';
}
-function toggleTheme() { setTheme(document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light'); }
-(function() { setTheme(localStorage.getItem('ha-theme') || 'dark') })();
+
+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') });
- const el = document.getElementById('tab-' + n); if (el) el.classList.add('active');
+ var el = document.getElementById('tab-' + n);
+ if (el) el.classList.add('active');
document.querySelectorAll('nav a').forEach(function(e) { e.classList.remove('active') });
- const m = document.querySelector('nav a[onclick*="' + n + '"]'); if (m) m.classList.add('active');
+ var match = document.querySelector('nav a[onclick*="' + n + '"]');
+ if (match) match.classList.add('active');
renderAll();
}
// ===== Tab Render Dispatch =====
-async function renderAll() {
- if (!state.currentConn) return;
- try { const s = await api('/status'); state.status = s; state.startedAt = s.startedAt ? new Date(s.startedAt).getTime() : null; updateConnIndicator() } catch(e) {}
+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 {
- const s = await api('/settings'); state.settings = s.settings || {}; state.meta = s.meta || {};
- state.settingsPlugins = s.plugins || ['core']; state.pluginMeta = s.plugin_meta || {};
+ 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 { renderOverview() } catch(e) {} try { renderChat() } catch(e) {} try { renderPlugins() } catch(e) {}
- try { renderKernel() } catch(e) {} try { renderOneSettings() } catch(e) {} try { renderAdapters() } 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 (state.uptimeTick) clearInterval(state.uptimeTick);
- state.uptimeTick = setInterval(function() {
- const el = document.querySelector('#uptime-val');
- if (el && state.startedAt) el.textContent = fmtUptime(Date.now() - state.startedAt);
+ 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);
}
-// ===== SSE =====
-function disconnectSSE() { if (state.eventSource) { state.eventSource.close(); state.eventSource = null; } }
-
-function connectSSE() {
- disconnectSSE(); if (!state.currentConn) return;
- connectFetchSSE(state.currentConn.url + '/api/v1/chat/events');
+// ===== 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 {
- const headers = {};
+ var headers = {};
if (state.currentConn && state.currentConn.apiKey) headers['X-API-Key'] = state.currentConn.apiKey;
- const resp = await fetch(url, { headers, cache: 'no-store' });
+ var resp = await fetch(url, { headers: headers, cache: 'no-store' });
if (!resp.ok || !resp.body) { setTimeout(function() { connectSSE() }, 5000); return; }
- const reader = resp.body.getReader(); const decoder = new TextDecoder();
- let buffer = ''; let reconnectTimer = null;
+ 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() {
- const lines = buffer.split('\n'); buffer = lines.pop() || '';
- let eventType = '', data = '';
- for (const line of lines) {
+ 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 = ''; }
@@ -290,30 +1630,48 @@ async function connectFetchSSE(url) {
}
function handleSSEEvent(type, raw) {
try {
- const p = JSON.parse(raw);
+ var ev = JSON.parse(raw); var p = ev.payload || {};
if (type === 'agent_output') {
- const last = state.messages[state.messages.length - 1];
- if (last && last.role === 'assistant' && last._streaming) {
- last.content = (last.content || '') + (p.content || '');
- rerenderChatIfActive();
+ 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;
}
- } else if (type === 'stage') {
- const phase = p.payload?.phase;
- if (phase === 'thinking') state.chatStage = 'Thinking...';
- else if (phase === 'before_toolcall') state.chatStage = 'Tool: ' + (p.payload?.tool || '');
- else if (phase === 'before_output') state.chatStage = 'Output...';
- updateChatStageBadge();
+ state.messages.push({ role: 'assistant', content: p.content || '', _streaming: true });
+ rerenderChatIfActive();
} else if (type === 'reasoning') {
- const last = state.messages[state.messages.length - 1];
- if (last && last.role === 'assistant' && last._streaming) {
- last.reasoning_content = (last.reasoning_content || '') + (p.payload?.content || '');
+ 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 { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); processLines(); } catch(e) { break; }
+ 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);
}
@@ -321,393 +1679,8 @@ async function connectFetchSSE(url) {
} catch(e) { setTimeout(function() { connectSSE() }, 5000); }
}
-function startSidebarRefresh() {
- if (state.sidebarRefreshTick) clearInterval(state.sidebarRefreshTick);
- state.sidebarRefreshTick = setInterval(async function() {
- try { await loadSidebarData() } catch(e) {}
- }, 5000);
-}
-
-// ===== Overview =====
-function statCard(l, v) { return ''; }
-
-function renderOverview() {
- const s = state.status || {}; const k = state.kernel;
- let html = '' + statCard('Status', s.status || 'unknown')
- + statCard('Uptime', '' + (state.startedAt ? fmtUptime(Date.now() - state.startedAt) : '-') + ' ')
- + statCard('Plugins', (k?.plugins || []).length || 0)
- + statCard('Version', s.version || '-') + '
';
- if (k) {
- html += ''
- + '
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 || '-') + '
'
- + 'Memory Graph '
- + 'Loading memory graph...
';
- document.getElementById('tab-overview').innerHTML = html;
- loadStarmapData();
-}
-
-async function loadStarmapData() {
- try {
- const resp = await api('/memory/graph');
- if (resp && resp.success && resp.data && resp.data.nodes && resp.data.nodes.length > 0) {
- state._starmapData = resp.data;
- document.getElementById('starmap-placeholder').textContent = resp.data.nodes.length + ' nodes, ' + (resp.data.edges?.length || 0) + ' edges';
- } else {
- document.getElementById('starmap-placeholder').textContent = 'No memory data yet';
- }
- } catch(e) {
- document.getElementById('starmap-placeholder').textContent = 'Failed to load: ' + e.message;
- }
-}
-
-// ===== Chat =====
-let _chatLayoutBuilt = false;
-let _terminals = [], _cmdHistory = [];
-
-function buildChatLayout() {
- const k = state.kernel || {};
- document.getElementById('tab-chat').innerHTML =
- ''
- + '
Chat '
- + '
'
- + '
'
- + 'Send
'
- + '
';
- _chatLayoutBuilt = true;
-}
-
-function renderChat() {
- if (!_chatLayoutBuilt) { buildChatLayout(); renderTerminalsList(); renderCmdHistoryList(); }
- const msgsEl = document.getElementById('chat-msgs');
- if (!msgsEl) return;
- if (state.messages.length === 0) {
- msgsEl.innerHTML = ''; return;
- }
- let html = '';
- state.messages.forEach(function(m) {
- const role = m.role || 'user'; let c = m.content || '';
- if (role === 'assistant') { c = renderMarkdown(c) } else { c = '' + escHtml(c) + ' ' }
- const rc = m.reasoning_content ? 'Collapse
' + renderMarkdown(m.reasoning_content) + '
' : '';
- html += '' + (role === 'user' ? 'U' : 'A') + '
'
- + '
';
- });
- msgsEl.innerHTML = html; msgsEl.scrollTop = msgsEl.scrollHeight;
- updateChatStageBadge();
-}
-
-function switchChatSub(name, el) {
- document.querySelectorAll('.sidebar-subnav span').forEach(function(e) { e.classList.remove('active') });
- if (el) el.classList.add('active');
- ['memory','context','knowledge'].forEach(function(s) { document.getElementById('chat-sub-' + s).style.display = s === name ? 'block' : 'none' });
-}
-
-async function sendChat() {
- const inp = document.getElementById('chat-input'); const btn = document.getElementById('chat-send-btn');
- const text = inp.value.trim(); if (!text || state.chatLoading) return;
- state.messages.push({ role: 'user', content: text }); inp.value = '';
- const streamingMsg = { role: 'assistant', content: '', reasoning_content: '', _streaming: true };
- state.messages.push(streamingMsg); renderChat();
- state.chatLoading = true; btn.disabled = true; btn.textContent = '...';
- try {
- const r = await api('/chat', { method: 'POST', body: JSON.stringify({ message: text }) });
- if (streamingMsg._streaming) {
- streamingMsg.content = r.response || '(no response)'; streamingMsg.reasoning_content = r.reasoning_content || '';
- } else {
- streamingMsg.content = (streamingMsg.content || '') + (r.response || ''); streamingMsg.reasoning_content = (streamingMsg.reasoning_content || '') + (r.reasoning_content || '');
- }
- delete streamingMsg._streaming; saveChatHistory(); renderChat();
- } catch(e) {
- if (streamingMsg._streaming) { streamingMsg.content = 'Error: ' + e.message; delete streamingMsg._streaming; }
- renderChat(); toast('Request failed: ' + e.message, true);
- } finally {
- state.chatLoading = false; btn.disabled = false; btn.textContent = 'Send'; renderChat();
- }
-}
-
-function updateChatStageBadge() {
- const badge = document.getElementById('chat-stage');
- if (!badge) return; badge.textContent = state.chatStage || ''; badge.style.display = state.chatStage ? 'inline' : 'none';
-}
-
function rerenderChatIfActive() {
- const tab = document.getElementById('tab-chat');
- if (tab && tab.classList.contains('active')) renderChat();
+ var tab = document.getElementById('tab-chat');
+ if (tab && tab.classList.contains('active')) { renderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory(); }
}
-async function loadSidebarData() {
- try { const d = await api('/terminals'); _terminals = d?.terminals || []; renderTerminalsList() } catch(e) {}
- try { const d = await api('/cmd/history'); _cmdHistory = d?.history || []; renderCmdHistoryList() } catch(e) {}
-}
-
-function renderTerminalsList() {
- const el = document.getElementById('term-list'); const badge = document.getElementById('term-count-badge');
- if (!el) return; if (badge) badge.textContent = _terminals.length;
- if (_terminals.length === 0) { el.innerHTML = 'No active terminals
'; return; }
- el.innerHTML = _terminals.map(function(t) {
- const status = t.running ? ' ' : ' ';
- return '' + status + ' ' + escHtml((t.command || t.id || '').substring(0, 40)) + ' ' + (t.uptime || '') + '
';
- }).join('');
-}
-
-function renderCmdHistoryList() {
- const el = document.getElementById('cmd-list'); const badge = document.getElementById('cmd-count-badge');
- if (!el) return; if (badge) badge.textContent = _cmdHistory.length;
- if (_cmdHistory.length === 0) { el.innerHTML = 'No command history
'; return; }
- el.innerHTML = _cmdHistory.slice(-10).reverse().map(function(c) {
- const status = c.status === 'completed' ? 'OK ' : '' + escHtml(c.status || 'FAIL') + ' ';
- return '' + escHtml((c.command || '').substring(0, 40)) + ' ' + status + '
';
- }).join('');
-}
-
-// ===== Chat History Persistence =====
-async function saveChatHistory() {
- try {
- const msgs = state.messages.filter(function(m) { return !m._streaming }).map(function(m) {
- return { role: m.role, content: m.content, reasoning_content: m.reasoning_content, time: new Date().toISOString() };
- }).slice(-100);
- await api('/settings', { method: 'PUT', body: JSON.stringify({ key: 'plugin.webui.chathistory', value: JSON.stringify(msgs) }) });
- } catch(e) {}
-}
-
-async function loadChatHistory() {
- if (state._chatHistoryLoaded || !state.currentConn) return;
- try {
- const s = await api('/settings?prefix=plugin.webui');
- const raw = s?.settings?.['plugin.webui.chathistory'];
- if (raw) {
- const parsed = JSON.parse(raw);
- if (Array.isArray(parsed) && parsed.length > 0) {
- state.messages = parsed.map(function(m) { return { role: m.role, content: m.content || '', reasoning_content: m.reasoning_content || '' } });
- state._chatHistoryLoaded = true;
- renderChat();
- }
- }
- } catch(e) {}
- state._chatHistoryLoaded = true;
-}
-
-// hook into renderChat init
-const _origRenderChat = renderChat;
-renderChat = function() {
- loadChatHistory();
- return _origRenderChat.apply(this, arguments);
-};
-
-async function queryMemoryChat() {
- const q = document.getElementById('mem-query')?.value; const r = document.getElementById('mem-result-chat');
- if (!r || !q) return; r.innerHTML = '
';
- try { const d = await api('/memory?q=' + encodeURIComponent(q) + '&depth=2'); r.innerHTML = '' + escHtml(JSON.stringify(d, null, 2)) + ' '; }
- catch(e) { r.innerHTML = 'Query failed: ' + escHtml(e.message) + '
'; }
-}
-
-async function queryMemoryContext() {
- const q = document.getElementById('ctx-query')?.value; const r = document.getElementById('ctx-result');
- if (!r) return; r.innerHTML = '
';
- try {
- const d = await api('/memory/context?q=' + encodeURIComponent(q || ''));
- let html = '';
- if (d?.summary) html += '
Summary ' + escHtml(d.summary) + '
';
- html += '
Token Estimate ' + (d?.token_estimate || 0) + '
';
- if (d?.entities?.length) html += '
Entities ' + d.entities.map(function(e) { return escHtml(e.name || e.id || '') }).join(', ') + '
';
- html += '
Context ' + escHtml(d?.context || 'No context') + ' ';
- r.innerHTML = html;
- } catch(e) { r.innerHTML = 'Query failed: ' + escHtml(e.message) + '
'; }
-}
-
-async function searchKnowledgeChat() {
- const q = document.getElementById('know-query')?.value; const r = document.getElementById('know-result-chat');
- if (!r || !q) return; r.innerHTML = '
';
- try { const d = await api('/knowledge?q=' + encodeURIComponent(q)); r.innerHTML = '' + escHtml(JSON.stringify(d, null, 2)) + ' '; }
- catch(e) { r.innerHTML = 'Search failed: ' + escHtml(e.message) + '
'; }
-}
-
-async function createKnowledgeChat() {
- const name = document.getElementById('know-name')?.value; const content = document.getElementById('know-content')?.value;
- if (!name || !content) { toast('Name and content required', true); return; }
- try { await api('/knowledge', { method: 'POST', body: JSON.stringify({ name, content }) }); toast('Knowledge created'); document.getElementById('know-name').value = ''; document.getElementById('know-content').value = ''; }
- catch(e) { toast('Create failed: ' + e.message, true); }
-}
-
-// ===== Plugins =====
-async function renderPlugins() {
- const list = state.installedPlugins?.plugins || []; const info = state.pluginInfo;
- let html = '';
- if (list.length === 0) { html += '
No plugins installed
'; }
- else {
- html += '
Name Type Status ';
- list.forEach(function(p) {
- const status = p.loaded ? 'Loaded ' : 'Error ';
- html += '' + escHtml(p.name || '') + ' ' + escHtml(p.type || '') + ' ' + status + ' '
- + 'Info ';
- }); html += '
';
- }
- html += '
';
- if (info) {
- html += '' + escHtml(info.name || '') + ' Details ' + escHtml(JSON.stringify(info, null, 2)) + ' '
- + '
Close ';
- }
- document.getElementById('tab-plugins').innerHTML = html;
-}
-
-async function showPluginInfo(name) {
- try { const d = await api('/plugins/' + encodeURIComponent(name)); state.pluginInfo = d; renderPlugins(); }
- catch(e) { toast('Failed: ' + e.message, true); }
-}
-
-async function installPluginFile(file) {
- if (!file) return;
- try {
- const form = new FormData(); form.append('plugin', file);
- await fetch(state.currentConn.url + '/api/v1/plugins', { method: 'POST', body: form, headers: state.currentConn.apiKey ? { 'X-API-Key': state.currentConn.apiKey } : {} });
- toast('Plugin uploaded'); state.installedPlugins = await api('/plugins'); renderPlugins();
- } catch(e) { toast('Upload failed: ' + e.message, true); }
-}
-
-async function reloadPlugins() {
- try { await api('/plugins/reload', { method: 'POST' }); state.kernel = await api('/kernel'); state.installedPlugins = await api('/plugins'); toast('Plugins reloaded'); renderPlugins(); renderOverview(); }
- catch(e) { toast('Reload failed: ' + e.message, true); }
-}
-
-// ===== Knowledge Browser =====
-async function renderKnowledgeBrowser() {
- if (document.getElementById('tab-knowledge')) return;
- const tab = document.getElementById('tab-kernel');
- if (!tab || !tab.classList.contains('active')) return;
- const cont = document.getElementById('knowledge-browser');
- if (!cont) return;
- try {
- const d = await api('/knowledge');
- let html = 'Knowledge Base ';
- if (d?.categories) {
- html += '
Name Size ';
- (d.categories || []).forEach(function(c) {
- html += '' + escHtml(c.name || c) + ' ' + (c.content_length || '-') + ' ';
- });
- html += '
';
- }
- if (d?.stats) {
- html += '
' + statCard('Categories', d.stats.categories || 0) + statCard('Items', d.stats.items || 0) + statCard('Size', d.stats.size || 0) + '
';
- }
- html += '
';
- cont.innerHTML = html;
- } catch(e) { cont.innerHTML = '' + escHtml(e.message) + '
'; }
-}
-
-// ===== Settings =====
-function renderOneSettings() {
- const section = state.selectedSection || 'core';
- const isPlugin = section.startsWith('plugin.'); const prefix = isPlugin ? section : (section === 'core' ? '' : section);
- const values = {}; const meta = {};
- if (isPlugin) { const pname = section.substring(7); Object.entries(state.settings).filter(function(e) { return e[0].startsWith('plugin.' + pname + '.') }).forEach(function(e) { values[e[0]] = e[1] }) }
- else if (section === 'core') { Object.entries(state.settings).filter(function(e) { return !e[0].startsWith('plugin.') }).forEach(function(e) { values[e[0]] = e[1] }) }
- else { Object.entries(state.settings).filter(function(e) { return e[0].startsWith(section + '.') || (!e[0].startsWith('plugin.') && e[0].startsWith(section)) }).forEach(function(e) { values[e[0]] = e[1] }) }
- Object.assign(meta, state.meta);
- let html = '' + (isPlugin ? 'Plugin: ' + section.substring(7) : 'Core Settings') + ' ';
- const keys = Object.keys(values);
- if (keys.length === 0) { html += '
No settings
' }
- else {
- keys.sort().forEach(function(k) {
- const v = values[k]; const m = meta[k]; const display = m?.displayName || k.split('.').pop() || k; const desc = m?.description || '';
- html += '
' + escHtml(k) + '
' + escHtml(display) + ' '
- + '
'
- + (desc ? '
' + escHtml(desc) + '
' : '') + '
';
- });
- }
- html += '
';
- document.getElementById('tab-settings').innerHTML = '';
-}
-
-async function saveSetting(key, value) {
- try { await api('/settings', { method: 'PUT', body: JSON.stringify({ key, value }) }); toast('Saved'); const s = await api('/settings'); state.settings = s.settings || {}; state.meta = s.meta || {}; renderOneSettings(); }
- catch(e) { toast('Save failed: ' + e.message, true); }
-}
-
-// ===== Adapters =====
-async function renderAdapters() {
- let html = '
LLM Adapters Upload ';
- try {
- const d = await api('/adapters'); const adapters = d?.adapters || [];
- if (adapters.length === 0) { html += '
No adapters
' }
- else {
- html += '
Name Type ';
- adapters.forEach(function(a) { html += '' + escHtml(a.name || a) + ' ' + escHtml(a.type || 'lua') + ' Delete ' });
- html += '
';
- }
- } catch(e) { html += '
Failed to load: ' + escHtml(e.message) + '
' }
- html += '
';
- document.getElementById('tab-adapters').innerHTML = html;
-}
-
-function showUploadAdapter() { const name = prompt('Adapter name:'); if (!name) return; const code = prompt('Paste Lua adapter code:'); if (!code) return; uploadAdapter(name, code); }
-async function uploadAdapter(name, code) { try { await api('/adapters', { method: 'POST', body: JSON.stringify({ name, code }) }); toast('Uploaded'); renderAdapters(); } catch(e) { toast('Failed: ' + e.message, true); } }
-async function deleteAdapter(name) { if (!confirm('Delete: ' + name + '?')) return; try { await api('/adapters/' + encodeURIComponent(name), { method: 'DELETE' }); toast('Deleted'); renderAdapters(); } catch(e) { toast('Failed: ' + e.message, true); } }
-
-// ===== Kernel =====
-async function renderKernel() {
- const k = state.kernel;
- let html = 'Kernel Status ';
- if (!k) { html += '
Unavailable
' }
- else {
- html += '
LLM Provider ' + escHtml(k.llm?.provider || '-') + '
Sources ' + (k.llm?.sources || 0) + '
Available ' + (k.llm?.available ? 'Yes' : 'No') + '
'
- + '
Memory Available ' + (k.memory?.available ? 'Yes' : 'No') + '
'
- + (k.memory?.available ? '
Entities ' + k.memory.entity_count + '
Relations ' + k.memory.relation_count + '
' : '') + '
';
- html += '
Runtime Goroutines ' + (k.runtime?.goroutines || '-') + '
Memory ' + (k.runtime?.memory_mb || '-') + ' MB
Go Version ' + escHtml(k.runtime?.go_version || '-') + '
';
- html += '
Plugins ';
- if (k.plugins && k.plugins.length > 0) { html += '
' + k.plugins.map(function(p) { return '' + escHtml(p.name || p) + ' ' }).join('') + '
' }
- }
- html += '
Actions Run Healthcheck '
- + 'View Text Memory '
- + '
'
- + '
';
- document.getElementById('tab-kernel').innerHTML = html;
- renderKnowledgeBrowser();
-}
-
-async function runHealthcheck() {
- const el = document.getElementById('health-result'); el.innerHTML = '
';
- try { const r = await api('/chat', { method: 'POST', body: JSON.stringify({ message: 'Please run the healthcheck tool for a full system check and report the results' }) }); el.innerHTML = 'Healthcheck Result ' + escHtml(r.response || '') + ' '; }
- catch(e) { el.innerHTML = '' + escHtml(e.message) + '
'; }
-}
-async function loadTextMemory() {
- const el = document.getElementById('text-memory-result'); el.innerHTML = '
';
- try { const d = await api('/memory/text'); el.innerHTML = 'Text Memory ' + escHtml(JSON.stringify(d, null, 2)) + ' '; }
- catch(e) { el.innerHTML = '' + escHtml(e.message) + '
'; }
-}
-
-// ===== Init =====
-document.addEventListener('DOMContentLoaded', initApp);
diff --git a/cmd/gui/renderer/index.html b/cmd/gui/renderer/index.html
index caca1aa..15ce857 100644
--- a/cmd/gui/renderer/index.html
+++ b/cmd/gui/renderer/index.html
@@ -5,6 +5,10 @@
HomeAgent
+
+
+
+
@@ -19,7 +23,7 @@
地址 / URL
- API 密钥 / API Key
+ API 密钥 / API Key (可选)