This commit is contained in:
decolua
2026-03-06 09:59:15 +07:00
parent 75f486b7a2
commit b7b4ac5592
7 changed files with 86 additions and 40 deletions

View File

@@ -145,7 +145,7 @@ Default URLs:
<tr>
<td align="center" width="120">
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
<b>Claude Code</b>
<b>Claude-Code</b>
</td>
<td align="center" width="120">
<img src="./public/providers/openclaw.png" width="60" alt="OpenClaw"/><br/>
@@ -193,7 +193,36 @@ Default URLs:
## 🌐 Supported Providers
### 🆓 Free Providers (Unlimited)
### 🔐 OAuth Providers
<div align="center">
<table>
<tr>
<td align="center" width="120">
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
<b>Claude-Code</b>
</td>
<td align="center" width="120">
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
<b>Antigravity</b>
</td>
<td align="center" width="120">
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
<b>Codex</b>
</td>
<td align="center" width="120">
<img src="./public/providers/github.png" width="60" alt="GitHub"/><br/>
<b>GitHub</b>
</td>
<td align="center" width="120">
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
<b>Cursor</b>
</td>
</tr>
</table>
</div>
### 🆓 Free Providers
<div align="center">
<table>
@@ -222,35 +251,6 @@ Default URLs:
</table>
</div>
### 🔐 OAuth Providers
<div align="center">
<table>
<tr>
<td align="center" width="120">
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
<b>Claude Code</b>
</td>
<td align="center" width="120">
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
<b>Antigravity</b>
</td>
<td align="center" width="120">
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
<b>Codex</b>
</td>
<td align="center" width="120">
<img src="./public/providers/github.png" width="60" alt="GitHub"/><br/>
<b>GitHub</b>
</td>
<td align="center" width="120">
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
<b>Cursor</b>
</td>
</tr>
</table>
</div>
### 🔑 API Key Providers (40+)
<div align="center">

View File

@@ -1,6 +1,6 @@
{
"name": "9router-app",
"version": "0.3.32",
"version": "0.3.33",
"description": "9Router web dashboard",
"private": true,
"scripts": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,5 +1,37 @@
import { NextResponse } from "next/server";
// Fetch with timeout wrapper
const fetchWithTimeout = (url, options, timeout = 10000) => {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timeout")), timeout)
)
]);
};
// Validate URL format
const isValidUrl = (url) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
// Parse error details for user-friendly messages
const getErrorMessage = (error) => {
if (error.cause?.code === "ECONNREFUSED") return "Connection refused - provider node offline or unreachable";
if (error.cause?.code === "ENOTFOUND") return "DNS lookup failed - invalid domain or network issue";
if (error.cause?.code === "ETIMEDOUT") return "Connection timeout - provider node too slow";
if (error.message.includes("timeout")) return "Request timeout (>10s) - provider node not responding";
if (error.cause?.code === "CERT_HAS_EXPIRED") return "SSL certificate expired";
if (error.cause?.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") return "SSL certificate verification failed";
if (error.cause?.code) return `Network error: ${error.cause.code}`;
return "Network connection failed - check URL and network connectivity";
};
// POST /api/provider-nodes/validate - Validate API key against base URL
export async function POST(request) {
try {
@@ -10,6 +42,11 @@ export async function POST(request) {
return NextResponse.json({ error: "Base URL and API key required" }, { status: 400 });
}
// Validate URL format
if (!isValidUrl(baseUrl)) {
return NextResponse.json({ error: "Invalid URL format" }, { status: 400 });
}
// Anthropic Compatible Validation
if (type === "anthropic-compatible") {
// Robustly construct URL: remove trailing slash, and remove trailing /messages if user added it
@@ -21,7 +58,7 @@ export async function POST(request) {
// Use /models endpoint for validation as many compatible providers support it (like OpenAI)
const modelsUrl = `${normalizedBase}/models`;
const res = await fetch(modelsUrl, {
const res = await fetchWithTimeout(modelsUrl, {
method: "GET",
headers: {
"x-api-key": apiKey,
@@ -30,18 +67,27 @@ export async function POST(request) {
}
});
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" });
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" });
}
// OpenAI Compatible Validation (Default)
const modelsUrl = `${baseUrl.replace(/\/$/, "")}/models`;
const res = await fetch(modelsUrl, {
const res = await fetchWithTimeout(modelsUrl, {
headers: { "Authorization": `Bearer ${apiKey}` },
});
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" });
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" });
} catch (error) {
console.log("Error validating provider node:", error);
return NextResponse.json({ error: "Validation failed" }, { status: 500 });
const errorMessage = getErrorMessage(error);
console.error("Error validating provider node:", {
message: error.message,
cause: error.cause,
code: error.cause?.code,
userMessage: errorMessage
});
return NextResponse.json({
valid: false,
error: errorMessage
}, { status: 500 });
}
}

View File

@@ -374,7 +374,7 @@ async function startServer(apiKey, sudoPassword) {
].join("\r\n");
const tmpVbs = path.join(os.tmpdir(), `mitm_uac_${Date.now()}.vbs`);
fs.writeFileSync(tmpVbs, vbs, "utf8");
spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: false, detached: true }).unref();
spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref();
await new Promise((resolve, reject) => {
const deadline = Date.now() + 90000;
@@ -511,7 +511,7 @@ async function stopServer(sudoPassword) {
].join("\r\n");
const tmpVbs = path.join(os.tmpdir(), "mitm_stop_uac.vbs");
fs.writeFileSync(tmpVbs, vbs, "utf8");
spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: false, detached: true }).unref();
spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref();
await new Promise((resolve) => {
const deadline = Date.now() + 30000;