@hirohsu/user-web-feedback 2.8.2 → 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.
package/dist/cli.cjs CHANGED
@@ -5283,6 +5283,9 @@ var init_logger = __esm({
5283
5283
  this.flushInterval = setInterval(() => {
5284
5284
  this.flushToDatabase();
5285
5285
  }, this.FLUSH_INTERVAL_MS);
5286
+ if (this.flushInterval.unref) {
5287
+ this.flushInterval.unref();
5288
+ }
5286
5289
  process.on("beforeExit", () => {
5287
5290
  this.flushToDatabase();
5288
5291
  });
@@ -79044,8 +79047,25 @@ function cleanExpiredCache() {
79044
79047
  function clearAICache() {
79045
79048
  cache.clear();
79046
79049
  }
79047
- async function validateAPIKey(apiKey, model) {
79050
+ async function validateAPIKey(apiKey, model, apiUrl, openaiCompatible) {
79048
79051
  try {
79052
+ const provider = getProviderFromUrl(apiUrl);
79053
+ if (openaiCompatible || provider === "openai" || provider === "nvidia" || provider === "zai") {
79054
+ const OpenAI = (await import("openai")).default;
79055
+ const client = new OpenAI({
79056
+ apiKey,
79057
+ baseURL: apiUrl || "https://api.openai.com/v1"
79058
+ });
79059
+ const response2 = await client.chat.completions.create({
79060
+ model,
79061
+ messages: [{ role: "user", content: "Hello" }],
79062
+ max_tokens: 10
79063
+ });
79064
+ if (response2.choices && response2.choices.length > 0) {
79065
+ return { valid: true };
79066
+ }
79067
+ return { valid: false, error: "\u7121\u6CD5\u751F\u6210\u56DE\u61C9" };
79068
+ }
79049
79069
  const genAI = new GoogleGenerativeAI(apiKey);
79050
79070
  const generativeModel = genAI.getGenerativeModel({ model });
79051
79071
  const result = await generativeModel.generateContent("Hello");
@@ -79062,6 +79082,16 @@ async function validateAPIKey(apiKey, model) {
79062
79082
  };
79063
79083
  }
79064
79084
  }
79085
+ function getProviderFromUrl(apiUrl) {
79086
+ if (!apiUrl) return "google";
79087
+ const normalizedUrl = apiUrl.toLowerCase();
79088
+ if (normalizedUrl.includes("api.openai.com")) return "openai";
79089
+ if (normalizedUrl.includes("api.anthropic.com")) return "anthropic";
79090
+ if (normalizedUrl.includes("generativelanguage.googleapis.com")) return "google";
79091
+ if (normalizedUrl.includes("nvidia.com")) return "nvidia";
79092
+ if (normalizedUrl.includes("bigmodel.cn") || normalizedUrl.includes("z.ai")) return "zai";
79093
+ return "openai";
79094
+ }
79065
79095
  function estimateTokenCount(text) {
79066
79096
  const englishChars = (text.match(/[a-zA-Z0-9]/g) || []).length;
79067
79097
  const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
@@ -93636,7 +93666,7 @@ var WebServer = class {
93636
93666
  this.app.post("/api/ai-settings/validate", async (req, res) => {
93637
93667
  const startTime = Date.now();
93638
93668
  try {
93639
- let { apiKey } = req.body;
93669
+ let { apiKey, apiUrl, openaiCompatible } = req.body;
93640
93670
  const { model } = req.body;
93641
93671
  if (!model) {
93642
93672
  const errorMsg = "\u6A21\u578B\u70BA\u5FC5\u586B\u6B04\u4F4D";
@@ -93656,6 +93686,7 @@ var WebServer = class {
93656
93686
  return;
93657
93687
  }
93658
93688
  let usingDatabaseKey = false;
93689
+ let usingDatabaseUrl = false;
93659
93690
  if (!apiKey) {
93660
93691
  const settings = getAISettings();
93661
93692
  if (!settings || !settings.apiKey || settings.apiKey === "YOUR_API_KEY_HERE") {
@@ -93678,13 +93709,18 @@ var WebServer = class {
93678
93709
  }
93679
93710
  apiKey = settings.apiKey;
93680
93711
  usingDatabaseKey = true;
93712
+ if (!apiUrl) {
93713
+ apiUrl = settings.apiUrl;
93714
+ usingDatabaseUrl = true;
93715
+ }
93681
93716
  logger.info("\u4F7F\u7528\u8CC7\u6599\u5EAB\u4E2D\u89E3\u5BC6\u7684 API Key \u9032\u884C\u9A57\u8B49");
93682
93717
  logger.debug(`\u89E3\u5BC6\u5F8C\u7684 API Key \u9577\u5EA6: ${apiKey.length}, \u524D\u7DB4: ${apiKey.substring(0, 3)}...`);
93683
93718
  } else {
93684
93719
  logger.info("\u4F7F\u7528\u65B0\u8F38\u5165\u7684 API Key \u9032\u884C\u9A57\u8B49");
93685
93720
  logger.debug(`\u65B0\u8F38\u5165\u7684 API Key \u9577\u5EA6: ${apiKey.length}, \u524D\u7DB4: ${apiKey.substring(0, 3)}...`);
93686
93721
  }
93687
- const result = await validateAPIKey(apiKey, model);
93722
+ logger.info(`\u9A57\u8B49\u4F7F\u7528 API URL: ${apiUrl} (\u8CC7\u6599\u5EAB: ${usingDatabaseUrl}), OpenAI \u76F8\u5BB9: ${openaiCompatible}`);
93723
+ const result = await validateAPIKey(apiKey, model, apiUrl, openaiCompatible);
93688
93724
  if (result.valid) {
93689
93725
  logger.info(`API Key \u9A57\u8B49\u6210\u529F (${usingDatabaseKey ? "\u8CC7\u6599\u5EAB" : "\u65B0\u8F38\u5165"})`);
93690
93726
  logAPIRequest({
@@ -95572,9 +95608,35 @@ var MCPServer = class {
95572
95608
  this.mcpServer.registerTool(
95573
95609
  "collect_feedback",
95574
95610
  {
95575
- description: "Collect feedback from users about AI work summary. This tool opens a web interface for users to provide feedback on the AI's work.",
95611
+ description: `Collect feedback from users about AI work. This tool opens a web interface for users to provide feedback.
95612
+
95613
+ CRITICAL: The 'work_summary' field is the PRIMARY and ONLY content displayed to users in the feedback UI. You MUST include ALL relevant information in this field as a comprehensive Markdown-formatted report. The UI renders Markdown, so use headings, tables, code blocks, and lists for better readability.`,
95576
95614
  inputSchema: {
95577
- work_summary: external_exports.string().describe("AI\u5DE5\u4F5C\u532F\u5831\u5167\u5BB9\uFF0C\u63CF\u8FF0AI\u5B8C\u6210\u7684\u5DE5\u4F5C\u548C\u7D50\u679C"),
95615
+ work_summary: external_exports.string().describe(`\u3010CRITICAL - THIS IS THE ONLY CONTENT SHOWN TO USERS\u3011
95616
+
95617
+ Include a COMPLETE Markdown report with ALL of the following sections:
95618
+
95619
+ ## Required Sections:
95620
+ 1. **\u{1F4CB} Task Summary** - Brief description of what was requested and accomplished
95621
+ 2. **\u{1F4C1} Implementation Details** - Files created/modified with:
95622
+ - Full file paths
95623
+ - Key code snippets in fenced code blocks
95624
+ - Explanation of changes
95625
+ 3. **\u2705 Status Table** - Markdown table showing completion status:
95626
+ | Item | Status | Notes |
95627
+ |------|--------|-------|
95628
+ | Feature A | \u2705 Done | ... |
95629
+ 4. **\u{1F9EA} Test Results** - Build/test command outputs and outcomes
95630
+ 5. **\u27A1\uFE0F Next Steps** - Actionable options in A/B/C format for user decision:
95631
+ - Option A: [action] - [description]
95632
+ - Option B: [action] - [description]
95633
+ 6. **\u{1F3D7}\uFE0F Architecture** (if applicable) - ASCII diagrams or Mermaid code blocks
95634
+
95635
+ ## Format Requirements:
95636
+ - Use Markdown: ## headings, \`code\`, **bold**, tables
95637
+ - Minimum 500 characters for non-trivial tasks
95638
+ - Be specific with file paths and code examples
95639
+ - Include ALL information user needs to make decisions`),
95578
95640
  project_name: external_exports.string().optional().describe("\u5C08\u6848\u540D\u7A31\uFF08\u7528\u65BC Dashboard \u5206\u7D44\u986F\u793A\uFF09"),
95579
95641
  project_path: external_exports.string().optional().describe("\u5C08\u6848\u8DEF\u5F91\uFF08\u7528\u65BC\u552F\u4E00\u8B58\u5225\u5C08\u6848\uFF09")
95580
95642
  }
package/dist/index.cjs CHANGED
@@ -22641,6 +22641,9 @@ var init_logger = __esm({
22641
22641
  this.flushInterval = setInterval(() => {
22642
22642
  this.flushToDatabase();
22643
22643
  }, this.FLUSH_INTERVAL_MS);
22644
+ if (this.flushInterval.unref) {
22645
+ this.flushInterval.unref();
22646
+ }
22644
22647
  process.on("beforeExit", () => {
22645
22648
  this.flushToDatabase();
22646
22649
  });
@@ -75931,8 +75934,25 @@ function cleanExpiredCache() {
75931
75934
  function clearAICache() {
75932
75935
  cache.clear();
75933
75936
  }
75934
- async function validateAPIKey(apiKey, model) {
75937
+ async function validateAPIKey(apiKey, model, apiUrl, openaiCompatible) {
75935
75938
  try {
75939
+ const provider = getProviderFromUrl(apiUrl);
75940
+ if (openaiCompatible || provider === "openai" || provider === "nvidia" || provider === "zai") {
75941
+ const OpenAI = (await import("openai")).default;
75942
+ const client = new OpenAI({
75943
+ apiKey,
75944
+ baseURL: apiUrl || "https://api.openai.com/v1"
75945
+ });
75946
+ const response2 = await client.chat.completions.create({
75947
+ model,
75948
+ messages: [{ role: "user", content: "Hello" }],
75949
+ max_tokens: 10
75950
+ });
75951
+ if (response2.choices && response2.choices.length > 0) {
75952
+ return { valid: true };
75953
+ }
75954
+ return { valid: false, error: "\u7121\u6CD5\u751F\u6210\u56DE\u61C9" };
75955
+ }
75936
75956
  const genAI = new GoogleGenerativeAI(apiKey);
75937
75957
  const generativeModel = genAI.getGenerativeModel({ model });
75938
75958
  const result = await generativeModel.generateContent("Hello");
@@ -75949,6 +75969,16 @@ async function validateAPIKey(apiKey, model) {
75949
75969
  };
75950
75970
  }
75951
75971
  }
75972
+ function getProviderFromUrl(apiUrl) {
75973
+ if (!apiUrl) return "google";
75974
+ const normalizedUrl = apiUrl.toLowerCase();
75975
+ if (normalizedUrl.includes("api.openai.com")) return "openai";
75976
+ if (normalizedUrl.includes("api.anthropic.com")) return "anthropic";
75977
+ if (normalizedUrl.includes("generativelanguage.googleapis.com")) return "google";
75978
+ if (normalizedUrl.includes("nvidia.com")) return "nvidia";
75979
+ if (normalizedUrl.includes("bigmodel.cn") || normalizedUrl.includes("z.ai")) return "zai";
75980
+ return "openai";
75981
+ }
75952
75982
  function estimateTokenCount(text) {
75953
75983
  const englishChars = (text.match(/[a-zA-Z0-9]/g) || []).length;
75954
75984
  const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
@@ -90651,7 +90681,7 @@ var WebServer = class {
90651
90681
  this.app.post("/api/ai-settings/validate", async (req, res) => {
90652
90682
  const startTime = Date.now();
90653
90683
  try {
90654
- let { apiKey } = req.body;
90684
+ let { apiKey, apiUrl, openaiCompatible } = req.body;
90655
90685
  const { model } = req.body;
90656
90686
  if (!model) {
90657
90687
  const errorMsg = "\u6A21\u578B\u70BA\u5FC5\u586B\u6B04\u4F4D";
@@ -90671,6 +90701,7 @@ var WebServer = class {
90671
90701
  return;
90672
90702
  }
90673
90703
  let usingDatabaseKey = false;
90704
+ let usingDatabaseUrl = false;
90674
90705
  if (!apiKey) {
90675
90706
  const settings = getAISettings();
90676
90707
  if (!settings || !settings.apiKey || settings.apiKey === "YOUR_API_KEY_HERE") {
@@ -90693,13 +90724,18 @@ var WebServer = class {
90693
90724
  }
90694
90725
  apiKey = settings.apiKey;
90695
90726
  usingDatabaseKey = true;
90727
+ if (!apiUrl) {
90728
+ apiUrl = settings.apiUrl;
90729
+ usingDatabaseUrl = true;
90730
+ }
90696
90731
  logger.info("\u4F7F\u7528\u8CC7\u6599\u5EAB\u4E2D\u89E3\u5BC6\u7684 API Key \u9032\u884C\u9A57\u8B49");
90697
90732
  logger.debug(`\u89E3\u5BC6\u5F8C\u7684 API Key \u9577\u5EA6: ${apiKey.length}, \u524D\u7DB4: ${apiKey.substring(0, 3)}...`);
90698
90733
  } else {
90699
90734
  logger.info("\u4F7F\u7528\u65B0\u8F38\u5165\u7684 API Key \u9032\u884C\u9A57\u8B49");
90700
90735
  logger.debug(`\u65B0\u8F38\u5165\u7684 API Key \u9577\u5EA6: ${apiKey.length}, \u524D\u7DB4: ${apiKey.substring(0, 3)}...`);
90701
90736
  }
90702
- const result = await validateAPIKey(apiKey, model);
90737
+ logger.info(`\u9A57\u8B49\u4F7F\u7528 API URL: ${apiUrl} (\u8CC7\u6599\u5EAB: ${usingDatabaseUrl}), OpenAI \u76F8\u5BB9: ${openaiCompatible}`);
90738
+ const result = await validateAPIKey(apiKey, model, apiUrl, openaiCompatible);
90703
90739
  if (result.valid) {
90704
90740
  logger.info(`API Key \u9A57\u8B49\u6210\u529F (${usingDatabaseKey ? "\u8CC7\u6599\u5EAB" : "\u65B0\u8F38\u5165"})`);
90705
90741
  logAPIRequest({
@@ -92587,9 +92623,35 @@ var MCPServer = class {
92587
92623
  this.mcpServer.registerTool(
92588
92624
  "collect_feedback",
92589
92625
  {
92590
- description: "Collect feedback from users about AI work summary. This tool opens a web interface for users to provide feedback on the AI's work.",
92626
+ description: `Collect feedback from users about AI work. This tool opens a web interface for users to provide feedback.
92627
+
92628
+ CRITICAL: The 'work_summary' field is the PRIMARY and ONLY content displayed to users in the feedback UI. You MUST include ALL relevant information in this field as a comprehensive Markdown-formatted report. The UI renders Markdown, so use headings, tables, code blocks, and lists for better readability.`,
92591
92629
  inputSchema: {
92592
- work_summary: external_exports.string().describe("AI\u5DE5\u4F5C\u532F\u5831\u5167\u5BB9\uFF0C\u63CF\u8FF0AI\u5B8C\u6210\u7684\u5DE5\u4F5C\u548C\u7D50\u679C"),
92630
+ work_summary: external_exports.string().describe(`\u3010CRITICAL - THIS IS THE ONLY CONTENT SHOWN TO USERS\u3011
92631
+
92632
+ Include a COMPLETE Markdown report with ALL of the following sections:
92633
+
92634
+ ## Required Sections:
92635
+ 1. **\u{1F4CB} Task Summary** - Brief description of what was requested and accomplished
92636
+ 2. **\u{1F4C1} Implementation Details** - Files created/modified with:
92637
+ - Full file paths
92638
+ - Key code snippets in fenced code blocks
92639
+ - Explanation of changes
92640
+ 3. **\u2705 Status Table** - Markdown table showing completion status:
92641
+ | Item | Status | Notes |
92642
+ |------|--------|-------|
92643
+ | Feature A | \u2705 Done | ... |
92644
+ 4. **\u{1F9EA} Test Results** - Build/test command outputs and outcomes
92645
+ 5. **\u27A1\uFE0F Next Steps** - Actionable options in A/B/C format for user decision:
92646
+ - Option A: [action] - [description]
92647
+ - Option B: [action] - [description]
92648
+ 6. **\u{1F3D7}\uFE0F Architecture** (if applicable) - ASCII diagrams or Mermaid code blocks
92649
+
92650
+ ## Format Requirements:
92651
+ - Use Markdown: ## headings, \`code\`, **bold**, tables
92652
+ - Minimum 500 characters for non-trivial tasks
92653
+ - Be specific with file paths and code examples
92654
+ - Include ALL information user needs to make decisions`),
92593
92655
  project_name: external_exports.string().optional().describe("\u5C08\u6848\u540D\u7A31\uFF08\u7528\u65BC Dashboard \u5206\u7D44\u986F\u793A\uFF09"),
92594
92656
  project_path: external_exports.string().optional().describe("\u5C08\u6848\u8DEF\u5F91\uFF08\u7528\u65BC\u552F\u4E00\u8B58\u5225\u5C08\u6848\uFF09")
92595
92657
  }
@@ -192,12 +192,21 @@
192
192
  </select>
193
193
  </div>
194
194
 
195
- <!-- 擴展提供商專用設定 -->
196
- <div id="nvidiaExtSettings" class="form-group" style="display: none;">
197
- <label class="form-label">API Endpoint</label>
198
- <input type="url" class="form-input" value="https://integrate.api.nvidia.com/v1" readonly>
195
+ <div class="form-group">
196
+ <label class="form-label" for="apiUrl">API Endpoint</label>
197
+ <input type="url" id="apiUrl" class="form-input" placeholder="API 端點 URL">
198
+ <p class="form-help">可自定義 API 端點,留空使用預設值</p>
199
199
  </div>
200
-
200
+
201
+ <div class="form-group">
202
+ <div class="checkbox-group">
203
+ <input type="checkbox" id="openaiCompatible" />
204
+ <label for="openaiCompatible">OpenAI 相容模式</label>
205
+ </div>
206
+ <p class="form-help">啟用後使用 OpenAI API 格式(適用於自建或第三方相容服務)</p>
207
+ </div>
208
+
209
+ <!-- Z.AI 專用設定 -->
201
210
  <div id="zaiExtSettings" class="form-group" style="display: none;">
202
211
  <label class="form-label" for="zaiRegion">地區</label>
203
212
  <select id="zaiRegion" class="form-select">
@@ -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,6 +35,8 @@
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"),
@@ -75,7 +78,6 @@
75
78
  resetPromptsBtn: document.getElementById("resetPromptsBtn"),
76
79
  savePromptsBtn: document.getElementById("savePromptsBtn"),
77
80
  // Extended Provider Settings (integrated into AI settings)
78
- nvidiaExtSettings: document.getElementById("nvidiaExtSettings"),
79
81
  zaiExtSettings: document.getElementById("zaiExtSettings"),
80
82
  zaiRegion: document.getElementById("zaiRegion"),
81
83
  toastContainer: document.getElementById("toastContainer"),
@@ -103,6 +105,9 @@
103
105
  if (elements.aiProvider) {
104
106
  elements.aiProvider.addEventListener("change", handleAIProviderChange);
105
107
  }
108
+ if (elements.zaiRegion) {
109
+ elements.zaiRegion.addEventListener("change", handleZaiRegionChange);
110
+ }
106
111
 
107
112
  // CLI Settings
108
113
  elements.aiModeApi.addEventListener("change", handleAIModeChange);
@@ -130,22 +135,43 @@
130
135
  }
131
136
  }
132
137
 
133
- function handleAIProviderChange() {
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) {
134
148
  const provider = elements.aiProvider?.value || 'google';
135
-
136
- // 隱藏所有擴展設定
137
- if (elements.nvidiaExtSettings) {
138
- elements.nvidiaExtSettings.style.display = 'none';
139
- }
149
+
150
+ // Z.AI 專用設定
140
151
  if (elements.zaiExtSettings) {
141
- elements.zaiExtSettings.style.display = 'none';
152
+ elements.zaiExtSettings.style.display = provider === 'zai' ? 'block' : 'none';
142
153
  }
143
-
144
- // 顯示對應的擴展設定
145
- if (provider === 'nvidia' && elements.nvidiaExtSettings) {
146
- elements.nvidiaExtSettings.style.display = 'block';
147
- } else if (provider === 'zai' && elements.zaiExtSettings) {
148
- elements.zaiExtSettings.style.display = 'block';
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';
149
175
  }
150
176
  }
151
177
 
@@ -182,6 +208,14 @@
182
208
  // 從 apiUrl 反向推斷 provider
183
209
  const provider = getProviderFromApiUrl(data.settings.apiUrl);
184
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
+ }
185
219
  // API 返回的是 apiKeyMasked(遮罩後的 key),顯示給用戶看
186
220
  originalApiKeyMasked = data.settings.apiKeyMasked || "";
187
221
  elements.apiKey.value = originalApiKeyMasked;
@@ -192,6 +226,8 @@
192
226
  elements.autoReplyTimerSeconds.value = data.settings.autoReplyTimerSeconds ?? 300;
193
227
  elements.maxToolRounds.value = data.settings.maxToolRounds ?? 5;
194
228
  elements.debugMode.checked = data.settings.debugMode || false;
229
+ // 更新 UI(不更新 URL,因為已經從資料庫載入)
230
+ handleAIProviderChange(false);
195
231
  }
196
232
  } catch (error) {
197
233
  console.error("Failed to load AI settings:", error);
@@ -326,6 +362,8 @@
326
362
  async function testAIConnection() {
327
363
  const apiKey = elements.apiKey.value;
328
364
  const model = elements.aiModel.value;
365
+ const provider = elements.aiProvider.value;
366
+ const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
329
367
 
330
368
  // 如果 API key 是遮罩值,表示用戶沒有修改,將使用資料庫中的 key
331
369
  const apiKeyChanged = apiKey !== originalApiKeyMasked;
@@ -344,8 +382,12 @@
344
382
  elements.testAiBtn.textContent = "測試中...";
345
383
 
346
384
  try {
347
- // 如果用戶修改了 API key 就傳送新的 key,否則不傳送(後端會使用資料庫中的)
348
- const payload = { model };
385
+ // 傳送當前表單的設定值進行測試
386
+ const payload = {
387
+ model,
388
+ apiUrl,
389
+ openaiCompatible: elements.openaiCompatible?.checked || false
390
+ };
349
391
  if (apiKeyChanged) {
350
392
  payload.apiKey = apiKey;
351
393
  }
@@ -375,18 +417,22 @@
375
417
  async function saveAISettings() {
376
418
  const provider = elements.aiProvider.value;
377
419
  const currentApiKey = elements.apiKey.value;
378
-
420
+
379
421
  // 只有當用戶真的修改了 API key 才傳送(不是遮罩值)
380
422
  const apiKeyChanged = currentApiKey !== originalApiKeyMasked;
381
-
423
+
424
+ // 使用表單中的 API URL,若為空則使用預設值
425
+ const apiUrl = elements.apiUrl?.value || DEFAULT_API_URLS[provider] || '';
426
+
382
427
  const settings = {
383
- apiUrl: getApiUrlFromProvider(provider),
428
+ apiUrl: apiUrl,
384
429
  model: elements.aiModel.value,
385
430
  temperature: parseFloat(elements.temperature.value) || 0.7,
386
431
  maxTokens: parseInt(elements.maxTokens.value) || 1000,
387
432
  autoReplyTimerSeconds: parseInt(elements.autoReplyTimerSeconds.value) || 300,
388
433
  maxToolRounds: parseInt(elements.maxToolRounds.value) || 5,
389
434
  debugMode: elements.debugMode.checked,
435
+ openaiCompatible: elements.openaiCompatible?.checked || false,
390
436
  };
391
437
 
392
438
  // 只有修改了 API key 才加入
@@ -574,7 +620,7 @@
574
620
  function renderPromptConfigs() {
575
621
  if (!elements.promptConfigList || !promptConfigs.length) return;
576
622
 
577
- const showEditor = (id) => id !== 'user_context' && id !== 'tool_results';
623
+ const showEditor = (id) => id !== 'user_context' && id !== 'tool_results' && id !== 'mcp_tools_detailed';
578
624
 
579
625
  elements.promptConfigList.innerHTML = promptConfigs.map(config => `
580
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;">
@@ -680,7 +726,20 @@
680
726
  function showToast(message, type = "info") {
681
727
  const toast = document.createElement("div");
682
728
  toast.className = `toast toast-${type}`;
683
- 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);
684
743
 
685
744
  elements.toastContainer.appendChild(toast);
686
745
 
@@ -688,12 +747,15 @@
688
747
  toast.classList.add("show");
689
748
  }, 10);
690
749
 
691
- setTimeout(() => {
692
- toast.classList.remove("show");
750
+ // 錯誤訊息不自動關閉,其他類型 3 秒後關閉
751
+ if (type !== "error") {
693
752
  setTimeout(() => {
694
- toast.remove();
695
- }, 300);
696
- }, 3000);
753
+ toast.classList.remove("show");
754
+ setTimeout(() => {
755
+ toast.remove();
756
+ }, 300);
757
+ }, 3000);
758
+ }
697
759
  }
698
760
 
699
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.2",
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,8 +46,9 @@
45
46
  "prepublishOnly": "npm run clean && npm run build && node scripts/remove-sourcemaps.cjs"
46
47
  },
47
48
  "dependencies": {
48
- "@hirohsu/user-web-feedback": "^2.8.1",
49
- "better-sqlite3": "^12.4.1"
49
+ "@hirohsu/user-web-feedback": "^2.8.2",
50
+ "better-sqlite3": "^12.4.1",
51
+ "openai": "^6.16.0"
50
52
  },
51
53
  "optionalDependencies": {
52
54
  "@google/generative-ai": "^0.24.1",