- 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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
137
src/app/api/oauth/iflow/cookie/route.js
Normal file
137
src/app/api/oauth/iflow/cookie/route.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user