@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +953 -0
  3. package/dist/cli.cjs +95778 -0
  4. package/dist/index.cjs +92818 -0
  5. package/dist/static/app.js +385 -0
  6. package/dist/static/components/navbar.css +406 -0
  7. package/dist/static/components/navbar.html +49 -0
  8. package/dist/static/components/navbar.js +211 -0
  9. package/dist/static/dashboard.css +495 -0
  10. package/dist/static/dashboard.html +95 -0
  11. package/dist/static/dashboard.js +540 -0
  12. package/dist/static/favicon.svg +27 -0
  13. package/dist/static/index.html +541 -0
  14. package/dist/static/logs.html +376 -0
  15. package/dist/static/logs.js +442 -0
  16. package/dist/static/mcp-settings.html +797 -0
  17. package/dist/static/mcp-settings.js +884 -0
  18. package/dist/static/modules/app-core.js +124 -0
  19. package/dist/static/modules/conversation-panel.js +247 -0
  20. package/dist/static/modules/feedback-handler.js +1420 -0
  21. package/dist/static/modules/image-handler.js +155 -0
  22. package/dist/static/modules/log-viewer.js +296 -0
  23. package/dist/static/modules/mcp-manager.js +474 -0
  24. package/dist/static/modules/prompt-manager.js +364 -0
  25. package/dist/static/modules/settings-manager.js +299 -0
  26. package/dist/static/modules/socket-manager.js +170 -0
  27. package/dist/static/modules/state-manager.js +352 -0
  28. package/dist/static/modules/timer-controller.js +243 -0
  29. package/dist/static/modules/ui-helpers.js +246 -0
  30. package/dist/static/settings.html +355 -0
  31. package/dist/static/settings.js +425 -0
  32. package/dist/static/socket.io.min.js +7 -0
  33. package/dist/static/style.css +2157 -0
  34. package/dist/static/terminals.html +357 -0
  35. package/dist/static/terminals.js +321 -0
  36. 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
+ })();