@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 +67 -5
- package/dist/index.cjs +67 -5
- package/dist/static/settings.html +14 -5
- package/dist/static/settings.js +89 -27
- package/package.json +6 -4
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
197
|
-
<
|
|
198
|
-
<
|
|
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">
|
package/dist/static/settings.js
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
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 (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
//
|
|
348
|
-
const payload = {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
692
|
-
|
|
750
|
+
// 錯誤訊息不自動關閉,其他類型 3 秒後關閉
|
|
751
|
+
if (type !== "error") {
|
|
693
752
|
setTimeout(() => {
|
|
694
|
-
toast.remove();
|
|
695
|
-
|
|
696
|
-
|
|
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.
|
|
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.
|
|
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",
|