@hirohsu/user-web-feedback 2.8.11 → 2.8.12

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 (34) hide show
  1. package/README.md +953 -953
  2. package/dist/cli.cjs +35048 -22240
  3. package/dist/index.cjs +39804 -26994
  4. package/dist/static/app.js +385 -385
  5. package/dist/static/components/navbar.css +406 -406
  6. package/dist/static/components/navbar.html +49 -49
  7. package/dist/static/components/navbar.js +211 -211
  8. package/dist/static/dashboard.css +495 -495
  9. package/dist/static/dashboard.html +95 -95
  10. package/dist/static/dashboard.js +540 -540
  11. package/dist/static/favicon.svg +27 -27
  12. package/dist/static/index.html +541 -541
  13. package/dist/static/logs.html +376 -376
  14. package/dist/static/logs.js +442 -442
  15. package/dist/static/mcp-settings.html +797 -797
  16. package/dist/static/mcp-settings.js +884 -884
  17. package/dist/static/modules/app-core.js +124 -124
  18. package/dist/static/modules/conversation-panel.js +247 -247
  19. package/dist/static/modules/feedback-handler.js +1420 -1420
  20. package/dist/static/modules/image-handler.js +155 -155
  21. package/dist/static/modules/log-viewer.js +296 -296
  22. package/dist/static/modules/mcp-manager.js +474 -474
  23. package/dist/static/modules/prompt-manager.js +364 -364
  24. package/dist/static/modules/settings-manager.js +299 -299
  25. package/dist/static/modules/socket-manager.js +170 -170
  26. package/dist/static/modules/state-manager.js +352 -352
  27. package/dist/static/modules/timer-controller.js +243 -243
  28. package/dist/static/modules/ui-helpers.js +246 -246
  29. package/dist/static/settings.html +426 -426
  30. package/dist/static/settings.js +767 -767
  31. package/dist/static/style.css +2156 -2156
  32. package/dist/static/terminals.html +357 -357
  33. package/dist/static/terminals.js +321 -321
  34. package/package.json +95 -95
@@ -1,767 +1,767 @@
1
- /**
2
- * 系統設定頁面
3
- */
4
-
5
- (function () {
6
- "use strict";
7
-
8
- const API_BASE = "";
9
-
10
- // Provider 與 API URL 的對應表 (key 需與 HTML select option value 一致)
11
- const PROVIDER_API_MAP = {
12
- openai: "https://api.openai.com/v1",
13
- google: "https://generativelanguage.googleapis.com/v1beta",
14
- anthropic: "https://api.anthropic.com/v1",
15
- local: "http://localhost:11434/v1"
16
- };
17
-
18
- // 反向查詢:從 API URL 取得 Provider
19
- function getProviderFromApiUrl(apiUrl) {
20
- if (!apiUrl) return "openai";
21
- const normalizedUrl = apiUrl.toLowerCase();
22
- if (normalizedUrl.includes("generativelanguage.googleapis.com")) return "google";
23
- if (normalizedUrl.includes("api.anthropic.com")) return "anthropic";
24
- if (normalizedUrl.includes("nvidia.com")) return "nvidia";
25
- if (normalizedUrl.includes("bigmodel.cn") || normalizedUrl.includes("z.ai")) return "zai";
26
- if (normalizedUrl.includes("api.openai.com")) return "openai";
27
- return "openai"; // 預設
28
- }
29
-
30
- // 從 Provider 取得 API URL
31
- function getApiUrlFromProvider(provider) {
32
- return PROVIDER_API_MAP[provider] || PROVIDER_API_MAP.openai;
33
- }
34
-
35
- const elements = {
36
- // AI Settings
37
- aiProvider: document.getElementById("aiProvider"),
38
- apiUrl: document.getElementById("apiUrl"),
39
- openaiCompatible: document.getElementById("openaiCompatible"),
40
- apiKey: document.getElementById("apiKey"),
41
- toggleApiKey: document.getElementById("toggleApiKey"),
42
- aiModel: document.getElementById("aiModel"),
43
- temperature: document.getElementById("temperature"),
44
- maxTokens: document.getElementById("maxTokens"),
45
- autoReplyTimerSeconds: document.getElementById("autoReplyTimerSeconds"),
46
- maxToolRounds: document.getElementById("maxToolRounds"),
47
- debugMode: document.getElementById("debugMode"),
48
- testAiBtn: document.getElementById("testAiBtn"),
49
- saveAiBtn: document.getElementById("saveAiBtn"),
50
- // CLI Settings
51
- aiModeApi: document.getElementById("aiModeApi"),
52
- aiModeCli: document.getElementById("aiModeCli"),
53
- cliTool: document.getElementById("cliTool"),
54
- cliToolGroup: document.getElementById("cliToolGroup"),
55
- cliToolStatus: document.getElementById("cliToolStatus"),
56
- cliTimeout: document.getElementById("cliTimeout"),
57
- cliTimeoutGroup: document.getElementById("cliTimeoutGroup"),
58
- cliFallbackToApi: document.getElementById("cliFallbackToApi"),
59
- cliFallbackGroup: document.getElementById("cliFallbackGroup"),
60
- detectCliBtn: document.getElementById("detectCliBtn"),
61
- saveCliBtn: document.getElementById("saveCliBtn"),
62
- // User Preferences
63
- autoSubmitOnTimeout: document.getElementById("autoSubmitOnTimeout"),
64
- confirmBeforeSubmit: document.getElementById("confirmBeforeSubmit"),
65
- defaultLanguage: document.getElementById("defaultLanguage"),
66
- savePreferencesBtn: document.getElementById("savePreferencesBtn"),
67
- // Self-Probe Settings
68
- enableSelfProbe: document.getElementById("enableSelfProbe"),
69
- selfProbeInterval: document.getElementById("selfProbeInterval"),
70
- selfProbeIntervalGroup: document.getElementById("selfProbeIntervalGroup"),
71
- selfProbeStatus: document.getElementById("selfProbeStatus"),
72
- selfProbeRunning: document.getElementById("selfProbeRunning"),
73
- selfProbeCount: document.getElementById("selfProbeCount"),
74
- selfProbeLastTime: document.getElementById("selfProbeLastTime"),
75
- saveSelfProbeBtn: document.getElementById("saveSelfProbeBtn"),
76
- // Prompt Config Settings
77
- promptConfigList: document.getElementById("promptConfigList"),
78
- resetPromptsBtn: document.getElementById("resetPromptsBtn"),
79
- savePromptsBtn: document.getElementById("savePromptsBtn"),
80
- // Extended Provider Settings (integrated into AI settings)
81
- zaiExtSettings: document.getElementById("zaiExtSettings"),
82
- zaiRegion: document.getElementById("zaiRegion"),
83
- toastContainer: document.getElementById("toastContainer"),
84
- };
85
-
86
- // CLI 工具檢測結果緩存
87
- let cliDetectionResult = null;
88
- // 追蹤原始的 apiKeyMasked,用於判斷用戶是否修改了 API key
89
- let originalApiKeyMasked = "";
90
-
91
- function init() {
92
- setupEventListeners();
93
- loadAISettings();
94
- loadCLISettings();
95
- loadPreferences();
96
- loadSelfProbeSettings();
97
- loadPromptConfigs();
98
- }
99
-
100
- function setupEventListeners() {
101
- // AI Settings
102
- elements.toggleApiKey.addEventListener("click", toggleApiKeyVisibility);
103
- elements.testAiBtn.addEventListener("click", testAIConnection);
104
- elements.saveAiBtn.addEventListener("click", saveAISettings);
105
- if (elements.aiProvider) {
106
- elements.aiProvider.addEventListener("change", handleAIProviderChange);
107
- }
108
- if (elements.zaiRegion) {
109
- elements.zaiRegion.addEventListener("change", handleZaiRegionChange);
110
- }
111
-
112
- // CLI Settings
113
- elements.aiModeApi.addEventListener("change", handleAIModeChange);
114
- elements.aiModeCli.addEventListener("change", handleAIModeChange);
115
- elements.detectCliBtn.addEventListener("click", detectCLITools);
116
- elements.saveCliBtn.addEventListener("click", saveCLISettings);
117
-
118
- // User Preferences
119
- elements.savePreferencesBtn.addEventListener("click", savePreferences);
120
-
121
- // Self-Probe Settings
122
- if (elements.enableSelfProbe) {
123
- elements.enableSelfProbe.addEventListener("change", handleSelfProbeToggle);
124
- }
125
- if (elements.saveSelfProbeBtn) {
126
- elements.saveSelfProbeBtn.addEventListener("click", saveSelfProbeSettings);
127
- }
128
-
129
- // Prompt Config Settings
130
- if (elements.resetPromptsBtn) {
131
- elements.resetPromptsBtn.addEventListener("click", resetPromptConfigs);
132
- }
133
- if (elements.savePromptsBtn) {
134
- elements.savePromptsBtn.addEventListener("click", savePromptConfigs);
135
- }
136
- }
137
-
138
- const DEFAULT_API_URLS = {
139
- openai: 'https://api.openai.com/v1',
140
- anthropic: 'https://api.anthropic.com/v1',
141
- google: 'https://generativelanguage.googleapis.com/v1beta',
142
- nvidia: 'https://integrate.api.nvidia.com/v1',
143
- zai: 'https://api.z.ai/api/coding/paas/v4',
144
- 'zai-china': 'https://open.bigmodel.cn/api/paas/v4'
145
- };
146
-
147
- function handleAIProviderChange(updateUrl = true) {
148
- const provider = elements.aiProvider?.value || 'google';
149
-
150
- // Z.AI 專用設定
151
- if (elements.zaiExtSettings) {
152
- elements.zaiExtSettings.style.display = provider === 'zai' ? 'block' : 'none';
153
- }
154
-
155
- // 更新預設 API URL(僅當 updateUrl 為 true 時)
156
- if (updateUrl && elements.apiUrl) {
157
- let defaultUrl;
158
- if (provider === 'zai') {
159
- const region = elements.zaiRegion?.value || 'international';
160
- defaultUrl = region === 'china' ? DEFAULT_API_URLS['zai-china'] : DEFAULT_API_URLS.zai;
161
- } else {
162
- defaultUrl = DEFAULT_API_URLS[provider] || '';
163
- }
164
- elements.apiUrl.value = defaultUrl;
165
- elements.apiUrl.placeholder = defaultUrl || 'API 端點 URL';
166
- }
167
- }
168
-
169
- function handleZaiRegionChange() {
170
- const region = elements.zaiRegion?.value || 'international';
171
- if (elements.apiUrl) {
172
- const defaultUrl = region === 'china' ? DEFAULT_API_URLS['zai-china'] : DEFAULT_API_URLS.zai;
173
- elements.apiUrl.value = defaultUrl;
174
- elements.apiUrl.placeholder = defaultUrl || 'API 端點 URL';
175
- }
176
- }
177
-
178
- function handleSelfProbeToggle() {
179
- const isEnabled = elements.enableSelfProbe.checked;
180
- if (elements.selfProbeIntervalGroup) {
181
- elements.selfProbeIntervalGroup.style.opacity = isEnabled ? "1" : "0.5";
182
- }
183
- }
184
-
185
- function handleAIModeChange() {
186
- const isCLIMode = elements.aiModeCli.checked;
187
- elements.cliToolGroup.style.display = isCLIMode ? "block" : "none";
188
- elements.cliTimeoutGroup.style.display = isCLIMode ? "block" : "none";
189
- elements.cliFallbackGroup.style.display = isCLIMode ? "block" : "none";
190
-
191
- if (isCLIMode && !cliDetectionResult) {
192
- detectCLITools();
193
- }
194
- }
195
-
196
- function toggleApiKeyVisibility() {
197
- const type = elements.apiKey.type;
198
- elements.apiKey.type = type === "password" ? "text" : "password";
199
- elements.toggleApiKey.textContent = type === "password" ? "🙈" : "👁️";
200
- }
201
-
202
- async function loadAISettings() {
203
- try {
204
- const response = await fetch(`${API_BASE}/api/ai-settings`);
205
- const data = await response.json();
206
-
207
- if (data.settings) {
208
- // 從 apiUrl 反向推斷 provider
209
- const provider = getProviderFromApiUrl(data.settings.apiUrl);
210
- elements.aiProvider.value = provider;
211
- // 設置 API URL
212
- if (elements.apiUrl) {
213
- elements.apiUrl.value = data.settings.apiUrl || DEFAULT_API_URLS[provider] || '';
214
- }
215
- // OpenAI 相容模式
216
- if (elements.openaiCompatible) {
217
- elements.openaiCompatible.checked = data.settings.openaiCompatible || false;
218
- }
219
- // API 返回的是 apiKeyMasked(遮罩後的 key),顯示給用戶看
220
- originalApiKeyMasked = data.settings.apiKeyMasked || "";
221
- elements.apiKey.value = originalApiKeyMasked;
222
- elements.apiKey.placeholder = originalApiKeyMasked ? "輸入新的 API Key 以更換" : "請輸入 API Key";
223
- elements.aiModel.value = data.settings.model || "";
224
- elements.temperature.value = data.settings.temperature ?? 0.7;
225
- elements.maxTokens.value = data.settings.maxTokens ?? 1000;
226
- elements.autoReplyTimerSeconds.value = data.settings.autoReplyTimerSeconds ?? 300;
227
- elements.maxToolRounds.value = data.settings.maxToolRounds ?? 5;
228
- elements.debugMode.checked = data.settings.debugMode || false;
229
- // 更新 UI(不更新 URL,因為已經從資料庫載入)
230
- handleAIProviderChange(false);
231
- }
232
- } catch (error) {
233
- console.error("Failed to load AI settings:", error);
234
- showToast("載入 AI 設定失敗", "error");
235
- }
236
- }
237
-
238
- async function loadPreferences() {
239
- try {
240
- const response = await fetch(`${API_BASE}/api/preferences`);
241
- const data = await response.json();
242
-
243
- if (data.preferences) {
244
- elements.autoSubmitOnTimeout.checked =
245
- data.preferences.autoSubmitOnTimeout || false;
246
- elements.confirmBeforeSubmit.checked =
247
- data.preferences.confirmBeforeSubmit || false;
248
- elements.defaultLanguage.value =
249
- data.preferences.defaultLanguage || "zh-TW";
250
- }
251
- } catch (error) {
252
- console.error("Failed to load preferences:", error);
253
- showToast("載入用戶偏好失敗", "error");
254
- }
255
- }
256
-
257
- async function loadCLISettings() {
258
- try {
259
- const response = await fetch(`${API_BASE}/api/cli/settings`);
260
- const data = await response.json();
261
-
262
- if (data.success && data.settings) {
263
- const settings = data.settings;
264
-
265
- if (settings.aiMode === "cli") {
266
- elements.aiModeCli.checked = true;
267
- elements.cliToolGroup.style.display = "block";
268
- elements.cliTimeoutGroup.style.display = "block";
269
- elements.cliFallbackGroup.style.display = "block";
270
- } else {
271
- elements.aiModeApi.checked = true;
272
- }
273
-
274
- elements.cliTool.value = settings.cliTool || "gemini";
275
- elements.cliTimeout.value = Math.round((settings.cliTimeout || 120000) / 1000);
276
- elements.cliFallbackToApi.checked = settings.cliFallbackToApi !== false;
277
-
278
- // 如果是 CLI 模式,檢測工具
279
- if (settings.aiMode === "cli") {
280
- detectCLITools();
281
- }
282
- }
283
- } catch (error) {
284
- console.error("Failed to load CLI settings:", error);
285
- }
286
- }
287
-
288
- async function detectCLITools() {
289
- elements.cliToolStatus.textContent = "正在檢測已安裝的 CLI 工具...";
290
- elements.detectCliBtn.disabled = true;
291
-
292
- try {
293
- const response = await fetch(`${API_BASE}/api/cli/detect?refresh=true`);
294
- const data = await response.json();
295
-
296
- if (data.success && data.tools) {
297
- cliDetectionResult = data.tools;
298
-
299
- const installedTools = data.tools.filter(t => t.installed);
300
-
301
- if (installedTools.length === 0) {
302
- elements.cliToolStatus.textContent = "⚠️ 未檢測到任何 CLI 工具,請先安裝 Gemini CLI 或 Claude CLI";
303
- elements.cliToolStatus.style.color = "var(--accent-orange)";
304
- } else {
305
- const toolNames = installedTools.map(t => `${t.name} (v${t.version})`).join(", ");
306
- elements.cliToolStatus.textContent = `✅ 已檢測到: ${toolNames}`;
307
- elements.cliToolStatus.style.color = "var(--accent-green)";
308
-
309
- // 更新下拉選單
310
- elements.cliTool.innerHTML = "";
311
- installedTools.forEach(tool => {
312
- const option = document.createElement("option");
313
- option.value = tool.name;
314
- option.textContent = `${tool.name === "gemini" ? "Gemini CLI" : "Claude CLI"} (v${tool.version})`;
315
- elements.cliTool.appendChild(option);
316
- });
317
- }
318
- }
319
- } catch (error) {
320
- console.error("Failed to detect CLI tools:", error);
321
- elements.cliToolStatus.textContent = "❌ CLI 工具檢測失敗";
322
- elements.cliToolStatus.style.color = "var(--accent-red)";
323
- } finally {
324
- elements.detectCliBtn.disabled = false;
325
- }
326
- }
327
-
328
- async function saveCLISettings() {
329
- const settings = {
330
- aiMode: elements.aiModeCli.checked ? "cli" : "api",
331
- cliTool: elements.cliTool.value,
332
- cliTimeout: parseInt(elements.cliTimeout.value) * 1000,
333
- cliFallbackToApi: elements.cliFallbackToApi.checked,
334
- };
335
-
336
- elements.saveCliBtn.disabled = true;
337
- elements.saveCliBtn.textContent = "儲存中...";
338
-
339
- try {
340
- const response = await fetch(`${API_BASE}/api/cli/settings`, {
341
- method: "PUT",
342
- headers: { "Content-Type": "application/json" },
343
- body: JSON.stringify(settings),
344
- });
345
-
346
- const data = await response.json();
347
-
348
- if (response.ok && data.success) {
349
- showToast("CLI 設定已儲存", "success");
350
- } else {
351
- showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
352
- }
353
- } catch (error) {
354
- console.error("Save CLI settings failed:", error);
355
- showToast("儲存失敗", "error");
356
- } finally {
357
- elements.saveCliBtn.disabled = false;
358
- elements.saveCliBtn.textContent = "儲存 CLI 設定";
359
- }
360
- }
361
-
362
- async function testAIConnection() {
363
- const apiKey = elements.apiKey.value;
364
- const model = elements.aiModel.value;
365
- const provider = elements.aiProvider.value;
366
- const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
367
-
368
- // 如果 API key 是遮罩值,表示用戶沒有修改,將使用資料庫中的 key
369
- const apiKeyChanged = apiKey !== originalApiKeyMasked;
370
-
371
- if (!apiKeyChanged && !originalApiKeyMasked) {
372
- showToast("請先輸入 API 金鑰", "error");
373
- return;
374
- }
375
-
376
- if (!model) {
377
- showToast("請先選擇模型", "error");
378
- return;
379
- }
380
-
381
- elements.testAiBtn.disabled = true;
382
- elements.testAiBtn.textContent = "測試中...";
383
-
384
- try {
385
- // 傳送當前表單的設定值進行測試
386
- const payload = {
387
- model,
388
- apiUrl,
389
- openaiCompatible: elements.openaiCompatible?.checked || false
390
- };
391
- if (apiKeyChanged) {
392
- payload.apiKey = apiKey;
393
- }
394
-
395
- const response = await fetch(`${API_BASE}/api/ai-settings/validate`, {
396
- method: "POST",
397
- headers: { "Content-Type": "application/json" },
398
- body: JSON.stringify(payload),
399
- });
400
-
401
- const data = await response.json();
402
-
403
- if (response.ok && data.success && data.valid) {
404
- showToast("AI 連接測試成功!", "success");
405
- } else {
406
- showToast(`連接測試失敗: ${data.error || "未知錯誤"}`, "error");
407
- }
408
- } catch (error) {
409
- console.error("Test AI connection failed:", error);
410
- showToast("連接測試失敗", "error");
411
- } finally {
412
- elements.testAiBtn.disabled = false;
413
- elements.testAiBtn.textContent = "測試連接";
414
- }
415
- }
416
-
417
- async function saveAISettings() {
418
- const provider = elements.aiProvider.value;
419
- const currentApiKey = elements.apiKey.value;
420
-
421
- // 只有當用戶真的修改了 API key 才傳送(不是遮罩值)
422
- const apiKeyChanged = currentApiKey !== originalApiKeyMasked;
423
-
424
- // 使用表單中的 API URL,若為空則使用預設值
425
- const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
426
-
427
- const settings = {
428
- apiUrl: apiUrl,
429
- model: elements.aiModel.value,
430
- temperature: parseFloat(elements.temperature.value) || 0.7,
431
- maxTokens: parseInt(elements.maxTokens.value) || 1000,
432
- autoReplyTimerSeconds: parseInt(elements.autoReplyTimerSeconds.value) || 300,
433
- maxToolRounds: parseInt(elements.maxToolRounds.value) || 5,
434
- debugMode: elements.debugMode.checked,
435
- openaiCompatible: elements.openaiCompatible?.checked || false,
436
- };
437
-
438
- // 只有修改了 API key 才加入
439
- if (apiKeyChanged) {
440
- if (!currentApiKey) {
441
- showToast("請輸入 API 金鑰", "error");
442
- return;
443
- }
444
- settings.apiKey = currentApiKey;
445
- }
446
-
447
- elements.saveAiBtn.disabled = true;
448
- elements.saveAiBtn.textContent = "儲存中...";
449
-
450
- try {
451
- const response = await fetch(`${API_BASE}/api/ai-settings`, {
452
- method: "PUT",
453
- headers: { "Content-Type": "application/json" },
454
- body: JSON.stringify(settings),
455
- });
456
-
457
- const data = await response.json();
458
- if (response.ok && data.success) {
459
- showToast("AI 設定已儲存", "success");
460
- } else {
461
- showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
462
- }
463
- } catch (error) {
464
- console.error("Save AI settings failed:", error);
465
- showToast("儲存失敗", "error");
466
- } finally {
467
- elements.saveAiBtn.disabled = false;
468
- elements.saveAiBtn.textContent = "儲存 AI 設定";
469
- }
470
- }
471
-
472
- async function savePreferences() {
473
- const preferences = {
474
- autoSubmitOnTimeout: elements.autoSubmitOnTimeout.checked,
475
- confirmBeforeSubmit: elements.confirmBeforeSubmit.checked,
476
- defaultLanguage: elements.defaultLanguage.value,
477
- };
478
-
479
- elements.savePreferencesBtn.disabled = true;
480
- elements.savePreferencesBtn.textContent = "儲存中...";
481
-
482
- try {
483
- const response = await fetch(`${API_BASE}/api/preferences`, {
484
- method: "PUT",
485
- headers: { "Content-Type": "application/json" },
486
- body: JSON.stringify(preferences),
487
- });
488
-
489
- const data = await response.json();
490
- if (response.ok && data.success) {
491
- showToast("偏好設定已儲存", "success");
492
- } else {
493
- showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
494
- }
495
- } catch (error) {
496
- console.error("Save preferences failed:", error);
497
- showToast("儲存失敗", "error");
498
- } finally {
499
- elements.savePreferencesBtn.disabled = false;
500
- elements.savePreferencesBtn.textContent = "儲存偏好設定";
501
- }
502
- }
503
-
504
- // ============ Self-Probe Settings ============
505
-
506
- async function loadSelfProbeSettings() {
507
- try {
508
- const response = await fetch(`${API_BASE}/api/settings/self-probe`);
509
- const data = await response.json();
510
-
511
- if (data.success) {
512
- const settings = data.settings || {};
513
- const stats = data.stats || {};
514
-
515
- if (elements.enableSelfProbe) {
516
- elements.enableSelfProbe.checked = settings.enabled || false;
517
- }
518
- if (elements.selfProbeInterval) {
519
- elements.selfProbeInterval.value = settings.intervalSeconds || 300;
520
- }
521
-
522
- // 更新狀態資訊
523
- updateSelfProbeStatus(stats);
524
- handleSelfProbeToggle();
525
- }
526
- } catch (error) {
527
- console.error("Failed to load Self-Probe settings:", error);
528
- }
529
- }
530
-
531
- function updateSelfProbeStatus(stats) {
532
- if (!elements.selfProbeStatus) return;
533
-
534
- if (stats.enabled) {
535
- elements.selfProbeStatus.style.display = "block";
536
-
537
- if (elements.selfProbeRunning) {
538
- elements.selfProbeRunning.textContent = `執行狀態: ${stats.isRunning ? "✅ 運行中" : "⏸️ 已停止"}`;
539
- }
540
- if (elements.selfProbeCount) {
541
- elements.selfProbeCount.textContent = `探查次數: ${stats.probeCount || 0}`;
542
- }
543
- if (elements.selfProbeLastTime) {
544
- const lastTime = stats.lastProbeTime
545
- ? new Date(stats.lastProbeTime).toLocaleString()
546
- : "尚未執行";
547
- elements.selfProbeLastTime.textContent = `上次探查: ${lastTime}`;
548
- }
549
- } else {
550
- elements.selfProbeStatus.style.display = "none";
551
- }
552
- }
553
-
554
- async function saveSelfProbeSettings() {
555
- const settings = {
556
- enabled: elements.enableSelfProbe?.checked || false,
557
- intervalSeconds: parseInt(elements.selfProbeInterval?.value) || 300,
558
- };
559
-
560
- // 驗證間隔
561
- if (settings.intervalSeconds < 60 || settings.intervalSeconds > 600) {
562
- showToast("探查間隔必須在 60-600 秒之間", "error");
563
- return;
564
- }
565
-
566
- if (elements.saveSelfProbeBtn) {
567
- elements.saveSelfProbeBtn.disabled = true;
568
- elements.saveSelfProbeBtn.textContent = "儲存中...";
569
- }
570
-
571
- try {
572
- const response = await fetch(`${API_BASE}/api/settings/self-probe`, {
573
- method: "POST",
574
- headers: { "Content-Type": "application/json" },
575
- body: JSON.stringify(settings),
576
- });
577
-
578
- const data = await response.json();
579
-
580
- if (response.ok && data.success) {
581
- showToast("Self-Probe 設定已儲存", "success");
582
- updateSelfProbeStatus(data.stats);
583
- } else {
584
- showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
585
- }
586
- } catch (error) {
587
- console.error("Save Self-Probe settings failed:", error);
588
- showToast("儲存失敗", "error");
589
- } finally {
590
- if (elements.saveSelfProbeBtn) {
591
- elements.saveSelfProbeBtn.disabled = false;
592
- elements.saveSelfProbeBtn.textContent = "儲存設定";
593
- }
594
- }
595
- }
596
-
597
- // ============ Prompt Config Functions ============
598
-
599
- let promptConfigs = [];
600
-
601
- async function loadPromptConfigs() {
602
- if (!elements.promptConfigList) return;
603
-
604
- try {
605
- const response = await fetch(`${API_BASE}/api/settings/prompts`);
606
- const data = await response.json();
607
-
608
- if (data.success && data.prompts) {
609
- promptConfigs = data.prompts;
610
- renderPromptConfigs();
611
- } else {
612
- elements.promptConfigList.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-muted);">無法載入配置</div>';
613
- }
614
- } catch (error) {
615
- console.error("Load prompt configs failed:", error);
616
- elements.promptConfigList.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-muted);">載入失敗</div>';
617
- }
618
- }
619
-
620
- function renderPromptConfigs() {
621
- if (!elements.promptConfigList || !promptConfigs.length) return;
622
-
623
- const showEditor = (id) => id !== 'user_context' && id !== 'tool_results' && id !== 'mcp_tools_detailed';
624
-
625
- elements.promptConfigList.innerHTML = promptConfigs.map(config => `
626
- <div class="prompt-config-item" data-id="${config.id}" style="background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 16px;">
627
- <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: ${showEditor(config.id) ? '12px' : '0'};">
628
- <span style="font-weight: 600; color: var(--text-primary); font-size: 14px;">${config.displayName}</span>
629
- <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
630
- <label style="display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary);">
631
- 第一次:
632
- <input type="number" class="first-order form-input" value="${config.firstOrder}" min="0" max="1000" step="10" style="width: 60px; padding: 4px 8px;">
633
- </label>
634
- <label style="display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary);">
635
- 第二次:
636
- <input type="number" class="second-order form-input" value="${config.secondOrder}" min="0" max="1000" step="10" style="width: 60px; padding: 4px 8px;">
637
- </label>
638
- <label style="display: flex; align-items: center; gap: 6px; font-size: 13px;">
639
- <input type="checkbox" class="prompt-enabled" ${config.enabled ? 'checked' : ''}>
640
- 啟用
641
- </label>
642
- </div>
643
- </div>
644
- ${showEditor(config.id) ? `
645
- <textarea class="prompt-content form-textarea" style="min-height: 100px;">${config.content || ''}</textarea>
646
- ` : ''}
647
- </div>
648
- `).join('');
649
- }
650
-
651
- async function savePromptConfigs() {
652
- if (!elements.savePromptsBtn) return;
653
-
654
- elements.savePromptsBtn.disabled = true;
655
- elements.savePromptsBtn.textContent = "儲存中...";
656
-
657
- try {
658
- const items = document.querySelectorAll('.prompt-config-item');
659
- const updates = [];
660
-
661
- items.forEach(item => {
662
- const id = item.dataset.id;
663
- const firstOrder = parseInt(item.querySelector('.first-order').value) || 0;
664
- const secondOrder = parseInt(item.querySelector('.second-order').value) || 0;
665
- const enabled = item.querySelector('.prompt-enabled').checked;
666
- const contentEl = item.querySelector('.prompt-content');
667
- const content = contentEl ? contentEl.value || null : null;
668
-
669
- updates.push({ id, firstOrder, secondOrder, enabled, content });
670
- });
671
-
672
- const response = await fetch(`${API_BASE}/api/settings/prompts`, {
673
- method: 'PUT',
674
- headers: { 'Content-Type': 'application/json' },
675
- body: JSON.stringify({ prompts: updates })
676
- });
677
-
678
- const data = await response.json();
679
-
680
- if (data.success) {
681
- showToast("提示詞配置已儲存", "success");
682
- if (data.prompts) {
683
- promptConfigs = data.prompts;
684
- }
685
- } else {
686
- showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
687
- }
688
- } catch (error) {
689
- console.error("Save prompt configs failed:", error);
690
- showToast("儲存失敗", "error");
691
- } finally {
692
- elements.savePromptsBtn.disabled = false;
693
- elements.savePromptsBtn.textContent = "儲存提示詞設定";
694
- }
695
- }
696
-
697
- async function resetPromptConfigs() {
698
- if (!confirm("確定要重置為預設配置?")) return;
699
- if (!elements.resetPromptsBtn) return;
700
-
701
- elements.resetPromptsBtn.disabled = true;
702
- elements.resetPromptsBtn.textContent = "重置中...";
703
-
704
- try {
705
- const response = await fetch(`${API_BASE}/api/settings/prompts/reset`, { method: 'POST' });
706
- const data = await response.json();
707
-
708
- if (data.success) {
709
- showToast("已重置為預設配置", "success");
710
- if (data.prompts) {
711
- promptConfigs = data.prompts;
712
- renderPromptConfigs();
713
- }
714
- } else {
715
- showToast(`重置失敗: ${data.error || "未知錯誤"}`, "error");
716
- }
717
- } catch (error) {
718
- console.error("Reset prompt configs failed:", error);
719
- showToast("重置失敗", "error");
720
- } finally {
721
- elements.resetPromptsBtn.disabled = false;
722
- elements.resetPromptsBtn.textContent = "恢復預設";
723
- }
724
- }
725
-
726
- function showToast(message, type = "info") {
727
- const toast = document.createElement("div");
728
- toast.className = `toast toast-${type}`;
729
-
730
- const messageSpan = document.createElement("span");
731
- messageSpan.textContent = message;
732
- toast.appendChild(messageSpan);
733
-
734
- // 添加關閉按鈕
735
- const closeBtn = document.createElement("button");
736
- closeBtn.textContent = "×";
737
- closeBtn.style.cssText = "margin-left: 12px; background: none; border: none; color: inherit; font-size: 18px; cursor: pointer; padding: 0 4px;";
738
- closeBtn.onclick = () => {
739
- toast.classList.remove("show");
740
- setTimeout(() => toast.remove(), 300);
741
- };
742
- toast.appendChild(closeBtn);
743
-
744
- elements.toastContainer.appendChild(toast);
745
-
746
- setTimeout(() => {
747
- toast.classList.add("show");
748
- }, 10);
749
-
750
- // 錯誤訊息不自動關閉,其他類型 3 秒後關閉
751
- if (type !== "error") {
752
- setTimeout(() => {
753
- toast.classList.remove("show");
754
- setTimeout(() => {
755
- toast.remove();
756
- }, 300);
757
- }, 3000);
758
- }
759
- }
760
-
761
- if (document.readyState === "loading") {
762
- document.addEventListener("DOMContentLoaded", init);
763
- } else {
764
- init();
765
- }
766
- })();
767
-
1
+ /**
2
+ * 系統設定頁面
3
+ */
4
+
5
+ (function () {
6
+ "use strict";
7
+
8
+ const API_BASE = "";
9
+
10
+ // Provider 與 API URL 的對應表 (key 需與 HTML select option value 一致)
11
+ const PROVIDER_API_MAP = {
12
+ openai: "https://api.openai.com/v1",
13
+ google: "https://generativelanguage.googleapis.com/v1beta",
14
+ anthropic: "https://api.anthropic.com/v1",
15
+ local: "http://localhost:11434/v1"
16
+ };
17
+
18
+ // 反向查詢:從 API URL 取得 Provider
19
+ function getProviderFromApiUrl(apiUrl) {
20
+ if (!apiUrl) return "openai";
21
+ const normalizedUrl = apiUrl.toLowerCase();
22
+ if (normalizedUrl.includes("generativelanguage.googleapis.com")) return "google";
23
+ if (normalizedUrl.includes("api.anthropic.com")) return "anthropic";
24
+ if (normalizedUrl.includes("nvidia.com")) return "nvidia";
25
+ if (normalizedUrl.includes("bigmodel.cn") || normalizedUrl.includes("z.ai")) return "zai";
26
+ if (normalizedUrl.includes("api.openai.com")) return "openai";
27
+ return "openai"; // 預設
28
+ }
29
+
30
+ // 從 Provider 取得 API URL
31
+ function getApiUrlFromProvider(provider) {
32
+ return PROVIDER_API_MAP[provider] || PROVIDER_API_MAP.openai;
33
+ }
34
+
35
+ const elements = {
36
+ // AI Settings
37
+ aiProvider: document.getElementById("aiProvider"),
38
+ apiUrl: document.getElementById("apiUrl"),
39
+ openaiCompatible: document.getElementById("openaiCompatible"),
40
+ apiKey: document.getElementById("apiKey"),
41
+ toggleApiKey: document.getElementById("toggleApiKey"),
42
+ aiModel: document.getElementById("aiModel"),
43
+ temperature: document.getElementById("temperature"),
44
+ maxTokens: document.getElementById("maxTokens"),
45
+ autoReplyTimerSeconds: document.getElementById("autoReplyTimerSeconds"),
46
+ maxToolRounds: document.getElementById("maxToolRounds"),
47
+ debugMode: document.getElementById("debugMode"),
48
+ testAiBtn: document.getElementById("testAiBtn"),
49
+ saveAiBtn: document.getElementById("saveAiBtn"),
50
+ // CLI Settings
51
+ aiModeApi: document.getElementById("aiModeApi"),
52
+ aiModeCli: document.getElementById("aiModeCli"),
53
+ cliTool: document.getElementById("cliTool"),
54
+ cliToolGroup: document.getElementById("cliToolGroup"),
55
+ cliToolStatus: document.getElementById("cliToolStatus"),
56
+ cliTimeout: document.getElementById("cliTimeout"),
57
+ cliTimeoutGroup: document.getElementById("cliTimeoutGroup"),
58
+ cliFallbackToApi: document.getElementById("cliFallbackToApi"),
59
+ cliFallbackGroup: document.getElementById("cliFallbackGroup"),
60
+ detectCliBtn: document.getElementById("detectCliBtn"),
61
+ saveCliBtn: document.getElementById("saveCliBtn"),
62
+ // User Preferences
63
+ autoSubmitOnTimeout: document.getElementById("autoSubmitOnTimeout"),
64
+ confirmBeforeSubmit: document.getElementById("confirmBeforeSubmit"),
65
+ defaultLanguage: document.getElementById("defaultLanguage"),
66
+ savePreferencesBtn: document.getElementById("savePreferencesBtn"),
67
+ // Self-Probe Settings
68
+ enableSelfProbe: document.getElementById("enableSelfProbe"),
69
+ selfProbeInterval: document.getElementById("selfProbeInterval"),
70
+ selfProbeIntervalGroup: document.getElementById("selfProbeIntervalGroup"),
71
+ selfProbeStatus: document.getElementById("selfProbeStatus"),
72
+ selfProbeRunning: document.getElementById("selfProbeRunning"),
73
+ selfProbeCount: document.getElementById("selfProbeCount"),
74
+ selfProbeLastTime: document.getElementById("selfProbeLastTime"),
75
+ saveSelfProbeBtn: document.getElementById("saveSelfProbeBtn"),
76
+ // Prompt Config Settings
77
+ promptConfigList: document.getElementById("promptConfigList"),
78
+ resetPromptsBtn: document.getElementById("resetPromptsBtn"),
79
+ savePromptsBtn: document.getElementById("savePromptsBtn"),
80
+ // Extended Provider Settings (integrated into AI settings)
81
+ zaiExtSettings: document.getElementById("zaiExtSettings"),
82
+ zaiRegion: document.getElementById("zaiRegion"),
83
+ toastContainer: document.getElementById("toastContainer"),
84
+ };
85
+
86
+ // CLI 工具檢測結果緩存
87
+ let cliDetectionResult = null;
88
+ // 追蹤原始的 apiKeyMasked,用於判斷用戶是否修改了 API key
89
+ let originalApiKeyMasked = "";
90
+
91
+ function init() {
92
+ setupEventListeners();
93
+ loadAISettings();
94
+ loadCLISettings();
95
+ loadPreferences();
96
+ loadSelfProbeSettings();
97
+ loadPromptConfigs();
98
+ }
99
+
100
+ function setupEventListeners() {
101
+ // AI Settings
102
+ elements.toggleApiKey.addEventListener("click", toggleApiKeyVisibility);
103
+ elements.testAiBtn.addEventListener("click", testAIConnection);
104
+ elements.saveAiBtn.addEventListener("click", saveAISettings);
105
+ if (elements.aiProvider) {
106
+ elements.aiProvider.addEventListener("change", handleAIProviderChange);
107
+ }
108
+ if (elements.zaiRegion) {
109
+ elements.zaiRegion.addEventListener("change", handleZaiRegionChange);
110
+ }
111
+
112
+ // CLI Settings
113
+ elements.aiModeApi.addEventListener("change", handleAIModeChange);
114
+ elements.aiModeCli.addEventListener("change", handleAIModeChange);
115
+ elements.detectCliBtn.addEventListener("click", detectCLITools);
116
+ elements.saveCliBtn.addEventListener("click", saveCLISettings);
117
+
118
+ // User Preferences
119
+ elements.savePreferencesBtn.addEventListener("click", savePreferences);
120
+
121
+ // Self-Probe Settings
122
+ if (elements.enableSelfProbe) {
123
+ elements.enableSelfProbe.addEventListener("change", handleSelfProbeToggle);
124
+ }
125
+ if (elements.saveSelfProbeBtn) {
126
+ elements.saveSelfProbeBtn.addEventListener("click", saveSelfProbeSettings);
127
+ }
128
+
129
+ // Prompt Config Settings
130
+ if (elements.resetPromptsBtn) {
131
+ elements.resetPromptsBtn.addEventListener("click", resetPromptConfigs);
132
+ }
133
+ if (elements.savePromptsBtn) {
134
+ elements.savePromptsBtn.addEventListener("click", savePromptConfigs);
135
+ }
136
+ }
137
+
138
+ const DEFAULT_API_URLS = {
139
+ openai: 'https://api.openai.com/v1',
140
+ anthropic: 'https://api.anthropic.com/v1',
141
+ google: 'https://generativelanguage.googleapis.com/v1beta',
142
+ nvidia: 'https://integrate.api.nvidia.com/v1',
143
+ zai: 'https://api.z.ai/api/coding/paas/v4',
144
+ 'zai-china': 'https://open.bigmodel.cn/api/paas/v4'
145
+ };
146
+
147
+ function handleAIProviderChange(updateUrl = true) {
148
+ const provider = elements.aiProvider?.value || 'google';
149
+
150
+ // Z.AI 專用設定
151
+ if (elements.zaiExtSettings) {
152
+ elements.zaiExtSettings.style.display = provider === 'zai' ? 'block' : 'none';
153
+ }
154
+
155
+ // 更新預設 API URL(僅當 updateUrl 為 true 時)
156
+ if (updateUrl && elements.apiUrl) {
157
+ let defaultUrl;
158
+ if (provider === 'zai') {
159
+ const region = elements.zaiRegion?.value || 'international';
160
+ defaultUrl = region === 'china' ? DEFAULT_API_URLS['zai-china'] : DEFAULT_API_URLS.zai;
161
+ } else {
162
+ defaultUrl = DEFAULT_API_URLS[provider] || '';
163
+ }
164
+ elements.apiUrl.value = defaultUrl;
165
+ elements.apiUrl.placeholder = defaultUrl || 'API 端點 URL';
166
+ }
167
+ }
168
+
169
+ function handleZaiRegionChange() {
170
+ const region = elements.zaiRegion?.value || 'international';
171
+ if (elements.apiUrl) {
172
+ const defaultUrl = region === 'china' ? DEFAULT_API_URLS['zai-china'] : DEFAULT_API_URLS.zai;
173
+ elements.apiUrl.value = defaultUrl;
174
+ elements.apiUrl.placeholder = defaultUrl || 'API 端點 URL';
175
+ }
176
+ }
177
+
178
+ function handleSelfProbeToggle() {
179
+ const isEnabled = elements.enableSelfProbe.checked;
180
+ if (elements.selfProbeIntervalGroup) {
181
+ elements.selfProbeIntervalGroup.style.opacity = isEnabled ? "1" : "0.5";
182
+ }
183
+ }
184
+
185
+ function handleAIModeChange() {
186
+ const isCLIMode = elements.aiModeCli.checked;
187
+ elements.cliToolGroup.style.display = isCLIMode ? "block" : "none";
188
+ elements.cliTimeoutGroup.style.display = isCLIMode ? "block" : "none";
189
+ elements.cliFallbackGroup.style.display = isCLIMode ? "block" : "none";
190
+
191
+ if (isCLIMode && !cliDetectionResult) {
192
+ detectCLITools();
193
+ }
194
+ }
195
+
196
+ function toggleApiKeyVisibility() {
197
+ const type = elements.apiKey.type;
198
+ elements.apiKey.type = type === "password" ? "text" : "password";
199
+ elements.toggleApiKey.textContent = type === "password" ? "🙈" : "👁️";
200
+ }
201
+
202
+ async function loadAISettings() {
203
+ try {
204
+ const response = await fetch(`${API_BASE}/api/ai-settings`);
205
+ const data = await response.json();
206
+
207
+ if (data.settings) {
208
+ // 從 apiUrl 反向推斷 provider
209
+ const provider = getProviderFromApiUrl(data.settings.apiUrl);
210
+ elements.aiProvider.value = provider;
211
+ // 設置 API URL
212
+ if (elements.apiUrl) {
213
+ elements.apiUrl.value = data.settings.apiUrl || DEFAULT_API_URLS[provider] || '';
214
+ }
215
+ // OpenAI 相容模式
216
+ if (elements.openaiCompatible) {
217
+ elements.openaiCompatible.checked = data.settings.openaiCompatible || false;
218
+ }
219
+ // API 返回的是 apiKeyMasked(遮罩後的 key),顯示給用戶看
220
+ originalApiKeyMasked = data.settings.apiKeyMasked || "";
221
+ elements.apiKey.value = originalApiKeyMasked;
222
+ elements.apiKey.placeholder = originalApiKeyMasked ? "輸入新的 API Key 以更換" : "請輸入 API Key";
223
+ elements.aiModel.value = data.settings.model || "";
224
+ elements.temperature.value = data.settings.temperature ?? 0.7;
225
+ elements.maxTokens.value = data.settings.maxTokens ?? 1000;
226
+ elements.autoReplyTimerSeconds.value = data.settings.autoReplyTimerSeconds ?? 300;
227
+ elements.maxToolRounds.value = data.settings.maxToolRounds ?? 5;
228
+ elements.debugMode.checked = data.settings.debugMode || false;
229
+ // 更新 UI(不更新 URL,因為已經從資料庫載入)
230
+ handleAIProviderChange(false);
231
+ }
232
+ } catch (error) {
233
+ console.error("Failed to load AI settings:", error);
234
+ showToast("載入 AI 設定失敗", "error");
235
+ }
236
+ }
237
+
238
+ async function loadPreferences() {
239
+ try {
240
+ const response = await fetch(`${API_BASE}/api/preferences`);
241
+ const data = await response.json();
242
+
243
+ if (data.preferences) {
244
+ elements.autoSubmitOnTimeout.checked =
245
+ data.preferences.autoSubmitOnTimeout || false;
246
+ elements.confirmBeforeSubmit.checked =
247
+ data.preferences.confirmBeforeSubmit || false;
248
+ elements.defaultLanguage.value =
249
+ data.preferences.defaultLanguage || "zh-TW";
250
+ }
251
+ } catch (error) {
252
+ console.error("Failed to load preferences:", error);
253
+ showToast("載入用戶偏好失敗", "error");
254
+ }
255
+ }
256
+
257
+ async function loadCLISettings() {
258
+ try {
259
+ const response = await fetch(`${API_BASE}/api/cli/settings`);
260
+ const data = await response.json();
261
+
262
+ if (data.success && data.settings) {
263
+ const settings = data.settings;
264
+
265
+ if (settings.aiMode === "cli") {
266
+ elements.aiModeCli.checked = true;
267
+ elements.cliToolGroup.style.display = "block";
268
+ elements.cliTimeoutGroup.style.display = "block";
269
+ elements.cliFallbackGroup.style.display = "block";
270
+ } else {
271
+ elements.aiModeApi.checked = true;
272
+ }
273
+
274
+ elements.cliTool.value = settings.cliTool || "gemini";
275
+ elements.cliTimeout.value = Math.round((settings.cliTimeout || 120000) / 1000);
276
+ elements.cliFallbackToApi.checked = settings.cliFallbackToApi !== false;
277
+
278
+ // 如果是 CLI 模式,檢測工具
279
+ if (settings.aiMode === "cli") {
280
+ detectCLITools();
281
+ }
282
+ }
283
+ } catch (error) {
284
+ console.error("Failed to load CLI settings:", error);
285
+ }
286
+ }
287
+
288
+ async function detectCLITools() {
289
+ elements.cliToolStatus.textContent = "正在檢測已安裝的 CLI 工具...";
290
+ elements.detectCliBtn.disabled = true;
291
+
292
+ try {
293
+ const response = await fetch(`${API_BASE}/api/cli/detect?refresh=true`);
294
+ const data = await response.json();
295
+
296
+ if (data.success && data.tools) {
297
+ cliDetectionResult = data.tools;
298
+
299
+ const installedTools = data.tools.filter(t => t.installed);
300
+
301
+ if (installedTools.length === 0) {
302
+ elements.cliToolStatus.textContent = "⚠️ 未檢測到任何 CLI 工具,請先安裝 Gemini CLI 或 Claude CLI";
303
+ elements.cliToolStatus.style.color = "var(--accent-orange)";
304
+ } else {
305
+ const toolNames = installedTools.map(t => `${t.name} (v${t.version})`).join(", ");
306
+ elements.cliToolStatus.textContent = `✅ 已檢測到: ${toolNames}`;
307
+ elements.cliToolStatus.style.color = "var(--accent-green)";
308
+
309
+ // 更新下拉選單
310
+ elements.cliTool.innerHTML = "";
311
+ installedTools.forEach(tool => {
312
+ const option = document.createElement("option");
313
+ option.value = tool.name;
314
+ option.textContent = `${tool.name === "gemini" ? "Gemini CLI" : "Claude CLI"} (v${tool.version})`;
315
+ elements.cliTool.appendChild(option);
316
+ });
317
+ }
318
+ }
319
+ } catch (error) {
320
+ console.error("Failed to detect CLI tools:", error);
321
+ elements.cliToolStatus.textContent = "❌ CLI 工具檢測失敗";
322
+ elements.cliToolStatus.style.color = "var(--accent-red)";
323
+ } finally {
324
+ elements.detectCliBtn.disabled = false;
325
+ }
326
+ }
327
+
328
+ async function saveCLISettings() {
329
+ const settings = {
330
+ aiMode: elements.aiModeCli.checked ? "cli" : "api",
331
+ cliTool: elements.cliTool.value,
332
+ cliTimeout: parseInt(elements.cliTimeout.value) * 1000,
333
+ cliFallbackToApi: elements.cliFallbackToApi.checked,
334
+ };
335
+
336
+ elements.saveCliBtn.disabled = true;
337
+ elements.saveCliBtn.textContent = "儲存中...";
338
+
339
+ try {
340
+ const response = await fetch(`${API_BASE}/api/cli/settings`, {
341
+ method: "PUT",
342
+ headers: { "Content-Type": "application/json" },
343
+ body: JSON.stringify(settings),
344
+ });
345
+
346
+ const data = await response.json();
347
+
348
+ if (response.ok && data.success) {
349
+ showToast("CLI 設定已儲存", "success");
350
+ } else {
351
+ showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
352
+ }
353
+ } catch (error) {
354
+ console.error("Save CLI settings failed:", error);
355
+ showToast("儲存失敗", "error");
356
+ } finally {
357
+ elements.saveCliBtn.disabled = false;
358
+ elements.saveCliBtn.textContent = "儲存 CLI 設定";
359
+ }
360
+ }
361
+
362
+ async function testAIConnection() {
363
+ const apiKey = elements.apiKey.value;
364
+ const model = elements.aiModel.value;
365
+ const provider = elements.aiProvider.value;
366
+ const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
367
+
368
+ // 如果 API key 是遮罩值,表示用戶沒有修改,將使用資料庫中的 key
369
+ const apiKeyChanged = apiKey !== originalApiKeyMasked;
370
+
371
+ if (!apiKeyChanged && !originalApiKeyMasked) {
372
+ showToast("請先輸入 API 金鑰", "error");
373
+ return;
374
+ }
375
+
376
+ if (!model) {
377
+ showToast("請先選擇模型", "error");
378
+ return;
379
+ }
380
+
381
+ elements.testAiBtn.disabled = true;
382
+ elements.testAiBtn.textContent = "測試中...";
383
+
384
+ try {
385
+ // 傳送當前表單的設定值進行測試
386
+ const payload = {
387
+ model,
388
+ apiUrl,
389
+ openaiCompatible: elements.openaiCompatible?.checked || false
390
+ };
391
+ if (apiKeyChanged) {
392
+ payload.apiKey = apiKey;
393
+ }
394
+
395
+ const response = await fetch(`${API_BASE}/api/ai-settings/validate`, {
396
+ method: "POST",
397
+ headers: { "Content-Type": "application/json" },
398
+ body: JSON.stringify(payload),
399
+ });
400
+
401
+ const data = await response.json();
402
+
403
+ if (response.ok && data.success && data.valid) {
404
+ showToast("AI 連接測試成功!", "success");
405
+ } else {
406
+ showToast(`連接測試失敗: ${data.error || "未知錯誤"}`, "error");
407
+ }
408
+ } catch (error) {
409
+ console.error("Test AI connection failed:", error);
410
+ showToast("連接測試失敗", "error");
411
+ } finally {
412
+ elements.testAiBtn.disabled = false;
413
+ elements.testAiBtn.textContent = "測試連接";
414
+ }
415
+ }
416
+
417
+ async function saveAISettings() {
418
+ const provider = elements.aiProvider.value;
419
+ const currentApiKey = elements.apiKey.value;
420
+
421
+ // 只有當用戶真的修改了 API key 才傳送(不是遮罩值)
422
+ const apiKeyChanged = currentApiKey !== originalApiKeyMasked;
423
+
424
+ // 使用表單中的 API URL,若為空則使用預設值
425
+ const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
426
+
427
+ const settings = {
428
+ apiUrl: apiUrl,
429
+ model: elements.aiModel.value,
430
+ temperature: parseFloat(elements.temperature.value) || 0.7,
431
+ maxTokens: parseInt(elements.maxTokens.value) || 1000,
432
+ autoReplyTimerSeconds: parseInt(elements.autoReplyTimerSeconds.value) || 300,
433
+ maxToolRounds: parseInt(elements.maxToolRounds.value) || 5,
434
+ debugMode: elements.debugMode.checked,
435
+ openaiCompatible: elements.openaiCompatible?.checked || false,
436
+ };
437
+
438
+ // 只有修改了 API key 才加入
439
+ if (apiKeyChanged) {
440
+ if (!currentApiKey) {
441
+ showToast("請輸入 API 金鑰", "error");
442
+ return;
443
+ }
444
+ settings.apiKey = currentApiKey;
445
+ }
446
+
447
+ elements.saveAiBtn.disabled = true;
448
+ elements.saveAiBtn.textContent = "儲存中...";
449
+
450
+ try {
451
+ const response = await fetch(`${API_BASE}/api/ai-settings`, {
452
+ method: "PUT",
453
+ headers: { "Content-Type": "application/json" },
454
+ body: JSON.stringify(settings),
455
+ });
456
+
457
+ const data = await response.json();
458
+ if (response.ok && data.success) {
459
+ showToast("AI 設定已儲存", "success");
460
+ } else {
461
+ showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
462
+ }
463
+ } catch (error) {
464
+ console.error("Save AI settings failed:", error);
465
+ showToast("儲存失敗", "error");
466
+ } finally {
467
+ elements.saveAiBtn.disabled = false;
468
+ elements.saveAiBtn.textContent = "儲存 AI 設定";
469
+ }
470
+ }
471
+
472
+ async function savePreferences() {
473
+ const preferences = {
474
+ autoSubmitOnTimeout: elements.autoSubmitOnTimeout.checked,
475
+ confirmBeforeSubmit: elements.confirmBeforeSubmit.checked,
476
+ defaultLanguage: elements.defaultLanguage.value,
477
+ };
478
+
479
+ elements.savePreferencesBtn.disabled = true;
480
+ elements.savePreferencesBtn.textContent = "儲存中...";
481
+
482
+ try {
483
+ const response = await fetch(`${API_BASE}/api/preferences`, {
484
+ method: "PUT",
485
+ headers: { "Content-Type": "application/json" },
486
+ body: JSON.stringify(preferences),
487
+ });
488
+
489
+ const data = await response.json();
490
+ if (response.ok && data.success) {
491
+ showToast("偏好設定已儲存", "success");
492
+ } else {
493
+ showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
494
+ }
495
+ } catch (error) {
496
+ console.error("Save preferences failed:", error);
497
+ showToast("儲存失敗", "error");
498
+ } finally {
499
+ elements.savePreferencesBtn.disabled = false;
500
+ elements.savePreferencesBtn.textContent = "儲存偏好設定";
501
+ }
502
+ }
503
+
504
+ // ============ Self-Probe Settings ============
505
+
506
+ async function loadSelfProbeSettings() {
507
+ try {
508
+ const response = await fetch(`${API_BASE}/api/settings/self-probe`);
509
+ const data = await response.json();
510
+
511
+ if (data.success) {
512
+ const settings = data.settings || {};
513
+ const stats = data.stats || {};
514
+
515
+ if (elements.enableSelfProbe) {
516
+ elements.enableSelfProbe.checked = settings.enabled || false;
517
+ }
518
+ if (elements.selfProbeInterval) {
519
+ elements.selfProbeInterval.value = settings.intervalSeconds || 300;
520
+ }
521
+
522
+ // 更新狀態資訊
523
+ updateSelfProbeStatus(stats);
524
+ handleSelfProbeToggle();
525
+ }
526
+ } catch (error) {
527
+ console.error("Failed to load Self-Probe settings:", error);
528
+ }
529
+ }
530
+
531
+ function updateSelfProbeStatus(stats) {
532
+ if (!elements.selfProbeStatus) return;
533
+
534
+ if (stats.enabled) {
535
+ elements.selfProbeStatus.style.display = "block";
536
+
537
+ if (elements.selfProbeRunning) {
538
+ elements.selfProbeRunning.textContent = `執行狀態: ${stats.isRunning ? "✅ 運行中" : "⏸️ 已停止"}`;
539
+ }
540
+ if (elements.selfProbeCount) {
541
+ elements.selfProbeCount.textContent = `探查次數: ${stats.probeCount || 0}`;
542
+ }
543
+ if (elements.selfProbeLastTime) {
544
+ const lastTime = stats.lastProbeTime
545
+ ? new Date(stats.lastProbeTime).toLocaleString()
546
+ : "尚未執行";
547
+ elements.selfProbeLastTime.textContent = `上次探查: ${lastTime}`;
548
+ }
549
+ } else {
550
+ elements.selfProbeStatus.style.display = "none";
551
+ }
552
+ }
553
+
554
+ async function saveSelfProbeSettings() {
555
+ const settings = {
556
+ enabled: elements.enableSelfProbe?.checked || false,
557
+ intervalSeconds: parseInt(elements.selfProbeInterval?.value) || 300,
558
+ };
559
+
560
+ // 驗證間隔
561
+ if (settings.intervalSeconds < 60 || settings.intervalSeconds > 600) {
562
+ showToast("探查間隔必須在 60-600 秒之間", "error");
563
+ return;
564
+ }
565
+
566
+ if (elements.saveSelfProbeBtn) {
567
+ elements.saveSelfProbeBtn.disabled = true;
568
+ elements.saveSelfProbeBtn.textContent = "儲存中...";
569
+ }
570
+
571
+ try {
572
+ const response = await fetch(`${API_BASE}/api/settings/self-probe`, {
573
+ method: "POST",
574
+ headers: { "Content-Type": "application/json" },
575
+ body: JSON.stringify(settings),
576
+ });
577
+
578
+ const data = await response.json();
579
+
580
+ if (response.ok && data.success) {
581
+ showToast("Self-Probe 設定已儲存", "success");
582
+ updateSelfProbeStatus(data.stats);
583
+ } else {
584
+ showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
585
+ }
586
+ } catch (error) {
587
+ console.error("Save Self-Probe settings failed:", error);
588
+ showToast("儲存失敗", "error");
589
+ } finally {
590
+ if (elements.saveSelfProbeBtn) {
591
+ elements.saveSelfProbeBtn.disabled = false;
592
+ elements.saveSelfProbeBtn.textContent = "儲存設定";
593
+ }
594
+ }
595
+ }
596
+
597
+ // ============ Prompt Config Functions ============
598
+
599
+ let promptConfigs = [];
600
+
601
+ async function loadPromptConfigs() {
602
+ if (!elements.promptConfigList) return;
603
+
604
+ try {
605
+ const response = await fetch(`${API_BASE}/api/settings/prompts`);
606
+ const data = await response.json();
607
+
608
+ if (data.success && data.prompts) {
609
+ promptConfigs = data.prompts;
610
+ renderPromptConfigs();
611
+ } else {
612
+ elements.promptConfigList.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-muted);">無法載入配置</div>';
613
+ }
614
+ } catch (error) {
615
+ console.error("Load prompt configs failed:", error);
616
+ elements.promptConfigList.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-muted);">載入失敗</div>';
617
+ }
618
+ }
619
+
620
+ function renderPromptConfigs() {
621
+ if (!elements.promptConfigList || !promptConfigs.length) return;
622
+
623
+ const showEditor = (id) => id !== 'user_context' && id !== 'tool_results' && id !== 'mcp_tools_detailed';
624
+
625
+ elements.promptConfigList.innerHTML = promptConfigs.map(config => `
626
+ <div class="prompt-config-item" data-id="${config.id}" style="background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 16px;">
627
+ <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: ${showEditor(config.id) ? '12px' : '0'};">
628
+ <span style="font-weight: 600; color: var(--text-primary); font-size: 14px;">${config.displayName}</span>
629
+ <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
630
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary);">
631
+ 第一次:
632
+ <input type="number" class="first-order form-input" value="${config.firstOrder}" min="0" max="1000" step="10" style="width: 60px; padding: 4px 8px;">
633
+ </label>
634
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary);">
635
+ 第二次:
636
+ <input type="number" class="second-order form-input" value="${config.secondOrder}" min="0" max="1000" step="10" style="width: 60px; padding: 4px 8px;">
637
+ </label>
638
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 13px;">
639
+ <input type="checkbox" class="prompt-enabled" ${config.enabled ? 'checked' : ''}>
640
+ 啟用
641
+ </label>
642
+ </div>
643
+ </div>
644
+ ${showEditor(config.id) ? `
645
+ <textarea class="prompt-content form-textarea" style="min-height: 100px;">${config.content || ''}</textarea>
646
+ ` : ''}
647
+ </div>
648
+ `).join('');
649
+ }
650
+
651
+ async function savePromptConfigs() {
652
+ if (!elements.savePromptsBtn) return;
653
+
654
+ elements.savePromptsBtn.disabled = true;
655
+ elements.savePromptsBtn.textContent = "儲存中...";
656
+
657
+ try {
658
+ const items = document.querySelectorAll('.prompt-config-item');
659
+ const updates = [];
660
+
661
+ items.forEach(item => {
662
+ const id = item.dataset.id;
663
+ const firstOrder = parseInt(item.querySelector('.first-order').value) || 0;
664
+ const secondOrder = parseInt(item.querySelector('.second-order').value) || 0;
665
+ const enabled = item.querySelector('.prompt-enabled').checked;
666
+ const contentEl = item.querySelector('.prompt-content');
667
+ const content = contentEl ? contentEl.value || null : null;
668
+
669
+ updates.push({ id, firstOrder, secondOrder, enabled, content });
670
+ });
671
+
672
+ const response = await fetch(`${API_BASE}/api/settings/prompts`, {
673
+ method: 'PUT',
674
+ headers: { 'Content-Type': 'application/json' },
675
+ body: JSON.stringify({ prompts: updates })
676
+ });
677
+
678
+ const data = await response.json();
679
+
680
+ if (data.success) {
681
+ showToast("提示詞配置已儲存", "success");
682
+ if (data.prompts) {
683
+ promptConfigs = data.prompts;
684
+ }
685
+ } else {
686
+ showToast(`儲存失敗: ${data.error || "未知錯誤"}`, "error");
687
+ }
688
+ } catch (error) {
689
+ console.error("Save prompt configs failed:", error);
690
+ showToast("儲存失敗", "error");
691
+ } finally {
692
+ elements.savePromptsBtn.disabled = false;
693
+ elements.savePromptsBtn.textContent = "儲存提示詞設定";
694
+ }
695
+ }
696
+
697
+ async function resetPromptConfigs() {
698
+ if (!confirm("確定要重置為預設配置?")) return;
699
+ if (!elements.resetPromptsBtn) return;
700
+
701
+ elements.resetPromptsBtn.disabled = true;
702
+ elements.resetPromptsBtn.textContent = "重置中...";
703
+
704
+ try {
705
+ const response = await fetch(`${API_BASE}/api/settings/prompts/reset`, { method: 'POST' });
706
+ const data = await response.json();
707
+
708
+ if (data.success) {
709
+ showToast("已重置為預設配置", "success");
710
+ if (data.prompts) {
711
+ promptConfigs = data.prompts;
712
+ renderPromptConfigs();
713
+ }
714
+ } else {
715
+ showToast(`重置失敗: ${data.error || "未知錯誤"}`, "error");
716
+ }
717
+ } catch (error) {
718
+ console.error("Reset prompt configs failed:", error);
719
+ showToast("重置失敗", "error");
720
+ } finally {
721
+ elements.resetPromptsBtn.disabled = false;
722
+ elements.resetPromptsBtn.textContent = "恢復預設";
723
+ }
724
+ }
725
+
726
+ function showToast(message, type = "info") {
727
+ const toast = document.createElement("div");
728
+ toast.className = `toast toast-${type}`;
729
+
730
+ const messageSpan = document.createElement("span");
731
+ messageSpan.textContent = message;
732
+ toast.appendChild(messageSpan);
733
+
734
+ // 添加關閉按鈕
735
+ const closeBtn = document.createElement("button");
736
+ closeBtn.textContent = "×";
737
+ closeBtn.style.cssText = "margin-left: 12px; background: none; border: none; color: inherit; font-size: 18px; cursor: pointer; padding: 0 4px;";
738
+ closeBtn.onclick = () => {
739
+ toast.classList.remove("show");
740
+ setTimeout(() => toast.remove(), 300);
741
+ };
742
+ toast.appendChild(closeBtn);
743
+
744
+ elements.toastContainer.appendChild(toast);
745
+
746
+ setTimeout(() => {
747
+ toast.classList.add("show");
748
+ }, 10);
749
+
750
+ // 錯誤訊息不自動關閉,其他類型 3 秒後關閉
751
+ if (type !== "error") {
752
+ setTimeout(() => {
753
+ toast.classList.remove("show");
754
+ setTimeout(() => {
755
+ toast.remove();
756
+ }, 300);
757
+ }, 3000);
758
+ }
759
+ }
760
+
761
+ if (document.readyState === "loading") {
762
+ document.addEventListener("DOMContentLoaded", init);
763
+ } else {
764
+ init();
765
+ }
766
+ })();
767
+