@greatlhd/ailo-desktop 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ const ICONS = {
6
6
  success: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
7
7
  error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>',
8
8
  info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
9
+ warning: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
9
10
  };
10
11
 
11
12
  function toast(msg, type = 'info', duration = 3500) {
@@ -40,18 +41,29 @@ function setBtnLoading(btn, loading, text) {
40
41
 
41
42
  function renderBadge(running, configured) {
42
43
  if (running) return '<span class="badge on"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="5"/></svg>运行中</span>';
43
- if (configured) return '<span class="badge warning">已配置 · 未运行</span>';
44
+ if (configured) return '<span class="badge warning">已配置</span>';
44
45
  return '<span class="badge muted">未配置</span>';
45
46
  }
46
47
 
47
48
  function updateNavBadge(id, running, configured) {
48
49
  const el = document.getElementById(id + 'NavBadge');
49
50
  if (!el) return;
50
- if (running) { el.style.display=''; el.className='nav-badge on'; el.textContent='运行中'; }
51
+ if (running) { el.style.display=''; el.className='nav-badge on'; el.textContent='在线'; }
51
52
  else if (configured) { el.style.display=''; el.className='nav-badge warning'; el.textContent='已配置'; }
52
53
  else { el.style.display='none'; }
53
54
  }
54
55
 
56
+ function updateBadgeCount(id, count) {
57
+ const el = document.getElementById(id);
58
+ if (!el) return;
59
+ if (count > 0) {
60
+ el.textContent = count;
61
+ el.style.display = '';
62
+ } else {
63
+ el.style.display = 'none';
64
+ }
65
+ }
66
+
55
67
  /* ─── Navigation ──────────────────────────────────────────────── */
56
68
  function nav(name) {
57
69
  document.querySelectorAll('.nav-item').forEach(el => el.classList.toggle('active', el.dataset.panel === name));
@@ -60,50 +72,107 @@ function nav(name) {
60
72
  if (panel) panel.classList.add('active');
61
73
  localStorage.setItem('ailo_nav', name);
62
74
 
63
- if (name === 'status') { loadStatus(); if (SHOW_CONNECTION_FORM) loadConnectionForm(); }
64
- if (name === 'env') loadEnvCheck();
65
- if (name === 'mcp') loadMCP();
66
- if (name === 'tools') toolsSub(localStorage.getItem('ailo_tools_sub') || 'reported');
67
- if (name === 'feishu') loadFeishuConfig();
75
+ switch (name) {
76
+ case 'status':
77
+ loadStatus();
78
+ if (SHOW_CONNECTION_FORM) loadConnectionForm();
79
+ break;
80
+ case 'env': loadEnvCheck(); break;
81
+ case 'mcp': loadMCP(); break;
82
+ case 'tools': toolsSub(localStorage.getItem('ailo_tools_sub') || 'tools'); break;
83
+ case 'feishu': loadFeishuConfig(); break;
84
+ }
68
85
  }
69
86
 
70
87
  function toolsSub(sub) {
71
- document.querySelectorAll('.sub-tabs:not(.blueprint-tabs) .sub-tab').forEach(b => b.classList.toggle('active', b.dataset.sub === sub));
88
+ document.querySelectorAll('#panel-tools .sub-tab').forEach(b => b.classList.toggle('active', b.dataset.sub === sub));
72
89
  document.querySelectorAll('#panel-tools .sub-panel').forEach(p => p.classList.toggle('active', p.id === 'sub-' + sub));
73
90
  localStorage.setItem('ailo_tools_sub', sub);
74
- if (sub === 'reported') loadReportedTools();
75
- if (sub === 'blueprints') loadAllBlueprints();
91
+ if (sub === 'tools') refreshTools();
92
+ else if (sub === 'skills') loadReportedSkills();
76
93
  }
77
94
 
78
95
  function hideModal(id) { document.getElementById(id).classList.remove('open'); }
79
96
  function showModal(id) { document.getElementById(id).classList.add('open'); }
80
97
 
81
98
  /* ─── Status ──────────────────────────────────────────────────── */
99
+ let lastStatus = { connected: false, endpointId: '' };
100
+
82
101
  async function loadStatus() {
83
102
  try {
84
103
  const s = await fetch('/api/status').then(r => r.json());
104
+ lastStatus = s;
105
+
106
+ // 侧边栏状态
85
107
  const dot = document.getElementById('globalDot');
86
108
  const gs = document.getElementById('globalStatus');
87
- const label = document.getElementById('connectionLabel');
88
- const sublabel = document.getElementById('connectionSublabel');
89
- const badgeWrap = document.getElementById('connectionBadgeWrap');
90
- const grid = document.getElementById('statusInfoGrid');
91
-
92
109
  if (dot) dot.className = 'status-dot ' + (s.connected ? 'on' : 'off');
93
110
  if (gs) gs.textContent = s.connected ? '已连接' : '未连接';
94
- if (label) label.textContent = s.connected ? '已连接至 Ailo' : '未连接';
95
- if (sublabel) sublabel.textContent = s.connected ? `端点 ID: ${s.endpointId || '-'}` : '尚未连接至 Ailo 服务';
96
- if (badgeWrap) badgeWrap.innerHTML = s.connected
97
- ? '<span class="badge on">在线</span>'
98
- : '<span class="badge off">离线</span>';
99
- if (grid) {
100
- grid.style.display = s.connected ? 'grid' : 'none';
101
- const epEl = document.getElementById('statusEndpointId');
102
- const connEl = document.getElementById('statusConnected');
103
- if (epEl) epEl.textContent = s.endpointId || '-';
104
- if (connEl) connEl.innerHTML = s.connected ? '<span class="badge on">已连接</span>' : '<span class="badge off">断开</span>';
111
+
112
+ // 状态页
113
+ const title = document.getElementById('statusTitle');
114
+ const subtitle = document.getElementById('statusSubtitle');
115
+ if (title) title.textContent = s.connected ? '已连接' : '未连接';
116
+ if (subtitle) subtitle.textContent = s.connected
117
+ ? `端点 ${s.endpointId || '-'} 已连接到 Ailo`
118
+ : '尚未连接至 Ailo 服务';
119
+
120
+ // 状态卡片统计
121
+ const icon = document.getElementById('statusIcon');
122
+ if (icon) {
123
+ icon.className = 'status-icon ' + (s.connected ? 'connected' : 'disconnected');
105
124
  }
106
- } catch (e) {}
125
+
126
+ // 更新端点 ID
127
+ const epEl = document.getElementById('statEndpointId');
128
+ if (epEl) epEl.textContent = s.endpointId || '-';
129
+
130
+ // 工具数量和 MCP 数量需要额外请求
131
+ if (s.connected) {
132
+ refreshToolsCount();
133
+ loadMcpCount();
134
+ } else {
135
+ updateBadgeCount('toolsCount', 0);
136
+ updateBadgeCount('mcpCount', 0);
137
+ const toolCountEl = document.getElementById('statToolCount');
138
+ const mcpCountEl = document.getElementById('statMcpCount');
139
+ if (toolCountEl) toolCountEl.textContent = '-';
140
+ if (mcpCountEl) mcpCountEl.textContent = '-';
141
+ }
142
+
143
+ // 运行时间
144
+ const uptimeEl = document.getElementById('statUptime');
145
+ if (uptimeEl) uptimeEl.textContent = s.uptime || '-';
146
+
147
+ } catch (e) {
148
+ console.error('Failed to load status:', e);
149
+ }
150
+ }
151
+
152
+ async function refreshToolsCount() {
153
+ try {
154
+ const tools = await fetch('/api/tools').then(r => r.json());
155
+ const count = Array.isArray(tools) ? tools.length : 0;
156
+ updateBadgeCount('toolsCount', count);
157
+ const el = document.getElementById('statToolCount');
158
+ if (el) el.textContent = count;
159
+ return count;
160
+ } catch (e) {
161
+ return 0;
162
+ }
163
+ }
164
+
165
+ async function loadMcpCount() {
166
+ try {
167
+ const d = await fetch('/api/mcp').then(r => r.json());
168
+ const runningCount = (d.servers || []).filter(s => s.running).length;
169
+ updateBadgeCount('mcpCount', runningCount);
170
+ const el = document.getElementById('statMcpCount');
171
+ if (el) el.textContent = runningCount;
172
+ return runningCount;
173
+ } catch (e) {
174
+ return 0;
175
+ }
107
176
  }
108
177
 
109
178
  async function loadConnectionForm() {
@@ -126,99 +195,203 @@ async function saveConnection() {
126
195
  if (!wsUrl || !apiKey || !endpointId) { toast('请填写全部三项连接信息', 'error'); return; }
127
196
  setBtnLoading(btn, true, '保存中...');
128
197
  try {
129
- const r = await fetch('/api/connection', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ailoWsUrl:wsUrl,ailoApiKey:apiKey,endpointId}) }).then(r=>r.json());
198
+ const r = await fetch('/api/connection', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ailoWsUrl:wsUrl, ailoApiKey:apiKey, endpointId}) }).then(r=>r.json());
130
199
  if (r.ok) {
131
- toast(r.message || '已保存', 'success');
132
- let cnt = 0; const iv = setInterval(() => { loadStatus(); if (++cnt >= 5) clearInterval(iv); }, 2000);
200
+ toast(r.message || '配置已保存,将自动重连', 'success');
201
+ let cnt = 0;
202
+ const iv = setInterval(async () => {
203
+ await loadStatus();
204
+ if (++cnt >= 10) clearInterval(iv);
205
+ }, 2000);
133
206
  } else toast(r.error || '保存失败', 'error');
134
207
  } catch (e) { toast('请求失败', 'error'); }
135
208
  setBtnLoading(btn, false);
136
209
  }
137
210
 
138
- /* ─── Tools ───────────────────────────────────────────────────── */
139
- async function loadReportedTools() {
211
+ /* ─── Tools & Skills ──────────────────────────────────────────── */
212
+ async function refreshTools() {
140
213
  const el = document.getElementById('reportedToolsList');
141
214
  if (!el) return;
142
215
  el.innerHTML = '<div class="loading-text">加载中...</div>';
216
+
143
217
  try {
144
- const d = await fetch('/api/tools').then(r => r.json());
145
- if (!d || !d.length) { el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">暂无工具</p>'; return; }
146
- let h = '<div class="table-wrap"><table><thead><tr><th>工具</th><th>来源</th><th>说明</th></tr></thead><tbody>';
147
- for (const t of d) {
148
- const src = t.source === 'builtin' ? '<span class="badge info">内置</span>' : '<span class="badge muted">MCP</span>';
149
- h += `<tr><td><code>${esc(t.name)}</code></td><td>${src}</td><td style="color:var(--text-muted);font-size:13px">${esc(t.description)}</td></tr>`;
218
+ const [toolsData, mcpData] = await Promise.all([
219
+ fetch('/api/tools').then(r => r.json()).catch(() => []),
220
+ fetch('/api/mcp').then(r => r.json()).catch(() => ({ servers: [] }))
221
+ ]);
222
+
223
+ const tools = Array.isArray(toolsData) ? toolsData : [];
224
+ const mcpServers = mcpData.servers || [];
225
+ const runningMcpServers = mcpServers.filter(s => s.running);
226
+
227
+ // 按来源分组
228
+ const builtinTools = tools.filter(t => t.source === 'builtin');
229
+ const mcpTools = tools.filter(t => t.source === 'mcp');
230
+
231
+ if (tools.length === 0 && runningMcpServers.length === 0) {
232
+ el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">暂无工具</p>';
233
+ updateBadgeCount('toolsCount', 0);
234
+ return;
235
+ }
236
+
237
+ let html = '';
238
+
239
+ // 内置工具
240
+ if (builtinTools.length > 0) {
241
+ html += `<div class="tools-section">
242
+ <div class="tools-section-title">内置工具 <span class="count">${builtinTools.length}</span></div>
243
+ <div class="tools-grid">`;
244
+ for (const t of builtinTools) {
245
+ html += `<div class="tool-item">
246
+ <div class="tool-name"><code>${esc(t.name)}</code></div>
247
+ <div class="tool-desc">${esc(t.description)}</div>
248
+ </div>`;
249
+ }
250
+ html += '</div></div>';
251
+ }
252
+
253
+ // MCP 工具
254
+ if (mcpTools.length > 0 || runningMcpServers.length > 0) {
255
+ html += `<div class="tools-section">
256
+ <div class="tools-section-title">MCP 工具 <span class="count">${mcpTools.length}</span></div>`;
257
+
258
+ if (mcpTools.length > 0) {
259
+ html += '<div class="tools-grid">';
260
+ for (const t of mcpTools) {
261
+ html += `<div class="tool-item">
262
+ <div class="tool-name"><code>${esc(t.name)}</code></div>
263
+ <div class="tool-desc">${esc(t.description)}</div>
264
+ </div>`;
265
+ }
266
+ html += '</div>';
267
+ } else {
268
+ html += '<p class="tools-empty">无运行中的 MCP 服务</p>';
269
+ }
270
+ html += '</div>';
150
271
  }
151
- el.innerHTML = h + '</tbody></table></div>';
152
- } catch (e) { el.textContent = '加载失败'; }
272
+
273
+ el.innerHTML = html;
274
+
275
+ // 更新 tab 计数
276
+ const toolsTabCount = document.getElementById('toolsTabCount');
277
+ if (toolsTabCount) toolsTabCount.textContent = tools.length;
278
+
279
+ // 更新侧边栏计数
280
+ updateBadgeCount('toolsCount', tools.length);
281
+
282
+ // 更新状态页
283
+ const toolCountEl = document.getElementById('statToolCount');
284
+ if (toolCountEl) toolCountEl.textContent = tools.length;
285
+
286
+ } catch (e) {
287
+ el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">加载失败</p>';
288
+ }
153
289
  }
154
290
 
155
- /* ─── Multi-blueprint ──────────────────────────────────────────── */
156
- let _bpData = [];
157
- let _bpActive = '';
291
+ async function loadReportedTools() {
292
+ await refreshTools();
293
+ }
158
294
 
159
- async function loadAllBlueprints() {
160
- const tabRow = document.getElementById('blueprintTabRow');
161
- const panels = document.getElementById('blueprintPanels');
162
- if (!tabRow || !panels) return;
163
- panels.innerHTML = '<div class="loading-text">加载中...</div>';
164
- tabRow.innerHTML = '';
295
+ async function loadReportedSkills() {
296
+ const el = document.getElementById('reportedSkillsList');
297
+ if (!el) return;
298
+ el.innerHTML = '<div class="loading-text">加载中...</div>';
165
299
  try {
166
- _bpData = await fetch('/api/blueprints').then(r => r.json());
167
- if (!_bpData || !_bpData.length) {
168
- panels.innerHTML = '<p style="color:var(--text-muted);font-size:14px">暂无蓝图</p>';
300
+ const d = await fetch('/api/skills').then(r => r.json());
301
+ const skills = Array.isArray(d) ? d : [];
302
+ if (skills.length === 0) {
303
+ el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">暂无技能</p>';
304
+ const tabCount = document.getElementById('skillsTabCount');
305
+ if (tabCount) tabCount.textContent = '0';
169
306
  return;
170
307
  }
171
- tabRow.innerHTML = _bpData.map((bp, i) =>
172
- `<button class="sub-tab${i===0?' active':''}" data-bp="${esc(bp.name)}" onclick="showBlueprintTab('${esc(bp.name)}')">${esc(bp.name)}</button>`
173
- ).join('');
174
- panels.innerHTML = _bpData.map((bp, i) =>
175
- `<div class="sub-panel${i===0?' active':''}" id="bp-panel-${esc(bp.name)}">${bp.content ? `<pre class="blueprint-content">${esc(bp.content)}</pre>` : '<p style="color:var(--text-muted);font-size:14px">无法读取内容</p>'}</div>`
176
- ).join('');
177
- _bpActive = _bpData[0]?.name || '';
178
- } catch (e) { panels.textContent = '加载失败'; }
179
- }
308
+ let html = '<div class="skills-grid">';
309
+ for (const s of skills) {
310
+ html += `<div class="skill-item">
311
+ <div class="skill-name">${esc(s.name)}</div>
312
+ <div class="skill-desc">${esc(s.description)}</div>
313
+ </div>`;
314
+ }
315
+ html += '</div>';
316
+ el.innerHTML = html;
180
317
 
181
- function showBlueprintTab(name) {
182
- const tabRow = document.getElementById('blueprintTabRow');
183
- if (!tabRow) return;
184
- tabRow.querySelectorAll('.sub-tab').forEach(b => b.classList.toggle('active', b.dataset.bp === name));
185
- document.querySelectorAll('#blueprintPanels .sub-panel').forEach(p => p.classList.toggle('active', p.id === 'bp-panel-' + name));
186
- _bpActive = name;
318
+ const tabCount = document.getElementById('skillsTabCount');
319
+ if (tabCount) tabCount.textContent = skills.length;
320
+ } catch (e) {
321
+ el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">加载失败</p>';
322
+ }
187
323
  }
188
324
 
189
325
  /* ─── MCP ─────────────────────────────────────────────────────── */
190
326
  async function loadMCP() {
191
327
  const el = document.getElementById('mcpList');
192
328
  if (!el) return;
329
+ el.innerHTML = '<div class="loading-text">加载中...</div>';
193
330
  try {
194
331
  const d = await fetch('/api/mcp').then(r => r.json());
195
- if (!d.servers || !d.servers.length) {
332
+ const servers = d.servers || [];
333
+
334
+ if (servers.length === 0) {
196
335
  el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">暂无 MCP 服务,点击「新增」添加</p>';
336
+ updateBadgeCount('mcpCount', 0);
337
+ const mcpCountEl = document.getElementById('statMcpCount');
338
+ if (mcpCountEl) mcpCountEl.textContent = '0';
197
339
  return;
198
340
  }
199
- let h = '<div class="table-wrap"><table><thead><tr><th>名称</th><th>传输</th><th>连接</th><th>状态</th><th>工具数</th><th>操作</th></tr></thead><tbody>';
200
- for (const s of d.servers) {
341
+
342
+ let html = '<div class="mcp-servers">';
343
+ for (const s of servers) {
201
344
  const transport = s.transport || 'stdio';
202
- const connInfo = transport === 'sse' ? `<code>${esc(s.url||'')}</code>` : `<code>${esc((s.command||'')+' '+(s.args||[]).join(' '))}</code>`;
203
- const statusBadge = s.running ? '<span class="badge on">运行中</span>' : '<span class="badge off">停止</span>';
204
- const transportBadge = transport==='sse' ? '<span class="badge info">SSE</span>' : '<span class="badge muted">stdio</span>';
205
- let actions = '';
206
- if (s.running) actions += `<button class="btn btn-secondary btn-sm" onclick="mcpStop('${esc(s.name)}')">停止</button> `;
207
- else actions += `<button class="btn btn-success btn-sm" onclick="mcpStart('${esc(s.name)}')">启动</button> `;
208
- actions += `<button class="btn btn-danger btn-sm" onclick="mcpDelete('${esc(s.name)}')">删除</button>`;
209
- h += `<tr><td><strong>${esc(s.name)}</strong></td><td>${transportBadge}</td><td style="max-width:180px;overflow:hidden">${connInfo}</td><td>${statusBadge}</td><td>${s.tools?.length||0}</td><td><div style="display:flex;gap:6px">${actions}</div></td></tr>`;
345
+ const connInfo = transport === 'sse'
346
+ ? `<code>${esc(s.url||'')}</code>`
347
+ : `<code>${esc((s.command||'')+' '+(s.args||[]).join(' '))}</code>`;
348
+ const statusClass = s.running ? 'running' : 'stopped';
349
+ const statusText = s.running ? '运行中' : '已停止';
350
+ const mcpToolsCount = s.tools?.length || 0;
351
+
352
+ html += `<div class="mcp-server-item ${statusClass}">
353
+ <div class="mcp-server-header">
354
+ <div class="mcp-server-name">${esc(s.name)}</div>
355
+ <div class="mcp-server-status">
356
+ <span class="status-dot ${statusClass}"></span>
357
+ ${statusText}
358
+ </div>
359
+ </div>
360
+ <div class="mcp-server-conn">${connInfo}</div>
361
+ <div class="mcp-server-footer">
362
+ <div class="mcp-server-tools">${mcpToolsCount} 个工具</div>
363
+ <div class="mcp-server-actions">
364
+ ${s.running
365
+ ? `<button class="btn btn-secondary btn-sm" onclick="mcpStop('${esc(s.name)}')">停止</button>`
366
+ : `<button class="btn btn-success btn-sm" onclick="mcpStart('${esc(s.name)}')">启动</button>`
367
+ }
368
+ <button class="btn btn-danger btn-sm" onclick="mcpDelete('${esc(s.name)}')">删除</button>
369
+ </div>
370
+ </div>
371
+ </div>`;
210
372
  }
211
- el.innerHTML = h + '</tbody></table></div>';
212
- } catch (e) { el.textContent = '加载失败'; }
373
+ html += '</div>';
374
+ el.innerHTML = html;
375
+
376
+ // 更新计数
377
+ const runningCount = servers.filter(s => s.running).length;
378
+ updateBadgeCount('mcpCount', runningCount);
379
+ const mcpCountEl = document.getElementById('statMcpCount');
380
+ if (mcpCountEl) mcpCountEl.textContent = runningCount;
381
+
382
+ } catch (e) {
383
+ el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">加载失败</p>';
384
+ }
213
385
  }
214
386
 
215
387
  function onMCPTransportChange() {
216
388
  const v = document.getElementById('mcpTransport').value;
217
- document.getElementById('mcpStdioFields').style.display = v==='stdio' ? '' : 'none';
218
- document.getElementById('mcpSSEFields').style.display = v==='sse' ? '' : 'none';
389
+ document.getElementById('mcpStdioFields').style.display = v === 'stdio' ? '' : 'none';
390
+ document.getElementById('mcpSSEFields').style.display = v === 'sse' ? '' : 'none';
219
391
  }
220
392
 
221
393
  function showMCPCreateModal() {
394
+ document.getElementById('mcpName').value = '';
222
395
  document.getElementById('mcpCommandArgsList').innerHTML = '';
223
396
  document.getElementById('mcpEnvList').innerHTML = '';
224
397
  document.getElementById('mcpTransport').value = 'stdio';
@@ -258,6 +431,7 @@ async function doCreateMCP() {
258
431
  if (k) env[k] = (row.querySelector('.mcp-env-val').value||'').trim();
259
432
  });
260
433
  if (!name) { toast('请填写名称', 'error'); return; }
434
+
261
435
  let payload;
262
436
  if (transport === 'sse') {
263
437
  const url = document.getElementById('mcpSSEUrl').value.trim();
@@ -269,16 +443,47 @@ async function doCreateMCP() {
269
443
  if (!command) { toast('请填写命令', 'error'); return; }
270
444
  payload = { action:'create', name, transport:'stdio', command, args:argvItems.slice(1), env };
271
445
  }
446
+
272
447
  try {
273
448
  const r = await fetch('/api/mcp', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }).then(r=>r.json());
274
- if (r.text) { hideModal('mcpCreateModal'); document.getElementById('mcpName').value=''; loadMCP(); toast(r.text, 'success'); }
275
- else toast(r.error || '添加失败', 'error');
449
+ if (r.text) {
450
+ hideModal('mcpCreateModal');
451
+ toast('MCP 服务已添加', 'success');
452
+ await loadMCP();
453
+ await refreshTools(); // 刷新工具列表
454
+ } else {
455
+ toast(r.error || '添加失败', 'error');
456
+ }
457
+ } catch (e) { toast('请求失败', 'error'); }
458
+ }
459
+
460
+ async function mcpStart(name) {
461
+ try {
462
+ const r = await fetch('/api/mcp', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'start', name})}).then(r=>r.json());
463
+ if (r.text) toast(r.text, 'success');
464
+ await loadMCP();
465
+ await refreshTools(); // 刷新工具列表
276
466
  } catch (e) { toast('请求失败', 'error'); }
277
467
  }
278
468
 
279
- async function mcpStart(name) { try { const r=await fetch('/api/mcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start',name})}).then(r=>r.json()); loadMCP(); if(r.text)toast(r.text,'success'); }catch(e){toast('请求失败','error');} }
280
- async function mcpStop(name) { try { await fetch('/api/mcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop',name})}); loadMCP(); }catch(e){toast('请求失败','error');} }
281
- async function mcpDelete(name) { if(!confirm('确定删除 MCP 服务「'+name+'」?'))return; try{await fetch('/api/mcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'delete',name})});loadMCP();}catch(e){toast('请求失败','error');} }
469
+ async function mcpStop(name) {
470
+ try {
471
+ await fetch('/api/mcp', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'stop', name})});
472
+ toast('MCP 服务已停止', 'info');
473
+ await loadMCP();
474
+ await refreshTools(); // 刷新工具列表
475
+ } catch (e) { toast('请求失败', 'error'); }
476
+ }
477
+
478
+ async function mcpDelete(name) {
479
+ if (!confirm(`确定删除 MCP 服务「${name}」?`)) return;
480
+ try {
481
+ await fetch('/api/mcp', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'delete', name})});
482
+ toast('MCP 服务已删除', 'success');
483
+ await loadMCP();
484
+ await refreshTools(); // 刷新工具列表
485
+ } catch (e) { toast('请求失败', 'error'); }
486
+ }
282
487
 
283
488
  /* ─── Platform configs ────────────────────────────────────────── */
284
489
  async function loadFeishuConfig() {
@@ -286,19 +491,28 @@ async function loadFeishuConfig() {
286
491
  const c = await fetch('/api/feishu/config').then(r=>r.json());
287
492
  document.getElementById('feishuAppId').value = c.appId||'';
288
493
  document.getElementById('feishuAppSecret').value = c.appSecret||'';
289
- document.getElementById('feishuStatusBadge').innerHTML = renderBadge(c.running, c.configured);
494
+ const badgeEl = document.getElementById('feishuStatusBadge');
495
+ if (badgeEl) badgeEl.innerHTML = renderBadge(c.running, c.configured);
290
496
  updateNavBadge('feishu', c.running, c.configured);
291
497
  } catch (e) {}
292
498
  }
499
+
293
500
  async function saveFeishuConfig() {
294
501
  const btn = document.getElementById('saveFeishuBtn');
295
- const data = { appId: document.getElementById('feishuAppId').value.trim(), appSecret: document.getElementById('feishuAppSecret').value };
502
+ const data = {
503
+ appId: document.getElementById('feishuAppId').value.trim(),
504
+ appSecret: document.getElementById('feishuAppSecret').value
505
+ };
296
506
  if (!data.appId) { toast('请填写 App ID', 'error'); return; }
297
507
  setBtnLoading(btn, true, '保存中...');
298
508
  try {
299
- const r = await fetch('/api/feishu/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}).then(r=>r.json());
300
- if (r.ok) { toast(r.message||'已保存', 'success'); setTimeout(loadFeishuConfig, 2000); }
301
- else toast(r.error||'保存失败', 'error');
509
+ const r = await fetch('/api/feishu/config', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) }).then(r=>r.json());
510
+ if (r.ok) {
511
+ toast('飞书配置已保存', 'success');
512
+ setTimeout(loadFeishuConfig, 2000);
513
+ } else {
514
+ toast(r.error||'保存失败', 'error');
515
+ }
302
516
  } catch (e) { toast('请求失败', 'error'); }
303
517
  setBtnLoading(btn, false);
304
518
  }
@@ -310,30 +524,33 @@ async function loadEnvCheck() {
310
524
  const el = document.getElementById('envCheckList');
311
525
  const row = document.getElementById('envInstallRow');
312
526
  if (!el) return;
527
+ el.innerHTML = '<div class="loading-text">加载中...</div>';
313
528
  try {
314
529
  const d = await fetch('/api/env/check').then(r=>r.json());
315
530
  const runtimes = d.runtimes || [];
316
531
  let hasAutoMissing = false;
317
- let h = '';
532
+ let h = '<div class="env-grid">';
318
533
  for (const r of runtimes) {
319
534
  if (r.canAutoInstall && !r.ok) hasAutoMissing = true;
320
535
  const icon = ENV_ICONS[r.id] || '📦';
321
- h += `<div class="env-item">
322
- <div class="env-icon ${r.ok?'ok':'fail'}">${icon}</div>
323
- <div class="env-body">
324
- <div class="env-name">${esc(r.name)} ${r.ok ? '<span class="badge on">已安装</span>' : '<span class="badge off">未安装</span>'}</div>
536
+ const statusClass = r.ok ? 'ok' : 'fail';
537
+ h += `<div class="env-item ${statusClass}">
538
+ <div class="env-icon">${icon}</div>
539
+ <div class="env-info">
540
+ <div class="env-name">${esc(r.name)}</div>
325
541
  <div class="env-desc">${esc(r.description)}</div>
326
542
  ${r.ok && r.detail ? `<div class="env-detail">${esc(r.detail)}</div>` : ''}
327
543
  </div>
328
- <div>
329
- ${!r.ok && r.hint ? `<button class="btn btn-secondary btn-sm env-hint-btn" data-name="${esc(r.name)}" data-hint="${esc(r.hint||'')}">安装教程</button>` : ''}
544
+ <div class="env-action">
545
+ ${r.ok ? `<span class="badge on">已安装</span>` : ''}
546
+ ${!r.ok && r.hint ? `<button class="btn btn-secondary btn-sm env-hint-btn" data-name="${esc(r.name)}" data-hint="${esc(r.hint||'')}">安装教程</button>` : ''}
330
547
  </div>
331
548
  </div>`;
332
549
  }
550
+ h += '</div>';
333
551
  el.innerHTML = h || '<p style="color:var(--text-muted);font-size:14px">暂无检测项</p>';
334
552
  if (row) row.style.display = hasAutoMissing ? '' : 'none';
335
553
 
336
- // 用事件委托处理「安装教程」按钮,避免在 onclick 属性里拼接含特殊字符的字符串
337
554
  el.querySelectorAll('.env-hint-btn').forEach(btn => {
338
555
  btn.addEventListener('click', () => {
339
556
  const name = btn.getAttribute('data-name') || '';
@@ -343,14 +560,17 @@ async function loadEnvCheck() {
343
560
  showModal('envHintModal');
344
561
  });
345
562
  });
346
- } catch (e) { el.textContent = '加载失败'; if (row) row.style.display='none'; }
563
+ } catch (e) {
564
+ el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">加载失败</p>';
565
+ if (row) row.style.display='none';
566
+ }
347
567
  }
348
568
 
349
569
  async function doEnvInstall() {
350
570
  const btn = document.getElementById('envInstallBtn');
351
571
  setBtnLoading(btn, true, '安装中...');
352
572
  try {
353
- const r = await fetch('/api/env/install',{method:'POST'}).then(x=>x.json());
573
+ const r = await fetch('/api/env/install', { method:'POST'}).then(x=>x.json());
354
574
  if (r.installed?.length) toast('已安装: ' + r.installed.join(', '), 'success');
355
575
  if (r.errors?.length) toast('安装失败: ' + r.errors.join('; '), 'error');
356
576
  loadEnvCheck();
@@ -361,17 +581,15 @@ async function doEnvInstall() {
361
581
  /* ─── Init ─────────────────────────────────────────────────────── */
362
582
  async function init() {
363
583
  await loadStatus();
364
- // 预加载通道状态用于侧边栏 badge
584
+ // 预加载飞书状态
365
585
  try {
366
- const [feishuCfg] = await Promise.allSettled([
367
- fetch('/api/feishu/config').then(r=>r.json()),
368
- ]);
369
- if (feishuCfg.status==='fulfilled') updateNavBadge('feishu', feishuCfg.value.running, feishuCfg.value.configured);
586
+ const feishuCfg = await fetch('/api/feishu/config').then(r=>r.json()).catch(() => ({}));
587
+ updateNavBadge('feishu', feishuCfg.running, feishuCfg.configured);
370
588
  } catch (e) {}
371
589
  }
372
590
 
373
591
  init();
374
- setInterval(loadStatus, 15000);
592
+ setInterval(loadStatus, 10000); // 状态轮询
375
593
 
376
594
  /* ─── Restore last nav on page load ────────────────────────── */
377
595
  (function restoreNav() {