@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.
- package/dist/cli.js +31 -8
- package/dist/config_server.js +14 -162
- package/dist/index.js +207 -173
- package/dist/static/app.css +252 -13
- package/dist/static/app.html +71 -84
- package/dist/static/app.js +326 -108
- package/package.json +3 -9
- package/src/cli.ts +35 -8
- package/src/config_server.ts +22 -171
- package/src/index.ts +221 -177
- package/src/static/app.css +252 -13
- package/src/static/app.html +71 -84
- package/src/static/app.js +326 -108
- package/src/dingtalk-types.ts +0 -26
- package/src/qq-types.ts +0 -49
- package/src/qq-ws.ts +0 -223
package/dist/static/app.js
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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('
|
|
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 === '
|
|
75
|
-
if (sub === '
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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 || '
|
|
132
|
-
let cnt = 0;
|
|
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
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
291
|
+
async function loadReportedTools() {
|
|
292
|
+
await refreshTools();
|
|
293
|
+
}
|
|
158
294
|
|
|
159
|
-
async function
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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'
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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) {
|
|
275
|
-
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
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')
|
|
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 = {
|
|
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) {
|
|
301
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
<div class="env-
|
|
324
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
-
//
|
|
584
|
+
// 预加载飞书状态
|
|
365
585
|
try {
|
|
366
|
-
const
|
|
367
|
-
|
|
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,
|
|
592
|
+
setInterval(loadStatus, 10000); // 状态轮询
|
|
375
593
|
|
|
376
594
|
/* ─── Restore last nav on page load ────────────────────────── */
|
|
377
595
|
(function restoreNav() {
|