@hirohsu/user-web-feedback 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +953 -0
- package/dist/cli.cjs +95778 -0
- package/dist/index.cjs +92818 -0
- package/dist/static/app.js +385 -0
- package/dist/static/components/navbar.css +406 -0
- package/dist/static/components/navbar.html +49 -0
- package/dist/static/components/navbar.js +211 -0
- package/dist/static/dashboard.css +495 -0
- package/dist/static/dashboard.html +95 -0
- package/dist/static/dashboard.js +540 -0
- package/dist/static/favicon.svg +27 -0
- package/dist/static/index.html +541 -0
- package/dist/static/logs.html +376 -0
- package/dist/static/logs.js +442 -0
- package/dist/static/mcp-settings.html +797 -0
- package/dist/static/mcp-settings.js +884 -0
- package/dist/static/modules/app-core.js +124 -0
- package/dist/static/modules/conversation-panel.js +247 -0
- package/dist/static/modules/feedback-handler.js +1420 -0
- package/dist/static/modules/image-handler.js +155 -0
- package/dist/static/modules/log-viewer.js +296 -0
- package/dist/static/modules/mcp-manager.js +474 -0
- package/dist/static/modules/prompt-manager.js +364 -0
- package/dist/static/modules/settings-manager.js +299 -0
- package/dist/static/modules/socket-manager.js +170 -0
- package/dist/static/modules/state-manager.js +352 -0
- package/dist/static/modules/timer-controller.js +243 -0
- package/dist/static/modules/ui-helpers.js +246 -0
- package/dist/static/settings.html +355 -0
- package/dist/static/settings.js +425 -0
- package/dist/static/socket.io.min.js +7 -0
- package/dist/static/style.css +2157 -0
- package/dist/static/terminals.html +357 -0
- package/dist/static/terminals.js +321 -0
- package/package.json +91 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系統日誌頁面
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
(function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const API_BASE = "";
|
|
9
|
+
const PAGE_SIZE = 50;
|
|
10
|
+
|
|
11
|
+
let currentPage = 1;
|
|
12
|
+
let currentFilters = {
|
|
13
|
+
level: "",
|
|
14
|
+
source: "",
|
|
15
|
+
};
|
|
16
|
+
let totalLogs = 0;
|
|
17
|
+
|
|
18
|
+
const elements = {
|
|
19
|
+
logsTableBody: document.getElementById("logsTableBody"),
|
|
20
|
+
levelFilter: document.getElementById("levelFilter"),
|
|
21
|
+
sourceFilter: document.getElementById("sourceFilter"),
|
|
22
|
+
refreshBtn: document.getElementById("refreshBtn"),
|
|
23
|
+
clearBtn: document.getElementById("clearBtn"),
|
|
24
|
+
prevPageBtn: document.getElementById("prevPageBtn"),
|
|
25
|
+
nextPageBtn: document.getElementById("nextPageBtn"),
|
|
26
|
+
pageInfo: document.getElementById("pageInfo"),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function init() {
|
|
30
|
+
setupEventListeners();
|
|
31
|
+
loadSources();
|
|
32
|
+
loadLogs();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setupEventListeners() {
|
|
36
|
+
elements.levelFilter.addEventListener("change", () => {
|
|
37
|
+
currentFilters.level = elements.levelFilter.value;
|
|
38
|
+
currentPage = 1;
|
|
39
|
+
loadLogs();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
elements.sourceFilter.addEventListener("change", () => {
|
|
43
|
+
currentFilters.source = elements.sourceFilter.value;
|
|
44
|
+
currentPage = 1;
|
|
45
|
+
loadLogs();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
elements.refreshBtn.addEventListener("click", () => {
|
|
49
|
+
loadLogs();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
elements.clearBtn.addEventListener("click", () => {
|
|
53
|
+
if (confirm("確定要清除所有日誌嗎?此操作無法復原。")) {
|
|
54
|
+
clearLogs();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
elements.prevPageBtn.addEventListener("click", () => {
|
|
59
|
+
if (currentPage > 1) {
|
|
60
|
+
currentPage--;
|
|
61
|
+
loadLogs();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
elements.nextPageBtn.addEventListener("click", () => {
|
|
66
|
+
const totalPages = Math.ceil(totalLogs / PAGE_SIZE);
|
|
67
|
+
if (currentPage < totalPages) {
|
|
68
|
+
currentPage++;
|
|
69
|
+
loadLogs();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadSources() {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${API_BASE}/api/logs/sources`);
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
|
|
79
|
+
if (data.sources && data.sources.length > 0) {
|
|
80
|
+
elements.sourceFilter.innerHTML =
|
|
81
|
+
'<option value="">全部</option>' +
|
|
82
|
+
data.sources
|
|
83
|
+
.map(
|
|
84
|
+
(s) =>
|
|
85
|
+
`<option value="${escapeHtml(s)}">${escapeHtml(s)}</option>`
|
|
86
|
+
)
|
|
87
|
+
.join("");
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("Failed to load sources:", error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function loadLogs() {
|
|
95
|
+
try {
|
|
96
|
+
const params = new URLSearchParams({
|
|
97
|
+
page: currentPage,
|
|
98
|
+
limit: PAGE_SIZE,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (currentFilters.level) {
|
|
102
|
+
params.append("level", currentFilters.level);
|
|
103
|
+
}
|
|
104
|
+
if (currentFilters.source) {
|
|
105
|
+
params.append("source", currentFilters.source);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const response = await fetch(`${API_BASE}/api/logs?${params}`);
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
|
|
111
|
+
totalLogs = data.pagination?.total || 0;
|
|
112
|
+
renderLogs(data.logs || []);
|
|
113
|
+
updatePagination();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Failed to load logs:", error);
|
|
116
|
+
showError("載入日誌失敗");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderLogs(logs) {
|
|
121
|
+
if (logs.length === 0) {
|
|
122
|
+
elements.logsTableBody.innerHTML = `
|
|
123
|
+
<tr>
|
|
124
|
+
<td colspan="4">
|
|
125
|
+
<div class="empty-logs">
|
|
126
|
+
<div class="icon">📭</div>
|
|
127
|
+
<p>沒有日誌記錄</p>
|
|
128
|
+
</div>
|
|
129
|
+
</td>
|
|
130
|
+
</tr>
|
|
131
|
+
`;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
elements.logsTableBody.innerHTML = logs
|
|
136
|
+
.map(
|
|
137
|
+
(log) => `
|
|
138
|
+
<tr>
|
|
139
|
+
<td class="log-timestamp">${formatTimestamp(log.timestamp)}</td>
|
|
140
|
+
<td><span class="log-level ${log.level}">${
|
|
141
|
+
log.level
|
|
142
|
+
}</span></td>
|
|
143
|
+
<td class="log-source">${escapeHtml(log.source || "-")}</td>
|
|
144
|
+
<td class="log-message" title="${escapeHtml(
|
|
145
|
+
log.message
|
|
146
|
+
)}">${escapeHtml(log.message)}</td>
|
|
147
|
+
</tr>
|
|
148
|
+
`
|
|
149
|
+
)
|
|
150
|
+
.join("");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function updatePagination() {
|
|
154
|
+
const totalPages = Math.ceil(totalLogs / PAGE_SIZE);
|
|
155
|
+
|
|
156
|
+
elements.prevPageBtn.disabled = currentPage <= 1;
|
|
157
|
+
elements.nextPageBtn.disabled =
|
|
158
|
+
currentPage >= totalPages || totalPages === 0;
|
|
159
|
+
elements.pageInfo.textContent = `第 ${currentPage} 頁 / 共 ${totalPages} 頁 (總計 ${totalLogs} 條)`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function clearLogs() {
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch(`${API_BASE}/api/logs`, {
|
|
165
|
+
method: "DELETE",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (response.ok) {
|
|
169
|
+
currentPage = 1;
|
|
170
|
+
loadLogs();
|
|
171
|
+
} else {
|
|
172
|
+
showError("清除日誌失敗");
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error("Failed to clear logs:", error);
|
|
176
|
+
showError("清除日誌失敗");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function formatTimestamp(timestamp) {
|
|
181
|
+
const date = new Date(timestamp);
|
|
182
|
+
return date.toLocaleString("zh-TW", {
|
|
183
|
+
year: "numeric",
|
|
184
|
+
month: "2-digit",
|
|
185
|
+
day: "2-digit",
|
|
186
|
+
hour: "2-digit",
|
|
187
|
+
minute: "2-digit",
|
|
188
|
+
second: "2-digit",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function escapeHtml(text) {
|
|
193
|
+
if (!text) return "";
|
|
194
|
+
const div = document.createElement("div");
|
|
195
|
+
div.textContent = text;
|
|
196
|
+
return div.innerHTML;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function showError(message) {
|
|
200
|
+
elements.logsTableBody.innerHTML = `
|
|
201
|
+
<tr>
|
|
202
|
+
<td colspan="4">
|
|
203
|
+
<div class="empty-logs">
|
|
204
|
+
<div class="icon">❌</div>
|
|
205
|
+
<p>${escapeHtml(message)}</p>
|
|
206
|
+
</div>
|
|
207
|
+
</td>
|
|
208
|
+
</tr>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ==================== API 日誌功能 ====================
|
|
213
|
+
|
|
214
|
+
let apiCurrentPage = 1;
|
|
215
|
+
let apiTotalLogs = 0;
|
|
216
|
+
let apiCurrentEndpointFilter = "";
|
|
217
|
+
let apiCurrentTypeFilter = "all"; // 'all', 'success', 'errors'
|
|
218
|
+
|
|
219
|
+
const apiElements = {
|
|
220
|
+
tableBody: document.getElementById("apiErrorsTableBody"),
|
|
221
|
+
endpointFilter: document.getElementById("apiEndpointFilter"),
|
|
222
|
+
typeFilter: document.getElementById("apiTypeFilter"),
|
|
223
|
+
refreshBtn: document.getElementById("apiRefreshBtn"),
|
|
224
|
+
cleanupBtn: document.getElementById("apiCleanupBtn"),
|
|
225
|
+
clearAllBtn: document.getElementById("apiClearAllBtn"),
|
|
226
|
+
prevPageBtn: document.getElementById("apiPrevPageBtn"),
|
|
227
|
+
nextPageBtn: document.getElementById("apiNextPageBtn"),
|
|
228
|
+
pageInfo: document.getElementById("apiPageInfo"),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
function setupTabSwitching() {
|
|
232
|
+
const tabBtns = document.querySelectorAll(".tab-btn");
|
|
233
|
+
tabBtns.forEach(btn => {
|
|
234
|
+
btn.addEventListener("click", () => {
|
|
235
|
+
tabBtns.forEach(b => {
|
|
236
|
+
b.classList.remove("active");
|
|
237
|
+
b.style.borderBottomColor = "transparent";
|
|
238
|
+
b.style.color = "var(--text-secondary)";
|
|
239
|
+
});
|
|
240
|
+
btn.classList.add("active");
|
|
241
|
+
btn.style.borderBottomColor = "var(--accent-color)";
|
|
242
|
+
btn.style.color = "var(--accent-color)";
|
|
243
|
+
|
|
244
|
+
const tab = btn.dataset.tab;
|
|
245
|
+
document.getElementById("systemLogsPanel").style.display = tab === "system" ? "block" : "none";
|
|
246
|
+
document.getElementById("apiErrorsPanel").style.display = tab === "api-errors" ? "block" : "none";
|
|
247
|
+
|
|
248
|
+
if (tab === "api-errors") {
|
|
249
|
+
loadApiLogs();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function setupApiEventListeners() {
|
|
256
|
+
if (apiElements.endpointFilter) {
|
|
257
|
+
apiElements.endpointFilter.addEventListener("change", () => {
|
|
258
|
+
apiCurrentEndpointFilter = apiElements.endpointFilter.value;
|
|
259
|
+
apiCurrentPage = 1;
|
|
260
|
+
loadApiLogs();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (apiElements.typeFilter) {
|
|
265
|
+
apiElements.typeFilter.addEventListener("change", () => {
|
|
266
|
+
apiCurrentTypeFilter = apiElements.typeFilter.value;
|
|
267
|
+
apiCurrentPage = 1;
|
|
268
|
+
loadApiLogs();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (apiElements.refreshBtn) {
|
|
273
|
+
apiElements.refreshBtn.addEventListener("click", () => {
|
|
274
|
+
loadApiLogs();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (apiElements.cleanupBtn) {
|
|
279
|
+
apiElements.cleanupBtn.addEventListener("click", async () => {
|
|
280
|
+
if (confirm("確定要清除超過7天的舊日誌嗎?")) {
|
|
281
|
+
try {
|
|
282
|
+
const response = await fetch(`${API_BASE}/api/api-logs/cleanup?days=7`, {
|
|
283
|
+
method: "DELETE",
|
|
284
|
+
});
|
|
285
|
+
const data = await response.json();
|
|
286
|
+
if (data.success) {
|
|
287
|
+
alert(`已清除 ${data.deleted} 筆舊日誌`);
|
|
288
|
+
loadApiLogs();
|
|
289
|
+
} else {
|
|
290
|
+
alert("清除失敗: " + (data.error || "未知錯誤"));
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
alert("清除失敗: " + error.message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (apiElements.clearAllBtn) {
|
|
300
|
+
apiElements.clearAllBtn.addEventListener("click", async () => {
|
|
301
|
+
if (confirm("確定要清除所有 API 日誌嗎?此操作無法復原。")) {
|
|
302
|
+
try {
|
|
303
|
+
const response = await fetch(`${API_BASE}/api/api-logs/clear`, {
|
|
304
|
+
method: "DELETE",
|
|
305
|
+
});
|
|
306
|
+
const data = await response.json();
|
|
307
|
+
if (data.success) {
|
|
308
|
+
alert(`已清除 ${data.deleted} 筆日誌`);
|
|
309
|
+
loadApiLogs();
|
|
310
|
+
} else {
|
|
311
|
+
alert("清除失敗: " + (data.error || "未知錯誤"));
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
alert("清除失敗: " + error.message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (apiElements.prevPageBtn) {
|
|
321
|
+
apiElements.prevPageBtn.addEventListener("click", () => {
|
|
322
|
+
if (apiCurrentPage > 1) {
|
|
323
|
+
apiCurrentPage--;
|
|
324
|
+
loadApiLogs();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (apiElements.nextPageBtn) {
|
|
330
|
+
apiElements.nextPageBtn.addEventListener("click", () => {
|
|
331
|
+
const totalPages = Math.ceil(apiTotalLogs / PAGE_SIZE);
|
|
332
|
+
if (apiCurrentPage < totalPages) {
|
|
333
|
+
apiCurrentPage++;
|
|
334
|
+
loadApiLogs();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function loadApiLogs() {
|
|
341
|
+
if (!apiElements.tableBody) return;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const params = new URLSearchParams({
|
|
345
|
+
limit: PAGE_SIZE,
|
|
346
|
+
offset: (apiCurrentPage - 1) * PAGE_SIZE,
|
|
347
|
+
filter: apiCurrentTypeFilter,
|
|
348
|
+
});
|
|
349
|
+
if (apiCurrentEndpointFilter) {
|
|
350
|
+
params.append("endpoint", apiCurrentEndpointFilter);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const response = await fetch(`${API_BASE}/api/api-logs?${params}`);
|
|
354
|
+
const data = await response.json();
|
|
355
|
+
|
|
356
|
+
if (!data.success) {
|
|
357
|
+
showApiError(data.error || "載入失敗");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
apiTotalLogs = data.total;
|
|
362
|
+
renderApiLogs(data.logs);
|
|
363
|
+
updateApiPagination();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
showApiError("載入日誌失敗: " + error.message);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function renderApiLogs(logs) {
|
|
370
|
+
if (!logs || logs.length === 0) {
|
|
371
|
+
apiElements.tableBody.innerHTML = `
|
|
372
|
+
<tr>
|
|
373
|
+
<td colspan="5">
|
|
374
|
+
<div class="empty-logs">
|
|
375
|
+
<div class="icon">📭</div>
|
|
376
|
+
<p>沒有日誌記錄</p>
|
|
377
|
+
</div>
|
|
378
|
+
</td>
|
|
379
|
+
</tr>
|
|
380
|
+
`;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
apiElements.tableBody.innerHTML = logs
|
|
385
|
+
.map(
|
|
386
|
+
(log) => `
|
|
387
|
+
<tr>
|
|
388
|
+
<td class="log-time">${formatTimestamp(log.createdAt)}</td>
|
|
389
|
+
<td><code>${escapeHtml(log.endpoint)}</code></td>
|
|
390
|
+
<td><span class="log-level ${log.method.toLowerCase()}">${escapeHtml(log.method)}</span></td>
|
|
391
|
+
<td><span class="log-level ${log.success ? 'info' : 'error'}">${log.success ? '✓ 成功' : '✗ 失敗'}</span></td>
|
|
392
|
+
<td class="log-message" title="${escapeHtml(log.errorDetails || log.message || '')}">${escapeHtml(log.message || '-')}</td>
|
|
393
|
+
</tr>
|
|
394
|
+
`
|
|
395
|
+
)
|
|
396
|
+
.join("");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function updateApiPagination() {
|
|
400
|
+
const totalPages = Math.ceil(apiTotalLogs / PAGE_SIZE) || 1;
|
|
401
|
+
if (apiElements.pageInfo) {
|
|
402
|
+
apiElements.pageInfo.textContent = `第 ${apiCurrentPage} / ${totalPages} 頁 (共 ${apiTotalLogs} 筆)`;
|
|
403
|
+
}
|
|
404
|
+
if (apiElements.prevPageBtn) {
|
|
405
|
+
apiElements.prevPageBtn.disabled = apiCurrentPage <= 1;
|
|
406
|
+
}
|
|
407
|
+
if (apiElements.nextPageBtn) {
|
|
408
|
+
apiElements.nextPageBtn.disabled = apiCurrentPage >= totalPages;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function showApiError(message) {
|
|
413
|
+
if (!apiElements.tableBody) return;
|
|
414
|
+
apiElements.tableBody.innerHTML = `
|
|
415
|
+
<tr>
|
|
416
|
+
<td colspan="5">
|
|
417
|
+
<div class="empty-logs">
|
|
418
|
+
<div class="icon">❌</div>
|
|
419
|
+
<p>${escapeHtml(message)}</p>
|
|
420
|
+
</div>
|
|
421
|
+
</td>
|
|
422
|
+
</tr>
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 初始化時也設定標籤切換和 API 日誌事件
|
|
427
|
+
function initApiLogs() {
|
|
428
|
+
setupTabSwitching();
|
|
429
|
+
setupApiEventListeners();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (document.readyState === "loading") {
|
|
433
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
434
|
+
init();
|
|
435
|
+
initApiLogs();
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
init();
|
|
439
|
+
initApiLogs();
|
|
440
|
+
}
|
|
441
|
+
})();
|
|
442
|
+
|