@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.
- package/README.md +953 -953
- package/dist/cli.cjs +35048 -22240
- package/dist/index.cjs +39804 -26994
- 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
|
@@ -1,299 +1,299 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* settings-manager.js
|
|
3
|
-
* AI 設定與使用者偏好管理模組
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
getAISettings,
|
|
8
|
-
setAISettings,
|
|
9
|
-
setPreferences,
|
|
10
|
-
setAutoReplyTimerSeconds,
|
|
11
|
-
setMaxToolRounds,
|
|
12
|
-
setDebugMode,
|
|
13
|
-
} from "./state-manager.js";
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
showToast,
|
|
17
|
-
showLoadingOverlay,
|
|
18
|
-
hideLoadingOverlay,
|
|
19
|
-
} from "./ui-helpers.js";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 載入 AI 設定
|
|
23
|
-
*/
|
|
24
|
-
export async function loadAISettings() {
|
|
25
|
-
try {
|
|
26
|
-
const response = await fetch("/api/ai-settings");
|
|
27
|
-
const data = await response.json();
|
|
28
|
-
|
|
29
|
-
if (data.success) {
|
|
30
|
-
setAISettings(data.settings);
|
|
31
|
-
|
|
32
|
-
// 讀取自動回覆計時器秒數設定
|
|
33
|
-
if (data.settings.autoReplyTimerSeconds !== undefined) {
|
|
34
|
-
setAutoReplyTimerSeconds(data.settings.autoReplyTimerSeconds);
|
|
35
|
-
console.log(
|
|
36
|
-
`從 AI 設定讀取自動回覆時間: ${data.settings.autoReplyTimerSeconds}s`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 讀取 AI 交談次數上限
|
|
41
|
-
if (data.settings.maxToolRounds !== undefined) {
|
|
42
|
-
setMaxToolRounds(data.settings.maxToolRounds);
|
|
43
|
-
console.log(
|
|
44
|
-
`從 AI 設定讀取 AI 交談次數: ${data.settings.maxToolRounds}`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 讀取 Debug 模式
|
|
49
|
-
if (data.settings.debugMode !== undefined) {
|
|
50
|
-
setDebugMode(data.settings.debugMode);
|
|
51
|
-
console.log(`從 AI 設定讀取 Debug 模式: ${data.settings.debugMode}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch (error) {
|
|
55
|
-
console.error("載入 AI 設定失敗:", error);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 載入使用者偏好
|
|
61
|
-
*/
|
|
62
|
-
export async function loadPreferences() {
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch("/api/preferences");
|
|
65
|
-
const data = await response.json();
|
|
66
|
-
|
|
67
|
-
if (data.success) {
|
|
68
|
-
setPreferences(data.preferences);
|
|
69
|
-
}
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error("載入使用者偏好失敗:", error);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 開啟 AI 設定彈窗
|
|
77
|
-
*/
|
|
78
|
-
export function openAISettingsModal() {
|
|
79
|
-
const aiSettings = getAISettings();
|
|
80
|
-
if (aiSettings) {
|
|
81
|
-
document.getElementById("apiUrl").value = aiSettings.apiUrl || "";
|
|
82
|
-
document.getElementById("model").value = aiSettings.model || "";
|
|
83
|
-
// API Key 欄位預設為空,不從資料庫讀取
|
|
84
|
-
document.getElementById("apiKey").value = "";
|
|
85
|
-
document.getElementById("apiKey").placeholder = "留空則保留原有 API Key";
|
|
86
|
-
document.getElementById("systemPrompt").value =
|
|
87
|
-
aiSettings.systemPrompt || "";
|
|
88
|
-
document.getElementById("mcpToolsPrompt").value =
|
|
89
|
-
aiSettings.mcpToolsPrompt || "";
|
|
90
|
-
document.getElementById("temperature").value =
|
|
91
|
-
aiSettings.temperature || 0.7;
|
|
92
|
-
document.getElementById("maxTokens").value = aiSettings.maxTokens || 1000;
|
|
93
|
-
document.getElementById("autoReplyTimerSeconds").value =
|
|
94
|
-
aiSettings.autoReplyTimerSeconds || 300;
|
|
95
|
-
document.getElementById("maxToolRounds").value =
|
|
96
|
-
aiSettings.maxToolRounds || 5;
|
|
97
|
-
document.getElementById("debugMode").checked =
|
|
98
|
-
aiSettings.debugMode || false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
document.getElementById("aiSettingsModal").classList.add("show");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 關閉 AI 設定彈窗
|
|
106
|
-
*/
|
|
107
|
-
export function closeAISettingsModal() {
|
|
108
|
-
document.getElementById("aiSettingsModal").classList.remove("show");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 儲存 AI 設定
|
|
113
|
-
*/
|
|
114
|
-
export async function saveAISettings() {
|
|
115
|
-
const apiUrl = document.getElementById("apiUrl").value.trim();
|
|
116
|
-
const model = document.getElementById("model").value.trim();
|
|
117
|
-
const apiKey = document.getElementById("apiKey").value.trim();
|
|
118
|
-
const systemPrompt = document.getElementById("systemPrompt").value.trim();
|
|
119
|
-
const mcpToolsPrompt = document.getElementById("mcpToolsPrompt").value.trim();
|
|
120
|
-
const temperature = parseFloat(document.getElementById("temperature").value);
|
|
121
|
-
const maxTokens = parseInt(document.getElementById("maxTokens").value);
|
|
122
|
-
const autoReplyTimerSeconds = parseInt(
|
|
123
|
-
document.getElementById("autoReplyTimerSeconds").value
|
|
124
|
-
);
|
|
125
|
-
const maxToolRoundsValue = parseInt(
|
|
126
|
-
document.getElementById("maxToolRounds").value
|
|
127
|
-
);
|
|
128
|
-
const debugModeValue = document.getElementById("debugMode").checked;
|
|
129
|
-
|
|
130
|
-
const settingsData = {
|
|
131
|
-
apiUrl: apiUrl || undefined,
|
|
132
|
-
model: model || undefined,
|
|
133
|
-
systemPrompt: systemPrompt || undefined,
|
|
134
|
-
mcpToolsPrompt: mcpToolsPrompt || undefined,
|
|
135
|
-
temperature,
|
|
136
|
-
maxTokens,
|
|
137
|
-
autoReplyTimerSeconds,
|
|
138
|
-
maxToolRounds: maxToolRoundsValue,
|
|
139
|
-
debugMode: debugModeValue,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// 只有當 API Key 不是遮罩格式且不為空時才更新
|
|
143
|
-
if (apiKey && !apiKey.startsWith("***")) {
|
|
144
|
-
settingsData.apiKey = apiKey;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
const response = await fetch("/api/ai-settings", {
|
|
149
|
-
method: "PUT",
|
|
150
|
-
headers: { "Content-Type": "application/json" },
|
|
151
|
-
body: JSON.stringify(settingsData),
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
let data;
|
|
155
|
-
try {
|
|
156
|
-
data = await response.json();
|
|
157
|
-
} catch (e) {
|
|
158
|
-
// 不是 JSON 回應,讀取純文字
|
|
159
|
-
const text = await response.text();
|
|
160
|
-
console.error("非 JSON 回應:", text);
|
|
161
|
-
showToast("error", "錯誤", `儲存失敗:${text}`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (data && data.success) {
|
|
166
|
-
setAISettings(data.settings);
|
|
167
|
-
|
|
168
|
-
// 更新自動回覆計時器秒數
|
|
169
|
-
if (data.settings.autoReplyTimerSeconds !== undefined) {
|
|
170
|
-
setAutoReplyTimerSeconds(data.settings.autoReplyTimerSeconds);
|
|
171
|
-
console.log(
|
|
172
|
-
`自動回覆時間已更新為: ${data.settings.autoReplyTimerSeconds}s`
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 更新 AI 交談次數上限
|
|
177
|
-
if (data.settings.maxToolRounds !== undefined) {
|
|
178
|
-
setMaxToolRounds(data.settings.maxToolRounds);
|
|
179
|
-
console.log(`AI 交談次數已更新為: ${data.settings.maxToolRounds}`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 更新 Debug 模式
|
|
183
|
-
if (data.settings.debugMode !== undefined) {
|
|
184
|
-
setDebugMode(data.settings.debugMode);
|
|
185
|
-
console.log(`Debug 模式已更新為: ${data.settings.debugMode}`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
closeAISettingsModal();
|
|
189
|
-
showToast("success", "成功", "AI 設定已儲存");
|
|
190
|
-
} else {
|
|
191
|
-
// 儘可能顯示詳細錯誤資訊
|
|
192
|
-
const detailParts = [];
|
|
193
|
-
if (data.error) detailParts.push(data.error);
|
|
194
|
-
if (data.details)
|
|
195
|
-
detailParts.push(
|
|
196
|
-
typeof data.details === "string"
|
|
197
|
-
? data.details
|
|
198
|
-
: JSON.stringify(data.details)
|
|
199
|
-
);
|
|
200
|
-
if (data.stack) detailParts.push(data.stack);
|
|
201
|
-
const message = detailParts.join(" \n ");
|
|
202
|
-
console.error("儲存 AI 設定失敗:", data);
|
|
203
|
-
showToast("error", "錯誤", message || "儲存 AI 設定失敗");
|
|
204
|
-
}
|
|
205
|
-
} catch (error) {
|
|
206
|
-
console.error("儲存 AI 設定失敗:", error);
|
|
207
|
-
// 如果有 response 物件,可嘗試讀取更多內容
|
|
208
|
-
if (error && error.response) {
|
|
209
|
-
try {
|
|
210
|
-
const text = await error.response.text();
|
|
211
|
-
showToast("error", "錯誤", `儲存失敗:${text}`);
|
|
212
|
-
return;
|
|
213
|
-
} catch (e) {
|
|
214
|
-
// ignore
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
showToast(
|
|
219
|
-
"error",
|
|
220
|
-
"錯誤",
|
|
221
|
-
error instanceof Error ? error.message : "儲存失敗"
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 測試 API Key
|
|
228
|
-
*/
|
|
229
|
-
export async function testAPIKey() {
|
|
230
|
-
const apiKeyInput = document.getElementById("apiKey").value.trim();
|
|
231
|
-
const model = document.getElementById("model").value.trim();
|
|
232
|
-
|
|
233
|
-
if (!model) {
|
|
234
|
-
showToast("error", "錯誤", "請輸入模型名稱");
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
showLoadingOverlay("正在測試 API Key...");
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const requestBody = { model };
|
|
242
|
-
|
|
243
|
-
// 判斷是否使用新輸入的 API Key:
|
|
244
|
-
// 1. API Key 不為空
|
|
245
|
-
// 2. API Key 不是遮罩格式(不以 *** 開頭)
|
|
246
|
-
// 如果是遮罩格式或為空,後端會自動使用資料庫中解密的 API Key
|
|
247
|
-
if (apiKeyInput && !apiKeyInput.startsWith("***")) {
|
|
248
|
-
requestBody.apiKey = apiKeyInput;
|
|
249
|
-
console.log("使用新輸入的 API Key 進行測試");
|
|
250
|
-
} else {
|
|
251
|
-
console.log("使用資料庫中儲存的 API Key 進行測試");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const response = await fetch("/api/ai-settings/validate", {
|
|
255
|
-
method: "POST",
|
|
256
|
-
headers: { "Content-Type": "application/json" },
|
|
257
|
-
body: JSON.stringify(requestBody),
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const data = await response.json();
|
|
261
|
-
|
|
262
|
-
if (data.valid) {
|
|
263
|
-
showToast("success", "測試成功", "API Key 有效");
|
|
264
|
-
} else {
|
|
265
|
-
showToast("error", "測試失敗", data.error || "API Key 無效");
|
|
266
|
-
}
|
|
267
|
-
} catch (error) {
|
|
268
|
-
console.error("測試 API Key 失敗:", error);
|
|
269
|
-
showToast("error", "錯誤", "測試失敗");
|
|
270
|
-
} finally {
|
|
271
|
-
hideLoadingOverlay();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* 切換 API Key 可見性
|
|
277
|
-
*/
|
|
278
|
-
export function toggleAPIKeyVisibility() {
|
|
279
|
-
const apiKeyInput = document.getElementById("apiKey");
|
|
280
|
-
const toggleBtn = document.getElementById("toggleApiKey");
|
|
281
|
-
|
|
282
|
-
if (apiKeyInput.type === "password") {
|
|
283
|
-
apiKeyInput.type = "text";
|
|
284
|
-
toggleBtn.innerHTML = '<span class="icon">🙈</span>';
|
|
285
|
-
} else {
|
|
286
|
-
apiKeyInput.type = "password";
|
|
287
|
-
toggleBtn.innerHTML = '<span class="icon">👁️</span>';
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export default {
|
|
292
|
-
loadAISettings,
|
|
293
|
-
loadPreferences,
|
|
294
|
-
openAISettingsModal,
|
|
295
|
-
closeAISettingsModal,
|
|
296
|
-
saveAISettings,
|
|
297
|
-
testAPIKey,
|
|
298
|
-
toggleAPIKeyVisibility,
|
|
299
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* settings-manager.js
|
|
3
|
+
* AI 設定與使用者偏好管理模組
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getAISettings,
|
|
8
|
+
setAISettings,
|
|
9
|
+
setPreferences,
|
|
10
|
+
setAutoReplyTimerSeconds,
|
|
11
|
+
setMaxToolRounds,
|
|
12
|
+
setDebugMode,
|
|
13
|
+
} from "./state-manager.js";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
showToast,
|
|
17
|
+
showLoadingOverlay,
|
|
18
|
+
hideLoadingOverlay,
|
|
19
|
+
} from "./ui-helpers.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 載入 AI 設定
|
|
23
|
+
*/
|
|
24
|
+
export async function loadAISettings() {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch("/api/ai-settings");
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
|
|
29
|
+
if (data.success) {
|
|
30
|
+
setAISettings(data.settings);
|
|
31
|
+
|
|
32
|
+
// 讀取自動回覆計時器秒數設定
|
|
33
|
+
if (data.settings.autoReplyTimerSeconds !== undefined) {
|
|
34
|
+
setAutoReplyTimerSeconds(data.settings.autoReplyTimerSeconds);
|
|
35
|
+
console.log(
|
|
36
|
+
`從 AI 設定讀取自動回覆時間: ${data.settings.autoReplyTimerSeconds}s`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 讀取 AI 交談次數上限
|
|
41
|
+
if (data.settings.maxToolRounds !== undefined) {
|
|
42
|
+
setMaxToolRounds(data.settings.maxToolRounds);
|
|
43
|
+
console.log(
|
|
44
|
+
`從 AI 設定讀取 AI 交談次數: ${data.settings.maxToolRounds}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 讀取 Debug 模式
|
|
49
|
+
if (data.settings.debugMode !== undefined) {
|
|
50
|
+
setDebugMode(data.settings.debugMode);
|
|
51
|
+
console.log(`從 AI 設定讀取 Debug 模式: ${data.settings.debugMode}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error("載入 AI 設定失敗:", error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 載入使用者偏好
|
|
61
|
+
*/
|
|
62
|
+
export async function loadPreferences() {
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch("/api/preferences");
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
|
|
67
|
+
if (data.success) {
|
|
68
|
+
setPreferences(data.preferences);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("載入使用者偏好失敗:", error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 開啟 AI 設定彈窗
|
|
77
|
+
*/
|
|
78
|
+
export function openAISettingsModal() {
|
|
79
|
+
const aiSettings = getAISettings();
|
|
80
|
+
if (aiSettings) {
|
|
81
|
+
document.getElementById("apiUrl").value = aiSettings.apiUrl || "";
|
|
82
|
+
document.getElementById("model").value = aiSettings.model || "";
|
|
83
|
+
// API Key 欄位預設為空,不從資料庫讀取
|
|
84
|
+
document.getElementById("apiKey").value = "";
|
|
85
|
+
document.getElementById("apiKey").placeholder = "留空則保留原有 API Key";
|
|
86
|
+
document.getElementById("systemPrompt").value =
|
|
87
|
+
aiSettings.systemPrompt || "";
|
|
88
|
+
document.getElementById("mcpToolsPrompt").value =
|
|
89
|
+
aiSettings.mcpToolsPrompt || "";
|
|
90
|
+
document.getElementById("temperature").value =
|
|
91
|
+
aiSettings.temperature || 0.7;
|
|
92
|
+
document.getElementById("maxTokens").value = aiSettings.maxTokens || 1000;
|
|
93
|
+
document.getElementById("autoReplyTimerSeconds").value =
|
|
94
|
+
aiSettings.autoReplyTimerSeconds || 300;
|
|
95
|
+
document.getElementById("maxToolRounds").value =
|
|
96
|
+
aiSettings.maxToolRounds || 5;
|
|
97
|
+
document.getElementById("debugMode").checked =
|
|
98
|
+
aiSettings.debugMode || false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
document.getElementById("aiSettingsModal").classList.add("show");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 關閉 AI 設定彈窗
|
|
106
|
+
*/
|
|
107
|
+
export function closeAISettingsModal() {
|
|
108
|
+
document.getElementById("aiSettingsModal").classList.remove("show");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 儲存 AI 設定
|
|
113
|
+
*/
|
|
114
|
+
export async function saveAISettings() {
|
|
115
|
+
const apiUrl = document.getElementById("apiUrl").value.trim();
|
|
116
|
+
const model = document.getElementById("model").value.trim();
|
|
117
|
+
const apiKey = document.getElementById("apiKey").value.trim();
|
|
118
|
+
const systemPrompt = document.getElementById("systemPrompt").value.trim();
|
|
119
|
+
const mcpToolsPrompt = document.getElementById("mcpToolsPrompt").value.trim();
|
|
120
|
+
const temperature = parseFloat(document.getElementById("temperature").value);
|
|
121
|
+
const maxTokens = parseInt(document.getElementById("maxTokens").value);
|
|
122
|
+
const autoReplyTimerSeconds = parseInt(
|
|
123
|
+
document.getElementById("autoReplyTimerSeconds").value
|
|
124
|
+
);
|
|
125
|
+
const maxToolRoundsValue = parseInt(
|
|
126
|
+
document.getElementById("maxToolRounds").value
|
|
127
|
+
);
|
|
128
|
+
const debugModeValue = document.getElementById("debugMode").checked;
|
|
129
|
+
|
|
130
|
+
const settingsData = {
|
|
131
|
+
apiUrl: apiUrl || undefined,
|
|
132
|
+
model: model || undefined,
|
|
133
|
+
systemPrompt: systemPrompt || undefined,
|
|
134
|
+
mcpToolsPrompt: mcpToolsPrompt || undefined,
|
|
135
|
+
temperature,
|
|
136
|
+
maxTokens,
|
|
137
|
+
autoReplyTimerSeconds,
|
|
138
|
+
maxToolRounds: maxToolRoundsValue,
|
|
139
|
+
debugMode: debugModeValue,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// 只有當 API Key 不是遮罩格式且不為空時才更新
|
|
143
|
+
if (apiKey && !apiKey.startsWith("***")) {
|
|
144
|
+
settingsData.apiKey = apiKey;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch("/api/ai-settings", {
|
|
149
|
+
method: "PUT",
|
|
150
|
+
headers: { "Content-Type": "application/json" },
|
|
151
|
+
body: JSON.stringify(settingsData),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
let data;
|
|
155
|
+
try {
|
|
156
|
+
data = await response.json();
|
|
157
|
+
} catch (e) {
|
|
158
|
+
// 不是 JSON 回應,讀取純文字
|
|
159
|
+
const text = await response.text();
|
|
160
|
+
console.error("非 JSON 回應:", text);
|
|
161
|
+
showToast("error", "錯誤", `儲存失敗:${text}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (data && data.success) {
|
|
166
|
+
setAISettings(data.settings);
|
|
167
|
+
|
|
168
|
+
// 更新自動回覆計時器秒數
|
|
169
|
+
if (data.settings.autoReplyTimerSeconds !== undefined) {
|
|
170
|
+
setAutoReplyTimerSeconds(data.settings.autoReplyTimerSeconds);
|
|
171
|
+
console.log(
|
|
172
|
+
`自動回覆時間已更新為: ${data.settings.autoReplyTimerSeconds}s`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 更新 AI 交談次數上限
|
|
177
|
+
if (data.settings.maxToolRounds !== undefined) {
|
|
178
|
+
setMaxToolRounds(data.settings.maxToolRounds);
|
|
179
|
+
console.log(`AI 交談次數已更新為: ${data.settings.maxToolRounds}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 更新 Debug 模式
|
|
183
|
+
if (data.settings.debugMode !== undefined) {
|
|
184
|
+
setDebugMode(data.settings.debugMode);
|
|
185
|
+
console.log(`Debug 模式已更新為: ${data.settings.debugMode}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
closeAISettingsModal();
|
|
189
|
+
showToast("success", "成功", "AI 設定已儲存");
|
|
190
|
+
} else {
|
|
191
|
+
// 儘可能顯示詳細錯誤資訊
|
|
192
|
+
const detailParts = [];
|
|
193
|
+
if (data.error) detailParts.push(data.error);
|
|
194
|
+
if (data.details)
|
|
195
|
+
detailParts.push(
|
|
196
|
+
typeof data.details === "string"
|
|
197
|
+
? data.details
|
|
198
|
+
: JSON.stringify(data.details)
|
|
199
|
+
);
|
|
200
|
+
if (data.stack) detailParts.push(data.stack);
|
|
201
|
+
const message = detailParts.join(" \n ");
|
|
202
|
+
console.error("儲存 AI 設定失敗:", data);
|
|
203
|
+
showToast("error", "錯誤", message || "儲存 AI 設定失敗");
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error("儲存 AI 設定失敗:", error);
|
|
207
|
+
// 如果有 response 物件,可嘗試讀取更多內容
|
|
208
|
+
if (error && error.response) {
|
|
209
|
+
try {
|
|
210
|
+
const text = await error.response.text();
|
|
211
|
+
showToast("error", "錯誤", `儲存失敗:${text}`);
|
|
212
|
+
return;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// ignore
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
showToast(
|
|
219
|
+
"error",
|
|
220
|
+
"錯誤",
|
|
221
|
+
error instanceof Error ? error.message : "儲存失敗"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 測試 API Key
|
|
228
|
+
*/
|
|
229
|
+
export async function testAPIKey() {
|
|
230
|
+
const apiKeyInput = document.getElementById("apiKey").value.trim();
|
|
231
|
+
const model = document.getElementById("model").value.trim();
|
|
232
|
+
|
|
233
|
+
if (!model) {
|
|
234
|
+
showToast("error", "錯誤", "請輸入模型名稱");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
showLoadingOverlay("正在測試 API Key...");
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const requestBody = { model };
|
|
242
|
+
|
|
243
|
+
// 判斷是否使用新輸入的 API Key:
|
|
244
|
+
// 1. API Key 不為空
|
|
245
|
+
// 2. API Key 不是遮罩格式(不以 *** 開頭)
|
|
246
|
+
// 如果是遮罩格式或為空,後端會自動使用資料庫中解密的 API Key
|
|
247
|
+
if (apiKeyInput && !apiKeyInput.startsWith("***")) {
|
|
248
|
+
requestBody.apiKey = apiKeyInput;
|
|
249
|
+
console.log("使用新輸入的 API Key 進行測試");
|
|
250
|
+
} else {
|
|
251
|
+
console.log("使用資料庫中儲存的 API Key 進行測試");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const response = await fetch("/api/ai-settings/validate", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { "Content-Type": "application/json" },
|
|
257
|
+
body: JSON.stringify(requestBody),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const data = await response.json();
|
|
261
|
+
|
|
262
|
+
if (data.valid) {
|
|
263
|
+
showToast("success", "測試成功", "API Key 有效");
|
|
264
|
+
} else {
|
|
265
|
+
showToast("error", "測試失敗", data.error || "API Key 無效");
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error("測試 API Key 失敗:", error);
|
|
269
|
+
showToast("error", "錯誤", "測試失敗");
|
|
270
|
+
} finally {
|
|
271
|
+
hideLoadingOverlay();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 切換 API Key 可見性
|
|
277
|
+
*/
|
|
278
|
+
export function toggleAPIKeyVisibility() {
|
|
279
|
+
const apiKeyInput = document.getElementById("apiKey");
|
|
280
|
+
const toggleBtn = document.getElementById("toggleApiKey");
|
|
281
|
+
|
|
282
|
+
if (apiKeyInput.type === "password") {
|
|
283
|
+
apiKeyInput.type = "text";
|
|
284
|
+
toggleBtn.innerHTML = '<span class="icon">🙈</span>';
|
|
285
|
+
} else {
|
|
286
|
+
apiKeyInput.type = "password";
|
|
287
|
+
toggleBtn.innerHTML = '<span class="icon">👁️</span>';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default {
|
|
292
|
+
loadAISettings,
|
|
293
|
+
loadPreferences,
|
|
294
|
+
openAISettingsModal,
|
|
295
|
+
closeAISettingsModal,
|
|
296
|
+
saveAISettings,
|
|
297
|
+
testAPIKey,
|
|
298
|
+
toggleAPIKeyVisibility,
|
|
299
|
+
};
|