@hirohsu/user-web-feedback 2.8.9 → 2.8.11
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/README.md +953 -953
- package/dist/cli.cjs +33309 -46041
- package/dist/index.cjs +35773 -48507
- package/dist/static/app.js +385 -385
- package/dist/static/components/navbar.css +406 -406
- package/dist/static/components/navbar.html +49 -49
- package/dist/static/components/navbar.js +211 -211
- package/dist/static/dashboard.css +495 -495
- package/dist/static/dashboard.html +95 -95
- package/dist/static/dashboard.js +540 -540
- package/dist/static/favicon.svg +27 -27
- package/dist/static/index.html +541 -541
- package/dist/static/logs.html +376 -376
- package/dist/static/logs.js +442 -442
- package/dist/static/mcp-settings.html +797 -797
- package/dist/static/mcp-settings.js +884 -884
- package/dist/static/modules/app-core.js +124 -124
- package/dist/static/modules/conversation-panel.js +247 -247
- package/dist/static/modules/feedback-handler.js +1420 -1420
- package/dist/static/modules/image-handler.js +155 -155
- package/dist/static/modules/log-viewer.js +296 -296
- package/dist/static/modules/mcp-manager.js +474 -474
- package/dist/static/modules/prompt-manager.js +364 -364
- package/dist/static/modules/settings-manager.js +299 -299
- package/dist/static/modules/socket-manager.js +170 -170
- package/dist/static/modules/state-manager.js +352 -352
- package/dist/static/modules/timer-controller.js +243 -243
- package/dist/static/modules/ui-helpers.js +246 -246
- package/dist/static/settings.html +426 -426
- package/dist/static/settings.js +767 -767
- package/dist/static/style.css +2156 -2156
- package/dist/static/terminals.html +357 -357
- package/dist/static/terminals.js +321 -321
- package/package.json +95 -95
package/dist/static/settings.js
CHANGED
|
@@ -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
|
+
|