- Refines the overall structure of the CLI tools and MITM server functionalities.

- Add buildQwenBaseUrl function to construct URLs for Qwen resources.
- Update buildProviderUrl to support Qwen model requests.
- Enhance token refresh logic to include provider-specific data for Qwen.
- Refactor CLI Tools page to exclude MITM tools and streamline model retrieval.
- Introduce new components for MITM server management.
- Update API routes to handle Qwen-specific resource URLs and improve error handling.
This commit is contained in:
decolua
2026-03-05 11:25:03 +07:00
parent 40a53fbd33
commit 573b0f0241
29 changed files with 1490 additions and 438 deletions

View File

@@ -1,21 +1,37 @@
"use server";
import { NextResponse } from "next/server";
import { getMitmStatus, startMitm, stopMitm, getCachedPassword, setCachedPassword, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
import {
getMitmStatus,
startServer,
stopServer,
enableToolDNS,
disableToolDNS,
getCachedPassword,
setCachedPassword,
loadEncryptedPassword,
initDbHooks,
} from "@/mitm/manager";
import { getSettings, updateSettings } from "@/lib/localDb";
// Inject DB hooks so manager.js (CJS) can persist settings without dynamic import issues
initDbHooks(getSettings, updateSettings);
// GET - Check MITM status
const isWin = process.platform === "win32";
function getPassword(provided) {
return provided || getCachedPassword() || null;
}
// GET - Full MITM status (server + per-tool DNS)
export async function GET() {
try {
const status = await getMitmStatus();
return NextResponse.json({
running: status.running,
pid: status.pid || null,
dnsConfigured: status.dnsConfigured || false,
certExists: status.certExists || false,
dnsStatus: status.dnsStatus || {},
certCoversTools: status.certCoversTools || {},
hasCachedPassword: !!getCachedPassword(),
});
} catch (error) {
@@ -24,13 +40,11 @@ export async function GET() {
}
}
// POST - Start MITM proxy
// POST - Start MITM server (cert + server, no DNS)
export async function POST(request) {
try {
const { apiKey, sudoPassword } = await request.json();
const isWin = process.platform === "win32";
// Priority: request password → in-memory cache → encrypted db
const pwd = sudoPassword || getCachedPassword() || await loadEncryptedPassword() || "";
const pwd = getPassword(sudoPassword) || await loadEncryptedPassword() || "";
if (!apiKey || (!isWin && !pwd)) {
return NextResponse.json(
@@ -39,38 +53,64 @@ export async function POST(request) {
);
}
const result = await startMitm(apiKey, pwd);
const result = await startServer(apiKey, pwd);
if (!isWin) setCachedPassword(pwd);
return NextResponse.json({
success: true,
running: result.running,
pid: result.pid,
steps: result.steps || { cert: true, server: true, dns: true },
});
return NextResponse.json({ success: true, running: result.running, pid: result.pid });
} catch (error) {
console.log("Error starting MITM:", error.message);
return NextResponse.json({ error: error.message || "Failed to start MITM proxy" }, { status: 500 });
console.log("Error starting MITM server:", error.message);
return NextResponse.json({ error: error.message || "Failed to start MITM server" }, { status: 500 });
}
}
// DELETE - Stop MITM proxy
// DELETE - Stop MITM server (removes all DNS first, then kills server)
export async function DELETE(request) {
try {
const { sudoPassword } = await request.json();
const isWin = process.platform === "win32";
const pwd = sudoPassword || getCachedPassword() || await loadEncryptedPassword() || "";
const body = await request.json().catch(() => ({}));
const { sudoPassword } = body;
const pwd = getPassword(sudoPassword) || await loadEncryptedPassword() || "";
if (!isWin && !pwd) {
return NextResponse.json({ error: "Missing sudoPassword" }, { status: 400 });
}
await stopMitm(pwd);
await stopServer(pwd);
if (!isWin && sudoPassword) setCachedPassword(sudoPassword);
return NextResponse.json({ success: true, running: false });
} catch (error) {
console.log("Error stopping MITM:", error.message);
return NextResponse.json({ error: error.message || "Failed to stop MITM proxy" }, { status: 500 });
console.log("Error stopping MITM server:", error.message);
return NextResponse.json({ error: error.message || "Failed to stop MITM server" }, { status: 500 });
}
}
// PATCH - Toggle DNS for a specific tool (enable/disable)
export async function PATCH(request) {
try {
const { tool, action, sudoPassword } = await request.json();
const pwd = getPassword(sudoPassword) || await loadEncryptedPassword() || "";
if (!tool || !action) {
return NextResponse.json({ error: "tool and action required" }, { status: 400 });
}
if (!isWin && !pwd) {
return NextResponse.json({ error: "Missing sudoPassword" }, { status: 400 });
}
if (action === "enable") {
await enableToolDNS(tool, pwd);
} else if (action === "disable") {
await disableToolDNS(tool, pwd);
} else {
return NextResponse.json({ error: "action must be enable or disable" }, { status: 400 });
}
if (!isWin && sudoPassword) setCachedPassword(sudoPassword);
const status = await getMitmStatus();
return NextResponse.json({ success: true, dnsStatus: status.dnsStatus });
} catch (error) {
console.log("Error toggling DNS:", error.message);
return NextResponse.json({ error: error.message || "Failed to toggle DNS" }, { status: 500 });
}
}

View File

@@ -34,15 +34,55 @@ export async function POST(request) {
});
const latencyMs = Date.now() - start;
// 200 = ok; 400 = bad request but auth passed (model reachable)
const ok = res.status === 200 || res.status === 400;
let error = null;
if (!ok) {
const text = await res.text().catch(() => "");
error = `HTTP ${res.status}${text ? `: ${text.slice(0, 120)}` : ""}`;
const rawText = await res.text().catch(() => "");
let parsed = null;
try {
parsed = rawText ? JSON.parse(rawText) : null;
} catch {}
if (!res.ok) {
const detail = parsed?.error?.message || parsed?.msg || parsed?.message || parsed?.error || rawText;
const error = `HTTP ${res.status}${detail ? `: ${String(detail).slice(0, 240)}` : ""}`;
return NextResponse.json({ ok: false, latencyMs, error, status: res.status });
}
return NextResponse.json({ ok, latencyMs, error });
// Some providers may return HTTP 200 but not a real completion for invalid models.
const providerStatus = parsed?.status;
const providerMsg = parsed?.msg || parsed?.message;
const hasProviderErrorStatus = providerStatus !== undefined
&& providerStatus !== null
&& String(providerStatus) !== "200"
&& String(providerStatus) !== "0";
if (hasProviderErrorStatus && providerMsg) {
return NextResponse.json({
ok: false,
latencyMs,
status: res.status,
error: `Provider status ${providerStatus}: ${String(providerMsg).slice(0, 240)}`,
});
}
if (parsed?.error) {
const providerError = parsed?.error?.message || parsed?.error || "Provider returned an error";
return NextResponse.json({
ok: false,
latencyMs,
status: res.status,
error: String(providerError).slice(0, 240),
});
}
const hasChoices = Array.isArray(parsed?.choices) && parsed.choices.length > 0;
if (!hasChoices) {
return NextResponse.json({
ok: false,
latencyMs,
status: res.status,
error: "Provider returned no completion choices for this model",
});
}
return NextResponse.json({ ok: true, latencyMs, error: null, status: res.status });
} catch (err) {
return NextResponse.json({ ok: false, error: err.message }, { status: 500 });
}

View File

@@ -0,0 +1,137 @@
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
/**
* iFlow Cookie-Based Authentication
* POST /api/oauth/iflow/cookie
* Body: { cookie: "BXAuth=xxx; ..." }
*/
export async function POST(request) {
try {
const { cookie } = await request.json();
if (!cookie || typeof cookie !== "string") {
return NextResponse.json({ error: "Cookie is required" }, { status: 400 });
}
// Normalize cookie
const trimmed = cookie.trim();
if (!trimmed.includes("BXAuth=")) {
return NextResponse.json({ error: "Cookie must contain BXAuth field" }, { status: 400 });
}
let normalizedCookie = trimmed;
if (!normalizedCookie.endsWith(";")) {
normalizedCookie += ";";
}
// Step 1: GET API key info to get the name
const getResponse = await fetch("https://platform.iflow.cn/api/openapi/apikey", {
method: "GET",
headers: {
"Cookie": normalizedCookie,
"Accept": "application/json, text/plain, */*",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
},
});
if (!getResponse.ok) {
const errorText = await getResponse.text();
return NextResponse.json(
{ error: `Failed to fetch API key info: ${errorText}` },
{ status: getResponse.status }
);
}
const getResult = await getResponse.json();
if (!getResult.success) {
return NextResponse.json(
{ error: `API key fetch failed: ${getResult.message}` },
{ status: 400 }
);
}
const keyData = getResult.data;
if (!keyData.name) {
return NextResponse.json({ error: "Missing name in API key info" }, { status: 400 });
}
// Step 2: POST to refresh API key
const postResponse = await fetch("https://platform.iflow.cn/api/openapi/apikey", {
method: "POST",
headers: {
"Cookie": normalizedCookie,
"Content-Type": "application/json",
"Accept": "application/json, text/plain, */*",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Origin": "https://platform.iflow.cn",
"Referer": "https://platform.iflow.cn/",
},
body: JSON.stringify({ name: keyData.name }),
});
if (!postResponse.ok) {
const errorText = await postResponse.text();
return NextResponse.json(
{ error: `Failed to refresh API key: ${errorText}` },
{ status: postResponse.status }
);
}
const postResult = await postResponse.json();
if (!postResult.success) {
return NextResponse.json(
{ error: `API key refresh failed: ${postResult.message}` },
{ status: 400 }
);
}
const refreshedKey = postResult.data;
if (!refreshedKey.apiKey) {
return NextResponse.json({ error: "Missing API key in response" }, { status: 400 });
}
// Extract only BXAuth from cookie
const bxAuthMatch = normalizedCookie.match(/BXAuth=([^;]+)/);
const bxAuth = bxAuthMatch ? bxAuthMatch[1] : "";
const cookieToSave = bxAuth ? `BXAuth=${bxAuth};` : "";
// Save to database
const connection = await createProviderConnection({
provider: "iflow",
authType: "cookie",
name: refreshedKey.name || keyData.name,
email: refreshedKey.name || keyData.name,
apiKey: refreshedKey.apiKey,
providerSpecificData: {
cookie: cookieToSave,
expireTime: refreshedKey.expireTime,
},
testStatus: "active",
isActive: true,
});
return NextResponse.json({
success: true,
connection: {
id: connection.id,
provider: connection.provider,
email: connection.email,
apiKey: refreshedKey.apiKey.substring(0, 10) + "...", // masked
expireTime: refreshedKey.expireTime,
},
});
} catch (error) {
console.error("iFlow cookie auth error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -44,6 +44,18 @@ const createOpenAIModelsConfig = (url) => ({
parseResponse: parseOpenAIStyleModels
});
const resolveQwenModelsUrl = (connection) => {
const fallback = "https://portal.qwen.ai/v1/models";
const raw = connection?.providerSpecificData?.resourceUrl;
if (!raw || typeof raw !== "string") return fallback;
const value = raw.trim();
if (!value) return fallback;
if (value.startsWith("http://") || value.startsWith("https://")) {
return `${value.replace(/\/$/, "")}/models`;
}
return `https://${value.replace(/\/$/, "")}/v1/models`;
};
// Provider models endpoints configuration
const PROVIDER_MODELS_CONFIG = {
claude: {
@@ -340,6 +352,9 @@ export async function GET(request, { params }) {
// Build request URL
let url = config.url;
if (connection.provider === "qwen") {
url = resolveQwenModelsUrl(connection);
}
if (config.authQuery) {
url += `?${config.authQuery}=${token}`;
}

View File

@@ -33,7 +33,8 @@ export async function POST(request) {
// Build URL and headers using provider service
const url = buildProviderUrl(provider, body.model || "test-model", true, {
baseUrlIndex: 0,
baseUrl: connection.providerSpecificData?.baseUrl
baseUrl: connection.providerSpecificData?.baseUrl,
qwenResourceUrl: connection.providerSpecificData?.resourceUrl
});
console.log("🚀 ~ POST ~ url:", url)
const headers = buildProviderHeaders(provider, credentials, true, body);

View File

@@ -93,7 +93,8 @@ export async function POST(request) {
// Build URL and headers
const url = buildProviderUrl(provider, model, true, {
baseUrlIndex: 0,
baseUrl: connection.providerSpecificData?.baseUrl
baseUrl: connection.providerSpecificData?.baseUrl,
qwenResourceUrl: connection.providerSpecificData?.resourceUrl
});
const headers = buildProviderHeaders(provider, credentials, true, actualBody);