@hirohsu/user-web-feedback 2.6.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/LICENSE +21 -0
- package/README.md +953 -0
- package/dist/cli.cjs +95778 -0
- package/dist/index.cjs +92818 -0
- package/dist/static/app.js +385 -0
- package/dist/static/components/navbar.css +406 -0
- package/dist/static/components/navbar.html +49 -0
- package/dist/static/components/navbar.js +211 -0
- package/dist/static/dashboard.css +495 -0
- package/dist/static/dashboard.html +95 -0
- package/dist/static/dashboard.js +540 -0
- package/dist/static/favicon.svg +27 -0
- package/dist/static/index.html +541 -0
- package/dist/static/logs.html +376 -0
- package/dist/static/logs.js +442 -0
- package/dist/static/mcp-settings.html +797 -0
- package/dist/static/mcp-settings.js +884 -0
- package/dist/static/modules/app-core.js +124 -0
- package/dist/static/modules/conversation-panel.js +247 -0
- package/dist/static/modules/feedback-handler.js +1420 -0
- package/dist/static/modules/image-handler.js +155 -0
- package/dist/static/modules/log-viewer.js +296 -0
- package/dist/static/modules/mcp-manager.js +474 -0
- package/dist/static/modules/prompt-manager.js +364 -0
- package/dist/static/modules/settings-manager.js +299 -0
- package/dist/static/modules/socket-manager.js +170 -0
- package/dist/static/modules/state-manager.js +352 -0
- package/dist/static/modules/timer-controller.js +243 -0
- package/dist/static/modules/ui-helpers.js +246 -0
- package/dist/static/settings.html +355 -0
- package/dist/static/settings.js +425 -0
- package/dist/static/socket.io.min.js +7 -0
- package/dist/static/style.css +2157 -0
- package/dist/static/terminals.html +357 -0
- package/dist/static/terminals.js +321 -0
- package/package.json +91 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Settings 前端邏輯
|
|
3
|
+
* 負責 MCP Server 的管理和工具配置
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const API_BASE = "";
|
|
10
|
+
|
|
11
|
+
// DOM 元素
|
|
12
|
+
const elements = {
|
|
13
|
+
serverList: document.getElementById("serverList"),
|
|
14
|
+
emptyState: document.getElementById("emptyState"),
|
|
15
|
+
serverModal: document.getElementById("serverModal"),
|
|
16
|
+
modalTitle: document.getElementById("modalTitle"),
|
|
17
|
+
serverForm: document.getElementById("serverForm"),
|
|
18
|
+
serverId: document.getElementById("serverId"),
|
|
19
|
+
serverName: document.getElementById("serverName"),
|
|
20
|
+
serverTransport: document.getElementById("serverTransport"),
|
|
21
|
+
serverCommand: document.getElementById("serverCommand"),
|
|
22
|
+
serverArgs: document.getElementById("serverArgs"),
|
|
23
|
+
serverEnv: document.getElementById("serverEnv"),
|
|
24
|
+
serverUrl: document.getElementById("serverUrl"),
|
|
25
|
+
serverDeferredStartup: document.getElementById("serverDeferredStartup"),
|
|
26
|
+
serverStartupArgsTemplate: document.getElementById(
|
|
27
|
+
"serverStartupArgsTemplate"
|
|
28
|
+
),
|
|
29
|
+
deferredFields: document.getElementById("deferredFields"),
|
|
30
|
+
stdioFields: document.getElementById("stdioFields"),
|
|
31
|
+
httpFields: document.getElementById("httpFields"),
|
|
32
|
+
addServerBtn: document.getElementById("addServerBtn"),
|
|
33
|
+
connectAllBtn: document.getElementById("connectAllBtn"),
|
|
34
|
+
disconnectAllBtn: document.getElementById("disconnectAllBtn"),
|
|
35
|
+
closeModal: document.getElementById("closeModal"),
|
|
36
|
+
cancelBtn: document.getElementById("cancelBtn"),
|
|
37
|
+
saveBtn: document.getElementById("saveBtn"),
|
|
38
|
+
createSerenaBtn: document.getElementById("createSerenaBtn"),
|
|
39
|
+
serenaProjectPath: document.getElementById("serenaProjectPath"),
|
|
40
|
+
loadingOverlay: document.getElementById("loadingOverlay"),
|
|
41
|
+
toastContainer: document.getElementById("toastContainer"),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// 狀態
|
|
45
|
+
let servers = [];
|
|
46
|
+
let socket = null;
|
|
47
|
+
|
|
48
|
+
// 初始化
|
|
49
|
+
function init() {
|
|
50
|
+
loadServers();
|
|
51
|
+
initEventListeners();
|
|
52
|
+
initSocketEvents();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 初始化 Socket.IO 事件
|
|
56
|
+
function initSocketEvents() {
|
|
57
|
+
if (typeof io === "undefined") {
|
|
58
|
+
console.warn("Socket.IO not available");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
socket = io();
|
|
63
|
+
|
|
64
|
+
socket.on("connect", () => {
|
|
65
|
+
console.log("Socket.IO connected");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
socket.on("mcp:server_connected", (data) => {
|
|
69
|
+
console.log("MCP Server connected:", data);
|
|
70
|
+
showToast(`${data.serverName} 已連接`, "success");
|
|
71
|
+
loadServers();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
socket.on("mcp:server_disconnected", (data) => {
|
|
75
|
+
console.log("MCP Server disconnected:", data);
|
|
76
|
+
if (data.reason === "unexpected") {
|
|
77
|
+
showToast(`⚠️ ${data.serverName} 意外斷開`, "warning");
|
|
78
|
+
}
|
|
79
|
+
loadServers();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
socket.on("mcp:server_error", (data) => {
|
|
83
|
+
console.error("MCP Server error:", data);
|
|
84
|
+
showToast(`❌ ${data.serverName} 錯誤: ${data.error}`, "error");
|
|
85
|
+
loadServers();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
socket.on("mcp:server_reconnecting", (data) => {
|
|
89
|
+
console.log("MCP Server reconnecting:", data);
|
|
90
|
+
showToast(
|
|
91
|
+
`🔄 ${data.serverName} 正在重連 (${data.attempt}/${data.maxAttempts})`,
|
|
92
|
+
"info"
|
|
93
|
+
);
|
|
94
|
+
loadServers();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
socket.on("mcp:server_state_changed", (data) => {
|
|
98
|
+
console.log("MCP Server state changed:", data);
|
|
99
|
+
loadServers();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 載入 Server 列表
|
|
104
|
+
async function loadServers() {
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(`${API_BASE}/api/mcp-servers`);
|
|
107
|
+
const data = await response.json();
|
|
108
|
+
|
|
109
|
+
if (data.success) {
|
|
110
|
+
servers = data.servers || [];
|
|
111
|
+
renderServers();
|
|
112
|
+
} else {
|
|
113
|
+
showToast("載入 Server 列表失敗", "error");
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Failed to load servers:", error);
|
|
117
|
+
showToast("載入 Server 列表失敗", "error");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 渲染 Server 列表
|
|
122
|
+
function renderServers() {
|
|
123
|
+
if (servers.length === 0) {
|
|
124
|
+
elements.serverList.innerHTML = "";
|
|
125
|
+
elements.emptyState.style.display = "block";
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
elements.emptyState.style.display = "none";
|
|
130
|
+
elements.serverList.innerHTML = servers
|
|
131
|
+
.map((server) => renderServerCard(server))
|
|
132
|
+
.join("");
|
|
133
|
+
|
|
134
|
+
// 綁定事件
|
|
135
|
+
bindServerEvents();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 渲染單個 Server 卡片
|
|
139
|
+
function renderServerCard(server) {
|
|
140
|
+
const state = server.state || {
|
|
141
|
+
status: "disconnected",
|
|
142
|
+
tools: [],
|
|
143
|
+
resources: [],
|
|
144
|
+
prompts: [],
|
|
145
|
+
};
|
|
146
|
+
const statusClass = state.status;
|
|
147
|
+
const statusText = getStatusText(state.status);
|
|
148
|
+
const tools = state.tools || [];
|
|
149
|
+
const isReconnecting = state.status === "reconnecting";
|
|
150
|
+
const hasError = state.status === "error" || state.lastError;
|
|
151
|
+
const isDeferred = server.deferredStartup;
|
|
152
|
+
const isDeferredWaiting =
|
|
153
|
+
isDeferred && state.status === "disconnected" && !hasError;
|
|
154
|
+
|
|
155
|
+
return `
|
|
156
|
+
<div class="server-card ${statusClass} ${
|
|
157
|
+
isDeferred ? "deferred" : ""
|
|
158
|
+
}" data-server-id="${server.id}">
|
|
159
|
+
<div class="server-header">
|
|
160
|
+
<div class="server-info">
|
|
161
|
+
<span class="server-name">${escapeHtml(
|
|
162
|
+
server.name
|
|
163
|
+
)}</span>
|
|
164
|
+
${
|
|
165
|
+
isDeferred
|
|
166
|
+
? '<span class="server-badge deferred-badge">⏳ 延遲啟動</span>'
|
|
167
|
+
: ""
|
|
168
|
+
}
|
|
169
|
+
<span class="server-status ${statusClass}">
|
|
170
|
+
<span class="status-dot"></span>
|
|
171
|
+
${isDeferredWaiting ? "等待專案資訊" : statusText}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="server-actions">
|
|
175
|
+
${
|
|
176
|
+
state.status === "connected"
|
|
177
|
+
? `<button class="btn btn-secondary btn-disconnect" data-id="${server.id}">斷開</button>`
|
|
178
|
+
: isReconnecting
|
|
179
|
+
? `<button class="btn btn-warning btn-cancel-reconnect" data-id="${server.id}">取消重連</button>`
|
|
180
|
+
: isDeferredWaiting
|
|
181
|
+
? ""
|
|
182
|
+
: `<button class="btn btn-success btn-connect" data-id="${server.id}">連接</button>`
|
|
183
|
+
}
|
|
184
|
+
${
|
|
185
|
+
hasError && !isReconnecting
|
|
186
|
+
? `<button class="btn btn-primary btn-retry" data-id="${server.id}">🔄 重試</button>`
|
|
187
|
+
: ""
|
|
188
|
+
}
|
|
189
|
+
<button class="btn btn-secondary btn-edit" data-id="${
|
|
190
|
+
server.id
|
|
191
|
+
}">編輯</button>
|
|
192
|
+
<button class="btn btn-danger btn-delete" data-id="${
|
|
193
|
+
server.id
|
|
194
|
+
}">刪除</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="server-body">
|
|
198
|
+
<div class="server-details">
|
|
199
|
+
<div class="detail-item">
|
|
200
|
+
<span class="detail-label">傳輸方式</span>
|
|
201
|
+
<span class="detail-value">${
|
|
202
|
+
server.transport
|
|
203
|
+
}</span>
|
|
204
|
+
</div>
|
|
205
|
+
${
|
|
206
|
+
server.transport === "stdio"
|
|
207
|
+
? `
|
|
208
|
+
<div class="detail-item">
|
|
209
|
+
<span class="detail-label">命令</span>
|
|
210
|
+
<span class="detail-value">${escapeHtml(
|
|
211
|
+
server.command || "-"
|
|
212
|
+
)}</span>
|
|
213
|
+
</div>
|
|
214
|
+
${
|
|
215
|
+
server.args && server.args.length > 0
|
|
216
|
+
? `
|
|
217
|
+
<div class="detail-item">
|
|
218
|
+
<span class="detail-label">參數</span>
|
|
219
|
+
<span class="detail-value">${escapeHtml(
|
|
220
|
+
server.args.join(" ")
|
|
221
|
+
)}</span>
|
|
222
|
+
</div>
|
|
223
|
+
`
|
|
224
|
+
: ""
|
|
225
|
+
}
|
|
226
|
+
`
|
|
227
|
+
: `
|
|
228
|
+
<div class="detail-item">
|
|
229
|
+
<span class="detail-label">URL</span>
|
|
230
|
+
<span class="detail-value">${escapeHtml(
|
|
231
|
+
server.url || "-"
|
|
232
|
+
)}</span>
|
|
233
|
+
</div>
|
|
234
|
+
`
|
|
235
|
+
}
|
|
236
|
+
${
|
|
237
|
+
isDeferred && server.startupArgsTemplate
|
|
238
|
+
? `
|
|
239
|
+
<div class="detail-item">
|
|
240
|
+
<span class="detail-label">啟動參數範本</span>
|
|
241
|
+
<span class="detail-value">${escapeHtml(
|
|
242
|
+
server.startupArgsTemplate
|
|
243
|
+
.split("\n")
|
|
244
|
+
.join(" ")
|
|
245
|
+
)}</span>
|
|
246
|
+
</div>
|
|
247
|
+
`
|
|
248
|
+
: ""
|
|
249
|
+
}
|
|
250
|
+
${
|
|
251
|
+
hasError
|
|
252
|
+
? `
|
|
253
|
+
<div class="error-section" style="grid-column: 1 / -1;">
|
|
254
|
+
<div class="detail-item">
|
|
255
|
+
<span class="detail-label" style="color: #ef4444;">⚠️ 錯誤</span>
|
|
256
|
+
<span class="detail-value" style="color: #ef4444;">${escapeHtml(
|
|
257
|
+
state.error || state.lastError
|
|
258
|
+
)}</span>
|
|
259
|
+
</div>
|
|
260
|
+
${
|
|
261
|
+
state.lastErrorAt
|
|
262
|
+
? `
|
|
263
|
+
<div class="detail-item">
|
|
264
|
+
<span class="detail-label" style="color: #f97316;">發生時間</span>
|
|
265
|
+
<span class="detail-value" style="color: #f97316;">${formatTime(
|
|
266
|
+
state.lastErrorAt
|
|
267
|
+
)}</span>
|
|
268
|
+
</div>
|
|
269
|
+
`
|
|
270
|
+
: ""
|
|
271
|
+
}
|
|
272
|
+
${
|
|
273
|
+
isReconnecting
|
|
274
|
+
? `
|
|
275
|
+
<div class="detail-item">
|
|
276
|
+
<span class="detail-label" style="color: #3b82f6;">重連狀態</span>
|
|
277
|
+
<span class="detail-value" style="color: #3b82f6;">
|
|
278
|
+
嘗試 ${
|
|
279
|
+
state.reconnectAttempts || 0
|
|
280
|
+
}/${state.maxReconnectAttempts || 3}
|
|
281
|
+
${
|
|
282
|
+
state.nextReconnectAt
|
|
283
|
+
? ` - 下次重連: ${formatTime(
|
|
284
|
+
state.nextReconnectAt
|
|
285
|
+
)}`
|
|
286
|
+
: ""
|
|
287
|
+
}
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
`
|
|
291
|
+
: ""
|
|
292
|
+
}
|
|
293
|
+
</div>
|
|
294
|
+
`
|
|
295
|
+
: ""
|
|
296
|
+
}
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
${
|
|
300
|
+
state.status === "connected" && tools.length > 0
|
|
301
|
+
? `
|
|
302
|
+
<div class="tools-section">
|
|
303
|
+
<div class="tools-header">
|
|
304
|
+
<span class="tools-title">🔧 工具列表</span>
|
|
305
|
+
<span class="tools-count">${
|
|
306
|
+
tools.length
|
|
307
|
+
} 個工具</span>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="tools-grid">
|
|
310
|
+
${tools
|
|
311
|
+
.map((tool) =>
|
|
312
|
+
renderToolItem(server.id, tool)
|
|
313
|
+
)
|
|
314
|
+
.join("")}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
`
|
|
318
|
+
: ""
|
|
319
|
+
}
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 格式化時間
|
|
326
|
+
function formatTime(isoString) {
|
|
327
|
+
if (!isoString) return "-";
|
|
328
|
+
const date = new Date(isoString);
|
|
329
|
+
return date.toLocaleString("zh-TW", {
|
|
330
|
+
hour: "2-digit",
|
|
331
|
+
minute: "2-digit",
|
|
332
|
+
second: "2-digit",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 渲染工具項目
|
|
337
|
+
function renderToolItem(serverId, tool) {
|
|
338
|
+
const enabled = tool.enabled !== false;
|
|
339
|
+
const encodedName = encodeURIComponent(tool.name);
|
|
340
|
+
|
|
341
|
+
return `
|
|
342
|
+
<div class="tool-item ${enabled ? "" : "disabled"}">
|
|
343
|
+
<input type="checkbox" class="tool-checkbox"
|
|
344
|
+
data-server-id="${serverId}"
|
|
345
|
+
data-tool-name="${encodedName}"
|
|
346
|
+
${enabled ? "checked" : ""}>
|
|
347
|
+
<div class="tool-info">
|
|
348
|
+
<div class="tool-name">${escapeHtml(tool.name)}</div>
|
|
349
|
+
${
|
|
350
|
+
tool.description
|
|
351
|
+
? `<div class="tool-description">${escapeHtml(
|
|
352
|
+
tool.description
|
|
353
|
+
)}</div>`
|
|
354
|
+
: ""
|
|
355
|
+
}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 獲取狀態文字
|
|
362
|
+
function getStatusText(status) {
|
|
363
|
+
const statusMap = {
|
|
364
|
+
connected: "已連接",
|
|
365
|
+
disconnected: "已斷開",
|
|
366
|
+
connecting: "連接中...",
|
|
367
|
+
reconnecting: "重連中...",
|
|
368
|
+
error: "錯誤",
|
|
369
|
+
};
|
|
370
|
+
return statusMap[status] || status;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 初始化事件監聽
|
|
374
|
+
function initEventListeners() {
|
|
375
|
+
// 新增 Server
|
|
376
|
+
elements.addServerBtn.addEventListener("click", () => openModal());
|
|
377
|
+
|
|
378
|
+
// 全部連接
|
|
379
|
+
elements.connectAllBtn.addEventListener("click", connectAll);
|
|
380
|
+
|
|
381
|
+
// 全部斷開
|
|
382
|
+
elements.disconnectAllBtn.addEventListener("click", disconnectAll);
|
|
383
|
+
|
|
384
|
+
// 創建 Serena
|
|
385
|
+
elements.createSerenaBtn.addEventListener("click", createSerena);
|
|
386
|
+
|
|
387
|
+
// Modal 事件
|
|
388
|
+
elements.closeModal.addEventListener("click", closeModal);
|
|
389
|
+
elements.cancelBtn.addEventListener("click", closeModal);
|
|
390
|
+
elements.saveBtn.addEventListener("click", saveServer);
|
|
391
|
+
|
|
392
|
+
// 傳輸方式切換
|
|
393
|
+
elements.serverTransport.addEventListener("change", (e) => {
|
|
394
|
+
const isStdio = e.target.value === "stdio";
|
|
395
|
+
elements.stdioFields.style.display = isStdio ? "block" : "none";
|
|
396
|
+
elements.httpFields.style.display = isStdio ? "none" : "block";
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// 延遲啟動開關
|
|
400
|
+
elements.serverDeferredStartup.addEventListener("change", (e) => {
|
|
401
|
+
elements.deferredFields.style.display = e.target.checked
|
|
402
|
+
? "block"
|
|
403
|
+
: "none";
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// 點擊 Modal 外部關閉
|
|
407
|
+
elements.serverModal.addEventListener("click", (e) => {
|
|
408
|
+
if (e.target === elements.serverModal) {
|
|
409
|
+
closeModal();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 綁定 Server 事件
|
|
415
|
+
function bindServerEvents() {
|
|
416
|
+
// 連接按鈕
|
|
417
|
+
document.querySelectorAll(".btn-connect").forEach((btn) => {
|
|
418
|
+
btn.addEventListener("click", () =>
|
|
419
|
+
connectServer(parseInt(btn.dataset.id))
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// 斷開按鈕
|
|
424
|
+
document.querySelectorAll(".btn-disconnect").forEach((btn) => {
|
|
425
|
+
btn.addEventListener("click", () =>
|
|
426
|
+
disconnectServer(parseInt(btn.dataset.id))
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// 編輯按鈕
|
|
431
|
+
document.querySelectorAll(".btn-edit").forEach((btn) => {
|
|
432
|
+
btn.addEventListener("click", () => editServer(parseInt(btn.dataset.id)));
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// 刪除按鈕
|
|
436
|
+
document.querySelectorAll(".btn-delete").forEach((btn) => {
|
|
437
|
+
btn.addEventListener("click", () =>
|
|
438
|
+
deleteServer(parseInt(btn.dataset.id))
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// 重試按鈕
|
|
443
|
+
document.querySelectorAll(".btn-retry").forEach((btn) => {
|
|
444
|
+
btn.addEventListener("click", () =>
|
|
445
|
+
retryServer(parseInt(btn.dataset.id))
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// 取消重連按鈕
|
|
450
|
+
document.querySelectorAll(".btn-cancel-reconnect").forEach((btn) => {
|
|
451
|
+
btn.addEventListener("click", () =>
|
|
452
|
+
cancelReconnect(parseInt(btn.dataset.id))
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// 工具啟用切換
|
|
457
|
+
document.querySelectorAll(".tool-checkbox").forEach((checkbox) => {
|
|
458
|
+
checkbox.addEventListener("change", (e) => {
|
|
459
|
+
const serverId = parseInt(e.target.dataset.serverId);
|
|
460
|
+
const toolName = decodeURIComponent(e.target.dataset.toolName);
|
|
461
|
+
const enabled = e.target.checked;
|
|
462
|
+
toggleToolEnabled(serverId, toolName, enabled);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 打開 Modal
|
|
468
|
+
function openModal(server = null) {
|
|
469
|
+
if (server) {
|
|
470
|
+
elements.modalTitle.textContent = "編輯 MCP Server";
|
|
471
|
+
elements.serverId.value = server.id;
|
|
472
|
+
elements.serverName.value = server.name;
|
|
473
|
+
elements.serverTransport.value = server.transport;
|
|
474
|
+
elements.serverCommand.value = server.command || "";
|
|
475
|
+
elements.serverArgs.value = (server.args || []).join("\n");
|
|
476
|
+
elements.serverEnv.value = server.env
|
|
477
|
+
? JSON.stringify(server.env, null, 2)
|
|
478
|
+
: "";
|
|
479
|
+
elements.serverUrl.value = server.url || "";
|
|
480
|
+
elements.serverDeferredStartup.checked = server.deferredStartup || false;
|
|
481
|
+
elements.serverStartupArgsTemplate.value =
|
|
482
|
+
server.startupArgsTemplate || "";
|
|
483
|
+
} else {
|
|
484
|
+
elements.modalTitle.textContent = "新增 MCP Server";
|
|
485
|
+
elements.serverId.value = "";
|
|
486
|
+
elements.serverForm.reset();
|
|
487
|
+
elements.serverEnv.value = "";
|
|
488
|
+
elements.serverDeferredStartup.checked = false;
|
|
489
|
+
elements.serverStartupArgsTemplate.value = "";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 更新欄位顯示
|
|
493
|
+
const isStdio = elements.serverTransport.value === "stdio";
|
|
494
|
+
elements.stdioFields.style.display = isStdio ? "block" : "none";
|
|
495
|
+
elements.httpFields.style.display = isStdio ? "none" : "block";
|
|
496
|
+
elements.deferredFields.style.display = elements.serverDeferredStartup
|
|
497
|
+
.checked
|
|
498
|
+
? "block"
|
|
499
|
+
: "none";
|
|
500
|
+
|
|
501
|
+
elements.serverModal.classList.add("active");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 關閉 Modal
|
|
505
|
+
function closeModal() {
|
|
506
|
+
elements.serverModal.classList.remove("active");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 儲存 Server
|
|
510
|
+
async function saveServer() {
|
|
511
|
+
const id = elements.serverId.value;
|
|
512
|
+
const name = elements.serverName.value.trim();
|
|
513
|
+
const transport = elements.serverTransport.value;
|
|
514
|
+
const command = elements.serverCommand.value.trim();
|
|
515
|
+
const argsText = elements.serverArgs.value.trim();
|
|
516
|
+
const envText = elements.serverEnv.value.trim();
|
|
517
|
+
const url = elements.serverUrl.value.trim();
|
|
518
|
+
const deferredStartup = elements.serverDeferredStartup.checked;
|
|
519
|
+
const startupArgsTemplate = elements.serverStartupArgsTemplate.value.trim();
|
|
520
|
+
|
|
521
|
+
if (!name) {
|
|
522
|
+
showToast("請輸入名稱", "error");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (transport === "stdio" && !command) {
|
|
527
|
+
showToast("stdio 傳輸方式需要指定命令", "error");
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (transport !== "stdio" && !url) {
|
|
532
|
+
showToast(`${transport} 傳輸方式需要指定 URL`, "error");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let env = {};
|
|
537
|
+
if (envText) {
|
|
538
|
+
try {
|
|
539
|
+
env = JSON.parse(envText);
|
|
540
|
+
} catch (e) {
|
|
541
|
+
showToast("環境變數格式錯誤,請使用 JSON 格式", "error");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const args = argsText
|
|
547
|
+
? argsText
|
|
548
|
+
.split("\n")
|
|
549
|
+
.map((a) => a.trim())
|
|
550
|
+
.filter((a) => a)
|
|
551
|
+
: [];
|
|
552
|
+
|
|
553
|
+
const data = {
|
|
554
|
+
name,
|
|
555
|
+
transport,
|
|
556
|
+
command: transport === "stdio" ? command : undefined,
|
|
557
|
+
args: transport === "stdio" ? args : undefined,
|
|
558
|
+
env:
|
|
559
|
+
transport === "stdio" && Object.keys(env).length > 0 ? env : undefined,
|
|
560
|
+
url: transport !== "stdio" ? url : undefined,
|
|
561
|
+
enabled: true,
|
|
562
|
+
deferredStartup,
|
|
563
|
+
startupArgsTemplate:
|
|
564
|
+
deferredStartup && startupArgsTemplate
|
|
565
|
+
? startupArgsTemplate
|
|
566
|
+
: undefined,
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
showLoading(true);
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const endpoint = id
|
|
573
|
+
? `${API_BASE}/api/mcp-servers/${id}`
|
|
574
|
+
: `${API_BASE}/api/mcp-servers`;
|
|
575
|
+
const method = id ? "PUT" : "POST";
|
|
576
|
+
|
|
577
|
+
const response = await fetch(endpoint, {
|
|
578
|
+
method,
|
|
579
|
+
headers: { "Content-Type": "application/json" },
|
|
580
|
+
body: JSON.stringify(data),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const result = await response.json();
|
|
584
|
+
|
|
585
|
+
if (result.success) {
|
|
586
|
+
showToast(id ? "Server 更新成功" : "Server 創建成功", "success");
|
|
587
|
+
closeModal();
|
|
588
|
+
loadServers();
|
|
589
|
+
} else {
|
|
590
|
+
showToast(result.error || "操作失敗", "error");
|
|
591
|
+
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
console.error("Save server failed:", error);
|
|
594
|
+
showToast("操作失敗", "error");
|
|
595
|
+
} finally {
|
|
596
|
+
showLoading(false);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 連接 Server
|
|
601
|
+
async function connectServer(id) {
|
|
602
|
+
showLoading(true);
|
|
603
|
+
try {
|
|
604
|
+
const response = await fetch(
|
|
605
|
+
`${API_BASE}/api/mcp-servers/${id}/connect`,
|
|
606
|
+
{
|
|
607
|
+
method: "POST",
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
const result = await response.json();
|
|
611
|
+
|
|
612
|
+
if (result.success) {
|
|
613
|
+
showToast("連接成功", "success");
|
|
614
|
+
} else {
|
|
615
|
+
showToast(result.error || "連接失敗", "error");
|
|
616
|
+
}
|
|
617
|
+
loadServers();
|
|
618
|
+
} catch (error) {
|
|
619
|
+
console.error("Connect failed:", error);
|
|
620
|
+
showToast("連接失敗", "error");
|
|
621
|
+
} finally {
|
|
622
|
+
showLoading(false);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 斷開 Server
|
|
627
|
+
async function disconnectServer(id) {
|
|
628
|
+
showLoading(true);
|
|
629
|
+
try {
|
|
630
|
+
const response = await fetch(
|
|
631
|
+
`${API_BASE}/api/mcp-servers/${id}/disconnect`,
|
|
632
|
+
{
|
|
633
|
+
method: "POST",
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
const result = await response.json();
|
|
637
|
+
|
|
638
|
+
if (result.success) {
|
|
639
|
+
showToast("已斷開連接", "success");
|
|
640
|
+
} else {
|
|
641
|
+
showToast(result.error || "斷開失敗", "error");
|
|
642
|
+
}
|
|
643
|
+
loadServers();
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error("Disconnect failed:", error);
|
|
646
|
+
showToast("斷開失敗", "error");
|
|
647
|
+
} finally {
|
|
648
|
+
showLoading(false);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// 重試連接 Server
|
|
653
|
+
async function retryServer(id) {
|
|
654
|
+
showLoading(true);
|
|
655
|
+
try {
|
|
656
|
+
const response = await fetch(`${API_BASE}/api/mcp-servers/${id}/retry`, {
|
|
657
|
+
method: "POST",
|
|
658
|
+
});
|
|
659
|
+
const result = await response.json();
|
|
660
|
+
|
|
661
|
+
if (result.success) {
|
|
662
|
+
showToast("重試連接成功", "success");
|
|
663
|
+
} else {
|
|
664
|
+
showToast(result.error || "重試連接失敗", "error");
|
|
665
|
+
}
|
|
666
|
+
loadServers();
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error("Retry failed:", error);
|
|
669
|
+
showToast("重試連接失敗", "error");
|
|
670
|
+
} finally {
|
|
671
|
+
showLoading(false);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 取消自動重連
|
|
676
|
+
async function cancelReconnect(id) {
|
|
677
|
+
try {
|
|
678
|
+
const response = await fetch(
|
|
679
|
+
`${API_BASE}/api/mcp-servers/${id}/cancel-reconnect`,
|
|
680
|
+
{
|
|
681
|
+
method: "POST",
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
const result = await response.json();
|
|
685
|
+
|
|
686
|
+
if (result.success) {
|
|
687
|
+
showToast("已取消自動重連", "info");
|
|
688
|
+
} else {
|
|
689
|
+
showToast(result.error || "取消失敗", "error");
|
|
690
|
+
}
|
|
691
|
+
loadServers();
|
|
692
|
+
} catch (error) {
|
|
693
|
+
console.error("Cancel reconnect failed:", error);
|
|
694
|
+
showToast("取消失敗", "error");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 編輯 Server
|
|
699
|
+
function editServer(id) {
|
|
700
|
+
const server = servers.find((s) => s.id === id);
|
|
701
|
+
if (server) {
|
|
702
|
+
openModal(server);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// 刪除 Server
|
|
707
|
+
async function deleteServer(id) {
|
|
708
|
+
if (!confirm("確定要刪除此 Server 嗎?")) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
showLoading(true);
|
|
713
|
+
try {
|
|
714
|
+
const response = await fetch(`${API_BASE}/api/mcp-servers/${id}`, {
|
|
715
|
+
method: "DELETE",
|
|
716
|
+
});
|
|
717
|
+
const result = await response.json();
|
|
718
|
+
|
|
719
|
+
if (result.success) {
|
|
720
|
+
showToast("Server 已刪除", "success");
|
|
721
|
+
loadServers();
|
|
722
|
+
} else {
|
|
723
|
+
showToast(result.error || "刪除失敗", "error");
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
console.error("Delete failed:", error);
|
|
727
|
+
showToast("刪除失敗", "error");
|
|
728
|
+
} finally {
|
|
729
|
+
showLoading(false);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// 全部連接
|
|
734
|
+
async function connectAll() {
|
|
735
|
+
showLoading(true);
|
|
736
|
+
try {
|
|
737
|
+
const response = await fetch(`${API_BASE}/api/mcp-servers/connect-all`, {
|
|
738
|
+
method: "POST",
|
|
739
|
+
});
|
|
740
|
+
const result = await response.json();
|
|
741
|
+
|
|
742
|
+
if (result.success) {
|
|
743
|
+
const successCount = result.results.filter((r) => r.success).length;
|
|
744
|
+
showToast(
|
|
745
|
+
`連接完成:${successCount}/${result.results.length} 成功`,
|
|
746
|
+
"success"
|
|
747
|
+
);
|
|
748
|
+
loadServers();
|
|
749
|
+
} else {
|
|
750
|
+
showToast(result.error || "連接失敗", "error");
|
|
751
|
+
}
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error("Connect all failed:", error);
|
|
754
|
+
showToast("連接失敗", "error");
|
|
755
|
+
} finally {
|
|
756
|
+
showLoading(false);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// 全部斷開
|
|
761
|
+
async function disconnectAll() {
|
|
762
|
+
showLoading(true);
|
|
763
|
+
try {
|
|
764
|
+
const response = await fetch(
|
|
765
|
+
`${API_BASE}/api/mcp-servers/disconnect-all`,
|
|
766
|
+
{
|
|
767
|
+
method: "POST",
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
const result = await response.json();
|
|
771
|
+
|
|
772
|
+
if (result.success) {
|
|
773
|
+
showToast("已斷開所有連接", "success");
|
|
774
|
+
loadServers();
|
|
775
|
+
} else {
|
|
776
|
+
showToast(result.error || "斷開失敗", "error");
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
console.error("Disconnect all failed:", error);
|
|
780
|
+
showToast("斷開失敗", "error");
|
|
781
|
+
} finally {
|
|
782
|
+
showLoading(false);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// 創建 Serena
|
|
787
|
+
async function createSerena() {
|
|
788
|
+
const projectPath = elements.serenaProjectPath.value.trim();
|
|
789
|
+
|
|
790
|
+
showLoading(true);
|
|
791
|
+
try {
|
|
792
|
+
const response = await fetch(
|
|
793
|
+
`${API_BASE}/api/mcp-presets/serena/create`,
|
|
794
|
+
{
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: { "Content-Type": "application/json" },
|
|
797
|
+
body: JSON.stringify({ projectPath, autoConnect: true }),
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
const result = await response.json();
|
|
801
|
+
|
|
802
|
+
if (result.success) {
|
|
803
|
+
const state = result.server?.state;
|
|
804
|
+
if (state?.status === "connected") {
|
|
805
|
+
showToast(
|
|
806
|
+
`Serena 創建並連接成功,共 ${state.tools?.length || 0} 個工具`,
|
|
807
|
+
"success"
|
|
808
|
+
);
|
|
809
|
+
} else {
|
|
810
|
+
showToast(
|
|
811
|
+
`Serena 創建成功,但連接失敗:${state?.error || "未知錯誤"}`,
|
|
812
|
+
"error"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
loadServers();
|
|
816
|
+
} else {
|
|
817
|
+
showToast(result.error || "創建失敗", "error");
|
|
818
|
+
}
|
|
819
|
+
} catch (error) {
|
|
820
|
+
console.error("Create Serena failed:", error);
|
|
821
|
+
showToast("創建 Serena 失敗", "error");
|
|
822
|
+
} finally {
|
|
823
|
+
showLoading(false);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// 切換工具啟用狀態
|
|
828
|
+
async function toggleToolEnabled(serverId, toolName, enabled) {
|
|
829
|
+
try {
|
|
830
|
+
const response = await fetch(
|
|
831
|
+
`${API_BASE}/api/mcp-servers/${serverId}/tools/${encodeURIComponent(
|
|
832
|
+
toolName
|
|
833
|
+
)}/enable`,
|
|
834
|
+
{
|
|
835
|
+
method: "PUT",
|
|
836
|
+
headers: { "Content-Type": "application/json" },
|
|
837
|
+
body: JSON.stringify({ enabled }),
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
const result = await response.json();
|
|
841
|
+
|
|
842
|
+
if (!result.success) {
|
|
843
|
+
showToast(result.error || "設定失敗", "error");
|
|
844
|
+
loadServers();
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
console.error("Toggle tool failed:", error);
|
|
848
|
+
showToast("設定失敗", "error");
|
|
849
|
+
loadServers();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// 顯示/隱藏 Loading
|
|
854
|
+
function showLoading(show) {
|
|
855
|
+
elements.loadingOverlay.classList.toggle("active", show);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// 顯示 Toast
|
|
859
|
+
function showToast(message, type = "info") {
|
|
860
|
+
const toast = document.createElement("div");
|
|
861
|
+
toast.className = `toast ${type}`;
|
|
862
|
+
toast.textContent = message;
|
|
863
|
+
elements.toastContainer.appendChild(toast);
|
|
864
|
+
|
|
865
|
+
setTimeout(() => {
|
|
866
|
+
toast.remove();
|
|
867
|
+
}, 3000);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// HTML 轉義
|
|
871
|
+
function escapeHtml(text) {
|
|
872
|
+
if (!text) return "";
|
|
873
|
+
const div = document.createElement("div");
|
|
874
|
+
div.textContent = text;
|
|
875
|
+
return div.innerHTML;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// 頁面載入完成後初始化
|
|
879
|
+
if (document.readyState === "loading") {
|
|
880
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
881
|
+
} else {
|
|
882
|
+
init();
|
|
883
|
+
}
|
|
884
|
+
})();
|