@greatlhd/ailo-desktop 1.0.0
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/copy-static.mjs +11 -0
- package/dist/browser_control.js +767 -0
- package/dist/browser_snapshot.js +174 -0
- package/dist/cli.js +36 -0
- package/dist/code_executor.js +95 -0
- package/dist/config_server.js +658 -0
- package/dist/connection_util.js +14 -0
- package/dist/constants.js +2 -0
- package/dist/desktop_state_store.js +57 -0
- package/dist/desktop_types.js +1 -0
- package/dist/desktop_verifier.js +40 -0
- package/dist/dingtalk-handler.js +173 -0
- package/dist/dingtalk-types.js +1 -0
- package/dist/email_handler.js +501 -0
- package/dist/exec_tool.js +90 -0
- package/dist/feishu-handler.js +620 -0
- package/dist/feishu-types.js +8 -0
- package/dist/feishu-utils.js +162 -0
- package/dist/fs_tools.js +398 -0
- package/dist/index.js +433 -0
- package/dist/mcp/config-manager.js +64 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/rpc.js +109 -0
- package/dist/mcp/session.js +140 -0
- package/dist/mcp_manager.js +253 -0
- package/dist/mouse_keyboard.js +516 -0
- package/dist/qq-handler.js +153 -0
- package/dist/qq-types.js +15 -0
- package/dist/qq-ws.js +178 -0
- package/dist/screenshot.js +271 -0
- package/dist/skills_hub.js +212 -0
- package/dist/skills_manager.js +103 -0
- package/dist/static/AGENTS.md +25 -0
- package/dist/static/app.css +539 -0
- package/dist/static/app.html +292 -0
- package/dist/static/app.js +380 -0
- package/dist/static/chat.html +994 -0
- package/dist/time_tool.js +22 -0
- package/dist/utils.js +15 -0
- package/package.json +38 -0
- package/src/browser_control.ts +739 -0
- package/src/browser_snapshot.ts +196 -0
- package/src/cli.ts +44 -0
- package/src/code_executor.ts +101 -0
- package/src/config_server.ts +723 -0
- package/src/connection_util.ts +23 -0
- package/src/constants.ts +2 -0
- package/src/desktop_state_store.ts +64 -0
- package/src/desktop_types.ts +44 -0
- package/src/desktop_verifier.ts +45 -0
- package/src/dingtalk-types.ts +26 -0
- package/src/exec_tool.ts +93 -0
- package/src/feishu-handler.ts +722 -0
- package/src/feishu-types.ts +66 -0
- package/src/feishu-utils.ts +174 -0
- package/src/fs_tools.ts +411 -0
- package/src/index.ts +474 -0
- package/src/mcp/config-manager.ts +85 -0
- package/src/mcp/index.ts +7 -0
- package/src/mcp/rpc.ts +131 -0
- package/src/mcp/session.ts +182 -0
- package/src/mcp_manager.ts +273 -0
- package/src/mouse_keyboard.ts +526 -0
- package/src/qq-types.ts +49 -0
- package/src/qq-ws.ts +223 -0
- package/src/screenshot.ts +297 -0
- package/src/static/app.css +539 -0
- package/src/static/app.html +292 -0
- package/src/static/app.js +380 -0
- package/src/static/chat.html +994 -0
- package/src/time_tool.ts +24 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/* ─── Runtime config injected by server ───────────────────────── */
|
|
2
|
+
const SHOW_CONNECTION_FORM = window.SHOW_CONNECTION_FORM === true;
|
|
3
|
+
|
|
4
|
+
/* ─── Toast ───────────────────────────────────────────────────── */
|
|
5
|
+
const ICONS = {
|
|
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
|
+
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
|
+
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
|
+
};
|
|
10
|
+
|
|
11
|
+
function toast(msg, type = 'info', duration = 3500) {
|
|
12
|
+
const c = document.getElementById('toast-container');
|
|
13
|
+
const el = document.createElement('div');
|
|
14
|
+
el.className = `toast ${type}`;
|
|
15
|
+
el.innerHTML = `${ICONS[type] || ICONS.info}<span>${esc(msg)}</span>`;
|
|
16
|
+
c.appendChild(el);
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
el.classList.add('hiding');
|
|
19
|
+
setTimeout(() => el.remove(), 250);
|
|
20
|
+
}, duration);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ─── Utils ───────────────────────────────────────────────────── */
|
|
24
|
+
function esc(s) {
|
|
25
|
+
if (s == null) return '';
|
|
26
|
+
const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setBtnLoading(btn, loading, text) {
|
|
30
|
+
if (!btn) return;
|
|
31
|
+
if (loading) {
|
|
32
|
+
btn.disabled = true;
|
|
33
|
+
btn._origHTML = btn.innerHTML;
|
|
34
|
+
btn.innerHTML = `<span class="spinner"></span>${text || '处理中...'}`;
|
|
35
|
+
} else {
|
|
36
|
+
btn.disabled = false;
|
|
37
|
+
if (btn._origHTML) { btn.innerHTML = btn._origHTML; btn._origHTML = null; }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderBadge(running, configured) {
|
|
42
|
+
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
|
+
return '<span class="badge muted">未配置</span>';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateNavBadge(id, running, configured) {
|
|
48
|
+
const el = document.getElementById(id + 'NavBadge');
|
|
49
|
+
if (!el) return;
|
|
50
|
+
if (running) { el.style.display=''; el.className='nav-badge on'; el.textContent='运行中'; }
|
|
51
|
+
else if (configured) { el.style.display=''; el.className='nav-badge warning'; el.textContent='已配置'; }
|
|
52
|
+
else { el.style.display='none'; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ─── Navigation ──────────────────────────────────────────────── */
|
|
56
|
+
function nav(name) {
|
|
57
|
+
document.querySelectorAll('.nav-item').forEach(el => el.classList.toggle('active', el.dataset.panel === name));
|
|
58
|
+
document.querySelectorAll('.panel').forEach(el => el.classList.remove('active'));
|
|
59
|
+
const panel = document.getElementById('panel-' + name);
|
|
60
|
+
if (panel) panel.classList.add('active');
|
|
61
|
+
localStorage.setItem('ailo_nav', name);
|
|
62
|
+
|
|
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();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toolsSub(sub) {
|
|
71
|
+
document.querySelectorAll('.sub-tabs:not(.blueprint-tabs) .sub-tab').forEach(b => b.classList.toggle('active', b.dataset.sub === sub));
|
|
72
|
+
document.querySelectorAll('#panel-tools .sub-panel').forEach(p => p.classList.toggle('active', p.id === 'sub-' + sub));
|
|
73
|
+
localStorage.setItem('ailo_tools_sub', sub);
|
|
74
|
+
if (sub === 'reported') loadReportedTools();
|
|
75
|
+
if (sub === 'blueprints') loadAllBlueprints();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hideModal(id) { document.getElementById(id).classList.remove('open'); }
|
|
79
|
+
function showModal(id) { document.getElementById(id).classList.add('open'); }
|
|
80
|
+
|
|
81
|
+
/* ─── Status ──────────────────────────────────────────────────── */
|
|
82
|
+
async function loadStatus() {
|
|
83
|
+
try {
|
|
84
|
+
const s = await fetch('/api/status').then(r => r.json());
|
|
85
|
+
const dot = document.getElementById('globalDot');
|
|
86
|
+
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
|
+
if (dot) dot.className = 'status-dot ' + (s.connected ? 'on' : 'off');
|
|
93
|
+
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>';
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadConnectionForm() {
|
|
110
|
+
const card = document.getElementById('connectionFormCard');
|
|
111
|
+
if (!SHOW_CONNECTION_FORM || !card) return;
|
|
112
|
+
card.style.display = '';
|
|
113
|
+
try {
|
|
114
|
+
const c = await fetch('/api/connection').then(r => r.json());
|
|
115
|
+
document.getElementById('connWsUrl').value = c.ailoWsUrl || '';
|
|
116
|
+
document.getElementById('connApiKey').value = c.ailoApiKey || '';
|
|
117
|
+
document.getElementById('connEndpointId').value = c.endpointId || '';
|
|
118
|
+
} catch (e) {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function saveConnection() {
|
|
122
|
+
const btn = document.getElementById('saveConnBtn');
|
|
123
|
+
const wsUrl = document.getElementById('connWsUrl').value.trim();
|
|
124
|
+
const apiKey = document.getElementById('connApiKey').value.trim();
|
|
125
|
+
const endpointId = document.getElementById('connEndpointId').value.trim();
|
|
126
|
+
if (!wsUrl || !apiKey || !endpointId) { toast('请填写全部三项连接信息', 'error'); return; }
|
|
127
|
+
setBtnLoading(btn, true, '保存中...');
|
|
128
|
+
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());
|
|
130
|
+
if (r.ok) {
|
|
131
|
+
toast(r.message || '已保存', 'success');
|
|
132
|
+
let cnt = 0; const iv = setInterval(() => { loadStatus(); if (++cnt >= 5) clearInterval(iv); }, 2000);
|
|
133
|
+
} else toast(r.error || '保存失败', 'error');
|
|
134
|
+
} catch (e) { toast('请求失败', 'error'); }
|
|
135
|
+
setBtnLoading(btn, false);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ─── Tools ───────────────────────────────────────────────────── */
|
|
139
|
+
async function loadReportedTools() {
|
|
140
|
+
const el = document.getElementById('reportedToolsList');
|
|
141
|
+
if (!el) return;
|
|
142
|
+
el.innerHTML = '<div class="loading-text">加载中...</div>';
|
|
143
|
+
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>`;
|
|
150
|
+
}
|
|
151
|
+
el.innerHTML = h + '</tbody></table></div>';
|
|
152
|
+
} catch (e) { el.textContent = '加载失败'; }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ─── Multi-blueprint ──────────────────────────────────────────── */
|
|
156
|
+
let _bpData = [];
|
|
157
|
+
let _bpActive = '';
|
|
158
|
+
|
|
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 = '';
|
|
165
|
+
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>';
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
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
|
+
}
|
|
180
|
+
|
|
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;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ─── MCP ─────────────────────────────────────────────────────── */
|
|
190
|
+
async function loadMCP() {
|
|
191
|
+
const el = document.getElementById('mcpList');
|
|
192
|
+
if (!el) return;
|
|
193
|
+
try {
|
|
194
|
+
const d = await fetch('/api/mcp').then(r => r.json());
|
|
195
|
+
if (!d.servers || !d.servers.length) {
|
|
196
|
+
el.innerHTML = '<p style="color:var(--text-muted);font-size:14px">暂无 MCP 服务,点击「新增」添加</p>';
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
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) {
|
|
201
|
+
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>`;
|
|
210
|
+
}
|
|
211
|
+
el.innerHTML = h + '</tbody></table></div>';
|
|
212
|
+
} catch (e) { el.textContent = '加载失败'; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function onMCPTransportChange() {
|
|
216
|
+
const v = document.getElementById('mcpTransport').value;
|
|
217
|
+
document.getElementById('mcpStdioFields').style.display = v==='stdio' ? '' : 'none';
|
|
218
|
+
document.getElementById('mcpSSEFields').style.display = v==='sse' ? '' : 'none';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function showMCPCreateModal() {
|
|
222
|
+
document.getElementById('mcpCommandArgsList').innerHTML = '';
|
|
223
|
+
document.getElementById('mcpEnvList').innerHTML = '';
|
|
224
|
+
document.getElementById('mcpTransport').value = 'stdio';
|
|
225
|
+
document.getElementById('mcpSSEUrl').value = '';
|
|
226
|
+
onMCPTransportChange();
|
|
227
|
+
addMCPArgvRow('npx'); addMCPArgvRow('-y'); addMCPArgvRow('@modelcontextprotocol/server-filesystem');
|
|
228
|
+
addMCPEnvRow('', '');
|
|
229
|
+
showModal('mcpCreateModal');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function addMCPArgvRow(val) {
|
|
233
|
+
const list = document.getElementById('mcpCommandArgsList');
|
|
234
|
+
const n = list.querySelectorAll('.mcp-argv-row').length + 1;
|
|
235
|
+
const row = document.createElement('div');
|
|
236
|
+
row.className = 'mcp-argv-row input-row';
|
|
237
|
+
row.innerHTML = `<input type="text" class="mcp-argv-item" placeholder="${n===1?'命令(如 npx)':'参数'}" style="flex:1"><button type="button" class="remove-btn" onclick="this.closest('.mcp-argv-row').remove()">✕</button>`;
|
|
238
|
+
list.appendChild(row);
|
|
239
|
+
row.querySelector('.mcp-argv-item').value = val || '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function addMCPEnvRow(k, v) {
|
|
243
|
+
const list = document.getElementById('mcpEnvList');
|
|
244
|
+
const row = document.createElement('div');
|
|
245
|
+
row.className = 'mcp-env-row input-row';
|
|
246
|
+
row.innerHTML = `<input type="text" class="mcp-env-key" placeholder="KEY" style="width:120px"><input type="text" class="mcp-env-val" placeholder="值" style="flex:1"><button type="button" class="remove-btn" onclick="this.closest('.mcp-env-row').remove()">✕</button>`;
|
|
247
|
+
list.appendChild(row);
|
|
248
|
+
row.querySelector('.mcp-env-key').value = k || '';
|
|
249
|
+
row.querySelector('.mcp-env-val').value = v || '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function doCreateMCP() {
|
|
253
|
+
const name = document.getElementById('mcpName').value.trim();
|
|
254
|
+
const transport = document.getElementById('mcpTransport').value;
|
|
255
|
+
const env = {};
|
|
256
|
+
document.querySelectorAll('.mcp-env-row').forEach(row => {
|
|
257
|
+
const k = (row.querySelector('.mcp-env-key').value||'').trim();
|
|
258
|
+
if (k) env[k] = (row.querySelector('.mcp-env-val').value||'').trim();
|
|
259
|
+
});
|
|
260
|
+
if (!name) { toast('请填写名称', 'error'); return; }
|
|
261
|
+
let payload;
|
|
262
|
+
if (transport === 'sse') {
|
|
263
|
+
const url = document.getElementById('mcpSSEUrl').value.trim();
|
|
264
|
+
if (!url) { toast('请填写服务器 URL', 'error'); return; }
|
|
265
|
+
payload = { action:'create', name, transport:'sse', url, env };
|
|
266
|
+
} else {
|
|
267
|
+
const argvItems = Array.from(document.querySelectorAll('.mcp-argv-item')).map(e=>e.value.trim()).filter(Boolean);
|
|
268
|
+
const command = argvItems[0]||'';
|
|
269
|
+
if (!command) { toast('请填写命令', 'error'); return; }
|
|
270
|
+
payload = { action:'create', name, transport:'stdio', command, args:argvItems.slice(1), env };
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
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');
|
|
276
|
+
} catch (e) { toast('请求失败', 'error'); }
|
|
277
|
+
}
|
|
278
|
+
|
|
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');} }
|
|
282
|
+
|
|
283
|
+
/* ─── Platform configs ────────────────────────────────────────── */
|
|
284
|
+
async function loadFeishuConfig() {
|
|
285
|
+
try {
|
|
286
|
+
const c = await fetch('/api/feishu/config').then(r=>r.json());
|
|
287
|
+
document.getElementById('feishuAppId').value = c.appId||'';
|
|
288
|
+
document.getElementById('feishuAppSecret').value = c.appSecret||'';
|
|
289
|
+
document.getElementById('feishuStatusBadge').innerHTML = renderBadge(c.running, c.configured);
|
|
290
|
+
updateNavBadge('feishu', c.running, c.configured);
|
|
291
|
+
} catch (e) {}
|
|
292
|
+
}
|
|
293
|
+
async function saveFeishuConfig() {
|
|
294
|
+
const btn = document.getElementById('saveFeishuBtn');
|
|
295
|
+
const data = { appId: document.getElementById('feishuAppId').value.trim(), appSecret: document.getElementById('feishuAppSecret').value };
|
|
296
|
+
if (!data.appId) { toast('请填写 App ID', 'error'); return; }
|
|
297
|
+
setBtnLoading(btn, true, '保存中...');
|
|
298
|
+
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');
|
|
302
|
+
} catch (e) { toast('请求失败', 'error'); }
|
|
303
|
+
setBtnLoading(btn, false);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ─── Env Check ────────────────────────────────────────────────── */
|
|
307
|
+
const ENV_ICONS = { node:'⬡', python:'🐍', playwright:'🎭' };
|
|
308
|
+
|
|
309
|
+
async function loadEnvCheck() {
|
|
310
|
+
const el = document.getElementById('envCheckList');
|
|
311
|
+
const row = document.getElementById('envInstallRow');
|
|
312
|
+
if (!el) return;
|
|
313
|
+
try {
|
|
314
|
+
const d = await fetch('/api/env/check').then(r=>r.json());
|
|
315
|
+
const runtimes = d.runtimes || [];
|
|
316
|
+
let hasAutoMissing = false;
|
|
317
|
+
let h = '';
|
|
318
|
+
for (const r of runtimes) {
|
|
319
|
+
if (r.canAutoInstall && !r.ok) hasAutoMissing = true;
|
|
320
|
+
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>
|
|
325
|
+
<div class="env-desc">${esc(r.description)}</div>
|
|
326
|
+
${r.ok && r.detail ? `<div class="env-detail">${esc(r.detail)}</div>` : ''}
|
|
327
|
+
</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>` : ''}
|
|
330
|
+
</div>
|
|
331
|
+
</div>`;
|
|
332
|
+
}
|
|
333
|
+
el.innerHTML = h || '<p style="color:var(--text-muted);font-size:14px">暂无检测项</p>';
|
|
334
|
+
if (row) row.style.display = hasAutoMissing ? '' : 'none';
|
|
335
|
+
|
|
336
|
+
// 用事件委托处理「安装教程」按钮,避免在 onclick 属性里拼接含特殊字符的字符串
|
|
337
|
+
el.querySelectorAll('.env-hint-btn').forEach(btn => {
|
|
338
|
+
btn.addEventListener('click', () => {
|
|
339
|
+
const name = btn.getAttribute('data-name') || '';
|
|
340
|
+
const hint = btn.getAttribute('data-hint') || '';
|
|
341
|
+
document.getElementById('envHintModalTitle').textContent = name + ' 安装说明';
|
|
342
|
+
document.getElementById('envHintModalContent').textContent = hint;
|
|
343
|
+
showModal('envHintModal');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
} catch (e) { el.textContent = '加载失败'; if (row) row.style.display='none'; }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function doEnvInstall() {
|
|
350
|
+
const btn = document.getElementById('envInstallBtn');
|
|
351
|
+
setBtnLoading(btn, true, '安装中...');
|
|
352
|
+
try {
|
|
353
|
+
const r = await fetch('/api/env/install',{method:'POST'}).then(x=>x.json());
|
|
354
|
+
if (r.installed?.length) toast('已安装: ' + r.installed.join(', '), 'success');
|
|
355
|
+
if (r.errors?.length) toast('安装失败: ' + r.errors.join('; '), 'error');
|
|
356
|
+
loadEnvCheck();
|
|
357
|
+
} catch (e) { toast('请求失败', 'error'); }
|
|
358
|
+
setBtnLoading(btn, false);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* ─── Init ─────────────────────────────────────────────────────── */
|
|
362
|
+
async function init() {
|
|
363
|
+
await loadStatus();
|
|
364
|
+
// 预加载通道状态用于侧边栏 badge
|
|
365
|
+
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);
|
|
370
|
+
} catch (e) {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
init();
|
|
374
|
+
setInterval(loadStatus, 15000);
|
|
375
|
+
|
|
376
|
+
/* ─── Restore last nav on page load ────────────────────────── */
|
|
377
|
+
(function restoreNav() {
|
|
378
|
+
const saved = localStorage.getItem('ailo_nav');
|
|
379
|
+
if (saved && document.getElementById('panel-' + saved)) nav(saved);
|
|
380
|
+
})();
|