@hirohsu/user-web-feedback 2.8.1 → 2.8.8

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.
@@ -21,7 +21,8 @@
21
21
  const normalizedUrl = apiUrl.toLowerCase();
22
22
  if (normalizedUrl.includes("generativelanguage.googleapis.com")) return "google";
23
23
  if (normalizedUrl.includes("api.anthropic.com")) return "anthropic";
24
- if (normalizedUrl.includes("localhost") || normalizedUrl.includes("127.0.0.1")) return "local";
24
+ if (normalizedUrl.includes("nvidia.com")) return "nvidia";
25
+ if (normalizedUrl.includes("bigmodel.cn") || normalizedUrl.includes("z.ai")) return "zai";
25
26
  if (normalizedUrl.includes("api.openai.com")) return "openai";
26
27
  return "openai"; // 預設
27
28
  }
@@ -34,11 +35,11 @@
34
35
  const elements = {
35
36
  // AI Settings
36
37
  aiProvider: document.getElementById("aiProvider"),
38
+ apiUrl: document.getElementById("apiUrl"),
39
+ openaiCompatible: document.getElementById("openaiCompatible"),
37
40
  apiKey: document.getElementById("apiKey"),
38
41
  toggleApiKey: document.getElementById("toggleApiKey"),
39
42
  aiModel: document.getElementById("aiModel"),
40
- systemPrompt: document.getElementById("systemPrompt"),
41
- mcpToolsPrompt: document.getElementById("mcpToolsPrompt"),
42
43
  temperature: document.getElementById("temperature"),
43
44
  maxTokens: document.getElementById("maxTokens"),
44
45
  autoReplyTimerSeconds: document.getElementById("autoReplyTimerSeconds"),
@@ -72,6 +73,13 @@
72
73
  selfProbeCount: document.getElementById("selfProbeCount"),
73
74
  selfProbeLastTime: document.getElementById("selfProbeLastTime"),
74
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"),
75
83
  toastContainer: document.getElementById("toastContainer"),
76
84
  };
77
85
 
@@ -86,6 +94,7 @@
86
94
  loadCLISettings();
87
95
  loadPreferences();
88
96
  loadSelfProbeSettings();
97
+ loadPromptConfigs();
89
98
  }
90
99
 
91
100
  function setupEventListeners() {
@@ -93,6 +102,12 @@
93
102
  elements.toggleApiKey.addEventListener("click", toggleApiKeyVisibility);
94
103
  elements.testAiBtn.addEventListener("click", testAIConnection);
95
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
+ }
96
111
 
97
112
  // CLI Settings
98
113
  elements.aiModeApi.addEventListener("change", handleAIModeChange);
@@ -110,6 +125,54 @@
110
125
  if (elements.saveSelfProbeBtn) {
111
126
  elements.saveSelfProbeBtn.addEventListener("click", saveSelfProbeSettings);
112
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
+ }
113
176
  }
114
177
 
115
178
  function handleSelfProbeToggle() {
@@ -145,18 +208,26 @@
145
208
  // 從 apiUrl 反向推斷 provider
146
209
  const provider = getProviderFromApiUrl(data.settings.apiUrl);
147
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
+ }
148
219
  // API 返回的是 apiKeyMasked(遮罩後的 key),顯示給用戶看
149
220
  originalApiKeyMasked = data.settings.apiKeyMasked || "";
150
221
  elements.apiKey.value = originalApiKeyMasked;
151
222
  elements.apiKey.placeholder = originalApiKeyMasked ? "輸入新的 API Key 以更換" : "請輸入 API Key";
152
223
  elements.aiModel.value = data.settings.model || "";
153
- elements.systemPrompt.value = data.settings.systemPrompt || "";
154
- elements.mcpToolsPrompt.value = data.settings.mcpToolsPrompt || "";
155
224
  elements.temperature.value = data.settings.temperature ?? 0.7;
156
225
  elements.maxTokens.value = data.settings.maxTokens ?? 1000;
157
226
  elements.autoReplyTimerSeconds.value = data.settings.autoReplyTimerSeconds ?? 300;
158
227
  elements.maxToolRounds.value = data.settings.maxToolRounds ?? 5;
159
228
  elements.debugMode.checked = data.settings.debugMode || false;
229
+ // 更新 UI(不更新 URL,因為已經從資料庫載入)
230
+ handleAIProviderChange(false);
160
231
  }
161
232
  } catch (error) {
162
233
  console.error("Failed to load AI settings:", error);
@@ -291,6 +362,8 @@
291
362
  async function testAIConnection() {
292
363
  const apiKey = elements.apiKey.value;
293
364
  const model = elements.aiModel.value;
365
+ const provider = elements.aiProvider.value;
366
+ const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
294
367
 
295
368
  // 如果 API key 是遮罩值,表示用戶沒有修改,將使用資料庫中的 key
296
369
  const apiKeyChanged = apiKey !== originalApiKeyMasked;
@@ -309,8 +382,12 @@
309
382
  elements.testAiBtn.textContent = "測試中...";
310
383
 
311
384
  try {
312
- // 如果用戶修改了 API key 就傳送新的 key,否則不傳送(後端會使用資料庫中的)
313
- const payload = { model };
385
+ // 傳送當前表單的設定值進行測試
386
+ const payload = {
387
+ model,
388
+ apiUrl,
389
+ openaiCompatible: elements.openaiCompatible?.checked || false
390
+ };
314
391
  if (apiKeyChanged) {
315
392
  payload.apiKey = apiKey;
316
393
  }
@@ -340,20 +417,22 @@
340
417
  async function saveAISettings() {
341
418
  const provider = elements.aiProvider.value;
342
419
  const currentApiKey = elements.apiKey.value;
343
-
420
+
344
421
  // 只有當用戶真的修改了 API key 才傳送(不是遮罩值)
345
422
  const apiKeyChanged = currentApiKey !== originalApiKeyMasked;
346
-
423
+
424
+ // 使用表單中的 API URL,若為空則使用預設值
425
+ const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
426
+
347
427
  const settings = {
348
- apiUrl: getApiUrlFromProvider(provider),
428
+ apiUrl: apiUrl,
349
429
  model: elements.aiModel.value,
350
- systemPrompt: elements.systemPrompt.value,
351
- mcpToolsPrompt: elements.mcpToolsPrompt.value,
352
430
  temperature: parseFloat(elements.temperature.value) || 0.7,
353
431
  maxTokens: parseInt(elements.maxTokens.value) || 1000,
354
432
  autoReplyTimerSeconds: parseInt(elements.autoReplyTimerSeconds.value) || 300,
355
433
  maxToolRounds: parseInt(elements.maxToolRounds.value) || 5,
356
434
  debugMode: elements.debugMode.checked,
435
+ openaiCompatible: elements.openaiCompatible?.checked || false,
357
436
  };
358
437
 
359
438
  // 只有修改了 API key 才加入
@@ -515,10 +594,152 @@
515
594
  }
516
595
  }
517
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
+
518
726
  function showToast(message, type = "info") {
519
727
  const toast = document.createElement("div");
520
728
  toast.className = `toast toast-${type}`;
521
- toast.textContent = message;
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);
522
743
 
523
744
  elements.toastContainer.appendChild(toast);
524
745
 
@@ -526,12 +747,15 @@
526
747
  toast.classList.add("show");
527
748
  }, 10);
528
749
 
529
- setTimeout(() => {
530
- toast.classList.remove("show");
750
+ // 錯誤訊息不自動關閉,其他類型 3 秒後關閉
751
+ if (type !== "error") {
531
752
  setTimeout(() => {
532
- toast.remove();
533
- }, 300);
534
- }, 3000);
753
+ toast.classList.remove("show");
754
+ setTimeout(() => {
755
+ toast.remove();
756
+ }, 300);
757
+ }, 3000);
758
+ }
535
759
  }
536
760
 
537
761
  if (document.readyState === "loading") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirohsu/user-web-feedback",
3
- "version": "2.8.1",
3
+ "version": "2.8.8",
4
4
  "description": "基於Node.js的MCP回饋收集器 - 支持AI工作彙報和用戶回饋收集",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {
@@ -34,7 +34,8 @@
34
34
  "build": "tsup",
35
35
  "dev": "tsx watch --clear-screen=false src/cli.ts",
36
36
  "start": "node dist/cli.js",
37
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
37
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --maxWorkers=50%",
38
+ "test:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js --maxWorkers=1 --forceExit",
38
39
  "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js src/__tests__/integration.test.ts --testPathIgnorePatterns=[] --forceExit",
39
40
  "test:all": "npm run test && npm run test:integration",
40
41
  "test:watch": "jest --watch",
@@ -45,7 +46,9 @@
45
46
  "prepublishOnly": "npm run clean && npm run build && node scripts/remove-sourcemaps.cjs"
46
47
  },
47
48
  "dependencies": {
48
- "better-sqlite3": "^12.4.1"
49
+ "@hirohsu/user-web-feedback": "^2.8.2",
50
+ "better-sqlite3": "^12.4.1",
51
+ "openai": "^6.16.0"
49
52
  },
50
53
  "optionalDependencies": {
51
54
  "@google/generative-ai": "^0.24.1",