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:
+404
-8
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user