feat: 插件-工具合并 — 创建 pkg/plugins 共享模块并移除 tool-engine

- 新增 backend/pkg/plugins/ 共享模块:SDK 接口、PluginManager、ToolRegistry(含环形缓冲区调用日志)
- 13 个通用插件从 plugin-manager 迁移至共享模块(import 路径统一)
- ai-core 切换至共享 ToolRegistry,进程内执行(零网络开销),包装 6 个专属工具
- plugin-manager 迁移至共享模块,保留管理 REST API
- 新增 DevTools 插件管理面板(侧边栏 → 🔌 插件管理)
- 移除 tool-engine 服务(从 go.work、DevTools 配置、编译系统)
- 工具调用记录 API 从 Tool-Engine 迁至 AI-Core(/api/v1/tools/calls)
- ai-core ContextStore 启动时从 PostgreSQL 恢复会话历史
- 清理所有过时引用和备份文件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 20:52:39 +08:00
parent 5325eaca3f
commit 673ff752c5
78 changed files with 1313 additions and 5187 deletions
+404 -8
View File
@@ -727,6 +727,9 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<span class="nav-icon">🎤</span><span class="nav-label">语音识别</span>
<span class="nav-badge" id="stt-badge" style="display:none">0</span>
</button>
<button class="nav-item" data-panel="plugins">
<span class="nav-icon">🔌</span><span class="nav-label">插件管理</span>
</button>
</details>
<details class="nav-group">
@@ -748,6 +751,9 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<button class="nav-item" data-panel="llmCalls">
<span class="nav-icon">📊</span><span class="nav-label">LLM 调用</span>
</button>
<button class="nav-item" data-panel="thinkingSchedule">
<span class="nav-icon"></span><span class="nav-label">思考调度</span>
</button>
</details>
</nav>
<div class="sidebar-footer">
@@ -781,6 +787,7 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<div class="panel" id="panel-toolCalls"></div>
<!-- 语音识别日志 -->
<div class="panel" id="panel-stt"></div>
<div class="panel" id="panel-plugins"></div>
<!-- 自主思考日志 -->
<div class="panel" id="panel-thinking"></div>
<!-- 记忆时间线 -->
@@ -790,6 +797,7 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<!-- 客户端管理 -->
<div class="panel" id="panel-clients"></div>
<div class="panel" id="panel-modelConfig"></div>
<div class="panel" id="panel-thinkingSchedule"></div>
<div class="panel" id="panel-llmCalls"></div>
</div>
</div>
@@ -808,11 +816,11 @@ const STATE = {
serviceStatus: {},
// 日志
activeLogTab: 'ai-core',
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [], 'iot-debug-service': [], 'memory-service': [], 'tool-engine': [], 'voice-service': [] },
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [], 'iot-debug-service': [], 'memory-service': [], 'voice-service': [] },
maxLogLines: 500,
logLayout: 'grid',
// 性能
perfHistory: { 'ai-core': [], 'gateway': [], 'iot-debug-service': [], 'frontend': [], 'memory-service': [], 'tool-engine': [], 'voice-service': [] },
perfHistory: { 'ai-core': [], 'gateway': [], 'iot-debug-service': [], 'frontend': [], 'memory-service': [], 'voice-service': [] },
// 会话
sessionsData: [],
sessionsAutoRefresh: null,
@@ -996,10 +1004,10 @@ function statusBadge(status) {
return map[status] || 'badge-stopped';
}
const ALL_SVC_IDS = ['ai-core', 'gateway', 'frontend', 'iot-debug-service', 'memory-service', 'tool-engine', 'voice-service'];
const ALL_SVC_IDS = ['ai-core', 'gateway', 'frontend', 'iot-debug-service', 'memory-service', 'voice-service'];
function escapeId(id) {
const map = { 'ai-core': 'AI-Core', 'gateway': 'Gateway', 'frontend': 'Frontend', 'iot-debug-service': 'IoT Debug', 'memory-service': 'Memory', 'tool-engine': 'Tool Engine', 'voice-service': 'Voice' };
const map = { 'ai-core': 'AI-Core', 'gateway': 'Gateway', 'frontend': 'Frontend', 'iot-debug-service': 'IoT Debug', 'memory-service': 'Memory', 'voice-service': 'Voice' };
return map[id] || id;
}
@@ -1085,10 +1093,11 @@ function switchPanel(name) {
const titles = {
dashboard: '🏠 仪表盘', memory: '🧠 记忆管理', sessions: '💬 会话监看',
services: '🖥 服务管理', iot: '🏠 IoT 设备控制', performance: '📊 性能监控', database: '🗄️ 数据库监看',
toolCalls: '🔧 工具调用记录', stt: '🎤 语音识别日志', thinking: '💭 自主思考', timeline: '⏱️ 记忆时间线',
toolCalls: '🔧 工具调用记录', stt: '🎤 语音识别日志', plugins: '🔌 插件管理', thinking: '💭 自主思考', timeline: '⏱️ 记忆时间线',
chatPlatforms: '💬 第三方聊天配置与消息日志',
clients: '📱 客户端管理',
modelConfig: '🤖 模型配置管理',
thinkingSchedule: '⏰ 思考调度配置',
llmCalls: '📊 LLM 调用日志',
};
document.getElementById('panel-title').textContent = titles[name] || name;
@@ -1111,11 +1120,13 @@ function switchPanel(name) {
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'toolCalls': renderToolCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'stt': renderSTTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'plugins': renderPluginsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'thinking': renderThinkingPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'timeline': renderTimelinePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); startTimelineAutoRefresh(); break;
case 'chatPlatforms': renderChatPlatformsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); startChatAutoRefresh(); break;
case 'clients': renderClientsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'modelConfig': renderModelConfigPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'thinkingSchedule': renderThinkingSchedulePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'llmCalls': renderLlmCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
}
}
@@ -2207,7 +2218,6 @@ function renderServicesPanel() {
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-iot-debug-service" style="height:300px"><div class="empty-state"><div class="icon">📝</div>IoT Debug</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-frontend" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Frontend</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-memory-service" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Memory</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-tool-engine" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Tool Engine</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-voice-service" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Voice</div></div></div>
</div>
</div>
@@ -4417,6 +4427,392 @@ async function deleteRoutingRule(purpose) {
renderRoutingTab();
}
// ========== 思考调度配置面板 ==========
const ALL_DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
const DAY_LABELS = { monday: '一', tuesday: '二', wednesday: '三', thursday: '四', friday: '五', saturday: '六', sunday: '日' };
function computeCurrentInterval(cfg) {
var now = new Date();
var wd = ALL_DAYS[(now.getDay() + 6) % 7];
var mins = now.getHours() * 60 + now.getMinutes();
for (var i = 0; i < cfg.rules.length; i++) {
var rule = cfg.rules[i];
if (!rule.days || rule.days.indexOf(wd) < 0) continue;
var tr = parseTimeRange(rule.time_range);
if (!tr) continue;
var inRange = tr.start <= tr.end ? (mins >= tr.start && mins < tr.end) : (mins >= tr.start || mins < tr.end);
if (!inRange) continue;
var excepted = false;
for (var e = 0; e < (rule.except || []).length; e++) {
var er = parseTimeRange(rule.except[e]);
if (er) {
var eIn = er.start <= er.end ? (mins >= er.start && mins < er.end) : (mins >= er.start || mins < er.end);
if (eIn) { excepted = true; break; }
}
}
if (!excepted) return rule.interval_minutes;
}
return cfg.default_interval_minutes || 5;
}
function parseTimeRange(r) {
var parts = r.split('-');
if (parts.length !== 2) return null;
var start = parseHM(parts[0].trim());
var end = parseHM(parts[1].trim());
if (start === null || end === null) return null;
return { start: start, end: end };
}
function parseHM(s) {
var parts = s.split(':');
if (parts.length !== 2) return null;
var h = parseInt(parts[0], 10), m = parseInt(parts[1], 10);
if (isNaN(h) || isNaN(m) || h < 0 || h > 23 || m < 0 || m > 59) return null;
return h * 60 + m;
}
function renderThinkingSchedulePanel() {
var panel = document.getElementById('panel-thinkingSchedule');
panel.innerHTML = '<div class="card"><div class="card-body">加载中...</div></div>';
api('/api/thinking-schedule').then(function(cfg) {
if (cfg.error) {
panel.innerHTML = '<div class="card"><div class="card-body"><div class="empty-state">⚠ 加载失败: ' + escHtml(cfg.error) + '</div></div></div>';
return;
}
drawScheduleForm(panel, cfg);
scheduleAutoSave(cfg);
});
}
var _scheduleAutoSaveTimer = null;
function scheduleAutoSave(cfg) {
if (_scheduleAutoSaveTimer) clearInterval(_scheduleAutoSaveTimer);
_scheduleAutoSaveTimer = setInterval(function() {
var cur = computeCurrentInterval(cfg);
var el = document.getElementById('schedule-current-interval');
if (el) el.textContent = '当前间隔: ' + cur + ' 分钟';
}, 30000);
}
function drawScheduleForm(panel, cfg) {
var curInterval = computeCurrentInterval(cfg);
var rulesHtml = '';
for (var i = 0; i < cfg.rules.length; i++) {
rulesHtml += buildRuleRow(cfg.rules[i], i);
}
panel.innerHTML =
'<div class="card" style="margin-bottom:12px">' +
'<div class="card-header">' +
'<span class="card-title">⏰ 思考调度配置</span>' +
'<div class="btn-group">' +
'<span id="schedule-current-interval" style="font-size:13px;color:var(--accent);margin-right:12px">当前间隔: ' + curInterval + ' 分钟</span>' +
'<button class="btn btn-sm btn-accent" onclick="saveSchedule()">💾 保存</button>' +
'</div>' +
'</div>' +
'<div class="card-body">' +
'<div style="margin-bottom:12px;display:flex;align-items:center;gap:10px">' +
'<label style="white-space:nowrap;font-weight:600">默认间隔 (分钟):</label>' +
'<input type="number" id="sched-default-interval" value="' + (cfg.default_interval_minutes || 5) + '" min="1" max="120" style="width:80px" class="form-input"/>' +
'<span style="color:var(--text2);font-size:12px">无规则匹配时使用此间隔</span>' +
'</div>' +
'<div class="table-wrap"><table><thead><tr>' +
'<th>规则名称</th><th>适用日期</th><th>时间段</th><th>排除时段</th><th>间隔(分)</th><th>操作</th>' +
'</tr></thead><tbody id="sched-rules-tbody">' +
rulesHtml +
'</tbody></table></div>' +
'<button class="btn btn-sm" onclick="addScheduleRule()" style="margin-top:8px"> 添加规则</button>' +
'</div>' +
'</div>';
}
function buildRuleRow(rule, idx) {
var daysHtml = '';
for (var d = 0; d < ALL_DAYS.length; d++) {
var day = ALL_DAYS[d];
var checked = (rule.days || []).indexOf(day) >= 0 ? ' checked' : '';
daysHtml += '<label style="display:inline-flex;align-items:center;gap:2px;margin-right:4px;font-size:12px;cursor:pointer">' +
'<input type="checkbox" data-sched-days="' + idx + '" value="' + day + '"' + checked + ' style="margin:0"/>' +
DAY_LABELS[day] + '</label>';
}
var exceptVal = (rule.except || []).join(', ');
return '<tr>' +
'<td><input type="text" id="sched-name-' + idx + '" value="' + escHtml(rule.name || '') + '" class="form-input" style="width:120px"/></td>' +
'<td>' + daysHtml + '</td>' +
'<td><input type="text" id="sched-range-' + idx + '" value="' + escHtml(rule.time_range || '') + '" class="form-input" placeholder="HH:MM-HH:MM" style="width:100px"/></td>' +
'<td><input type="text" id="sched-except-' + idx + '" value="' + escHtml(exceptVal) + '" class="form-input" placeholder="HH:MM-HH:MM, ..." style="width:140px"/></td>' +
'<td><input type="number" id="sched-interval-' + idx + '" value="' + (rule.interval_minutes || 5) + '" min="1" max="120" class="form-input" style="width:60px"/></td>' +
'<td><button class="btn btn-xs btn-red" onclick="deleteScheduleRule(' + idx + ')">🗑</button></td>' +
'</tr>';
}
function collectScheduleConfig() {
var cfg = {
version: '1.0',
default_interval_minutes: parseInt(document.getElementById('sched-default-interval').value, 10) || 5,
rules: []
};
var tbody = document.getElementById('sched-rules-tbody');
var rows = tbody.querySelectorAll('tr');
for (var i = 0; i < rows.length; i++) {
var nameEl = document.getElementById('sched-name-' + i);
if (!nameEl) continue;
var days = [];
var checks = document.querySelectorAll('[data-sched-days="' + i + '"]:checked');
for (var c = 0; c < checks.length; c++) {
days.push(checks[c].value);
}
var exceptRaw = document.getElementById('sched-except-' + i).value.trim();
var exceptList = [];
if (exceptRaw) {
exceptList = exceptRaw.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; });
}
cfg.rules.push({
name: nameEl.value.trim(),
days: days,
time_range: document.getElementById('sched-range-' + i).value.trim(),
except: exceptList,
interval_minutes: parseInt(document.getElementById('sched-interval-' + i).value, 10) || 5
});
}
return cfg;
}
function addScheduleRule() {
var cfg = collectScheduleConfig();
cfg.rules.push({ name: '', days: [], time_range: '09:00-17:00', except: [], interval_minutes: 5 });
var panel = document.getElementById('panel-thinkingSchedule');
drawScheduleForm(panel, cfg);
}
function deleteScheduleRule(idx) {
var cfg = collectScheduleConfig();
cfg.rules.splice(idx, 1);
var panel = document.getElementById('panel-thinkingSchedule');
drawScheduleForm(panel, cfg);
}
async function saveSchedule() {
var cfg = collectScheduleConfig();
var result = await api('/api/thinking-schedule', { method: 'PUT', body: JSON.stringify(cfg) });
if (result.error) {
alert('保存失败: ' + result.error);
return;
}
// Re-render to refresh the current interval preview
api('/api/thinking-schedule').then(function(cfg) {
var panel = document.getElementById('panel-thinkingSchedule');
drawScheduleForm(panel, cfg);
});
}
// ========== 插件管理面板 ==========
var pluginsTab = 'list'; // 'list' or 'tools'
var pluginListData = [];
var toolListData = [];
function renderPluginsPanel() {
var panel = document.getElementById('panel-plugins');
panel.innerHTML =
'<div class="card">' +
'<div class="card-header">' +
'<span class="card-title">🔌 插件管理</span>' +
'<div class="btn-group">' +
'<button class="btn btn-sm' + (pluginsTab === 'list' ? ' btn-accent' : '') + '" onclick="switchPluginsTab(\'list\')">📦 插件列表</button>' +
'<button class="btn btn-sm' + (pluginsTab === 'tools' ? ' btn-accent' : '') + '" onclick="switchPluginsTab(\'tools\')">🔧 工具注册表</button>' +
'</div>' +
'</div>' +
'<div class="card-body" id="plugins-tab-content">' +
'<div class="empty-state">加载中...</div>' +
'</div>' +
'</div>';
if (pluginsTab === 'list') {
loadPluginList();
} else {
loadToolList();
}
}
function switchPluginsTab(tab) {
pluginsTab = tab;
renderPluginsPanel();
}
async function loadPluginList() {
var content = document.getElementById('plugins-tab-content');
try {
var result = await api('/api/plugins');
if (result.error) {
content.innerHTML = '<div class="empty-state">⚠ 加载失败: ' + escHtml(result.error) + '</div>';
return;
}
pluginListData = result.plugins || [];
var html = '';
if (pluginListData.length === 0) {
html = '<div class="empty-state">📦 暂无已安装的插件</div>';
} else {
html = '<div style="margin-bottom:8px;color:var(--text2);font-size:13px">共 ' + pluginListData.length + ' 个插件</div>';
html += '<div class="table-wrap"><table><thead><tr>' +
'<th>名称</th><th>版本</th><th>作者</th><th>分类</th><th>状态</th><th>工具</th><th>操作</th>' +
'</tr></thead><tbody>';
for (var i = 0; i < pluginListData.length; i++) {
var p = pluginListData[i];
var m = p.metadata || {};
var statusBadge = getStatusBadge(p.status, p.enabled);
var catIcon = getCategoryIcon(m.category);
var toolCount = (p.tools || []).length;
html += '<tr>' +
'<td><strong>' + escHtml(m.displayName || m.name) + '</strong><br><span style="font-size:11px;color:var(--text2)">' + escHtml(m.name) + '</span></td>' +
'<td>' + escHtml(m.version || '-') + '</td>' +
'<td>' + escHtml((m.author && m.author.name) || '-') + '</td>' +
'<td>' + catIcon + ' ' + escHtml(m.category || '-') + '</td>' +
'<td>' + statusBadge + '</td>' +
'<td><span class="badge">' + toolCount + '</span></td>' +
'<td>' + buildPluginActions(p) + '</td>' +
'</tr>';
}
html += '</tbody></table></div>';
}
content.innerHTML = html;
} catch (e) {
content.innerHTML = '<div class="empty-state">⚠ 请求失败: ' + escHtml(e.message) + '</div>';
}
}
async function loadToolList(filterText) {
var content = document.getElementById('plugins-tab-content');
try {
var result = await api('/api/tools');
if (result.error) {
content.innerHTML = '<div class="empty-state">⚠ 加载失败: ' + escHtml(result.error) + '</div>';
return;
}
toolListData = result.tools || [];
var filtered = toolListData;
if (filterText) {
var q = filterText.toLowerCase();
filtered = toolListData.filter(function(t) {
return (t.id || '').toLowerCase().indexOf(q) >= 0 ||
(t.name || '').toLowerCase().indexOf(q) >= 0 ||
(t.category || '').toLowerCase().indexOf(q) >= 0;
});
}
var html = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">' +
'<input type="text" class="form-input" id="tool-search" placeholder="搜索工具名称/ID/Category..." style="width:260px" oninput="loadToolList(this.value)"/>' +
'<span style="color:var(--text2);font-size:13px">共 ' + toolListData.length + ' 个工具' + (filtered.length !== toolListData.length ? ',显示 ' + filtered.length + ' 个' : '') + '</span>' +
'</div>';
if (filtered.length === 0) {
html += '<div class="empty-state">🔧 ' + (filterText ? '无匹配工具' : '暂无已注册的工具') + '</div>';
} else {
html += '<div class="table-wrap"><table><thead><tr>' +
'<th>Tool ID</th><th>Category</th><th>Complexity</th><th>DangerLevel</th><th>参数</th>' +
'</tr></thead><tbody>';
for (var i = 0; i < filtered.length; i++) {
var t = filtered[i];
var paramCount = 0;
var paramNames = [];
if (t.parameters && t.parameters.properties) {
paramNames = Object.keys(t.parameters.properties);
paramCount = paramNames.length;
}
var dangerBadge = '';
if (t.danger_level && t.danger_level !== 'low') {
var dc = t.danger_level === 'high' ? 'var(--red)' : 'var(--orange)';
dangerBadge = '<span class="badge" style="background:' + dc + '">' + escHtml(t.danger_level) + '</span>';
}
html += '<tr style="cursor:pointer" onclick="toggleToolParams(\'' + escHtml(t.id) + '\')">' +
'<td><strong>' + escHtml(t.name || t.id) + '</strong><br><span style="font-size:11px;color:var(--text2)">' + escHtml(t.id) + '</span></td>' +
'<td>' + escHtml(t.category || '-') + '</td>' +
'<td><span class="badge">' + escHtml(t.complexity || 'simple') + '</span></td>' +
'<td>' + (dangerBadge || escHtml(t.danger_level || 'low')) + '</td>' +
'<td><span style="color:var(--accent)">' + paramCount + ' 参数</span></td>' +
'</tr>' +
'<tr id="tool-params-' + escHtml(t.id) + '" style="display:none">' +
'<td colspan="5">' +
'<div class="card" style="margin:4px 0">' +
'<div class="card-body" style="font-size:12px">' +
'<div style="margin-bottom:4px"><strong>描述:</strong> ' + escHtml(t.description || '-') + '</div>' +
'<pre style="background:var(--bg);padding:8px;border-radius:4px;white-space:pre-wrap;max-height:200px;overflow-y:auto">' + escHtml(JSON.stringify(t.parameters, null, 2)) + '</pre>' +
'</div>' +
'</div>' +
'</td>' +
'</tr>';
}
html += '</tbody></table></div>';
}
content.innerHTML = html;
} catch (e) {
content.innerHTML = '<div class="empty-state">⚠ 请求失败: ' + escHtml(e.message) + '</div>';
}
}
function toggleToolParams(toolId) {
var row = document.getElementById('tool-params-' + toolId);
if (row) {
row.style.display = row.style.display === 'none' ? '' : 'none';
}
}
function getStatusBadge(status, enabled) {
if (status === 'error') {
return '<span class="badge" style="background:var(--red)">错误</span>';
}
if (!enabled || status === 'disabled') {
return '<span class="badge" style="background:var(--text2)">已禁用</span>';
}
if (status === 'running') {
return '<span class="badge" style="background:var(--green)">运行中</span>';
}
return '<span class="badge" style="background:var(--green)">' + escHtml(status) + '</span>';
}
function getCategoryIcon(cat) {
var icons = {
utility: '🔧', text: '📝', security: '🔒', data: '📊', filesystem: '📁',
network: '🌐', web: '🌍', iot: '🏠',
};
return icons[cat] || '📦';
}
function buildPluginActions(p) {
var name = escHtml((p.metadata && p.metadata.name) || '');
var enabled = p.enabled;
var canEnable = !enabled;
var canDisable = enabled && (p.metadata && p.metadata.name !== '');
var html = '';
if (canEnable) {
html += '<button class="btn btn-xs btn-accent" onclick="pluginAction(\'' + name + '\', \'enable\')" title="启用">▶</button> ';
}
if (canDisable) {
html += '<button class="btn btn-xs" onclick="pluginAction(\'' + name + '\', \'disable\')" title="禁用">⏸</button> ';
}
html += '<button class="btn btn-xs" onclick="pluginAction(\'' + name + '\', \'reload\')" title="重载">🔄</button>';
return html;
}
async function pluginAction(id, action) {
try {
var result = await api('/api/plugins/' + encodeURIComponent(id) + '/' + action, { method: 'POST' });
if (result.error) {
alert('操作失败 (' + action + '): ' + result.error);
return;
}
loadPluginList();
} catch (e) {
alert('请求异常: ' + e.message);
}
}
// ========== 客户端管理面板 ==========
function renderClientsPanel() {
@@ -4597,7 +4993,7 @@ function formatTokens(n) {
// Listen for browser back/forward navigation.
window.addEventListener('hashchange', function() {
var hash = location.hash.replace('#', '');
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig', 'llmCalls'];
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig', 'llmCalls', 'thinkingSchedule', 'plugins'];
if (hash && validPanels.indexOf(hash) >= 0 && hash !== STATE.activePanel) {
switchPanel(hash);
}
@@ -4608,7 +5004,7 @@ refreshStatus();
// Restore last panel from URL hash, or default to dashboard.
var initHash = location.hash.replace('#', '');
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig', 'llmCalls'];
var validPanels = ['dashboard', 'memory', 'sessions', 'services', 'iot', 'performance', 'database', 'toolCalls', 'stt', 'thinking', 'timeline', 'chatPlatforms', 'clients', 'modelConfig', 'llmCalls', 'thinkingSchedule', 'plugins'];
if (initHash && validPanels.indexOf(initHash) >= 0) {
switchPanel(initHash);
} else {