mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
- Updated CLI tool components to accept initial status as a prop, improving state management for tool statuses.
- Added functionality to fetch and set statuses for various CLI tools (Claude, Codex, Droid, OpenClaw, Antigravity) on component mount. - Enhanced error handling and logging in the OAuth provider test utilities and DNS management functions. - Improved the MITM server to handle multiple target hosts and provide clearer error messages regarding port usage.
This commit is contained in:
@@ -8,6 +8,14 @@ import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, Default
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
const STATUS_ENDPOINTS = {
|
||||
claude: "/api/cli-tools/claude-settings",
|
||||
codex: "/api/cli-tools/codex-settings",
|
||||
droid: "/api/cli-tools/droid-settings",
|
||||
openclaw: "/api/cli-tools/openclaw-settings",
|
||||
antigravity: "/api/cli-tools/antigravity-mitm",
|
||||
};
|
||||
|
||||
export default function CLIToolsPageClient({ machineId }) {
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -17,13 +25,34 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
const [tunnelEnabled, setTunnelEnabled] = useState(false);
|
||||
const [tunnelUrl, setTunnelUrl] = useState("");
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
const [toolStatuses, setToolStatuses] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchConnections();
|
||||
loadCloudSettings();
|
||||
fetchApiKeys();
|
||||
fetchAllStatuses();
|
||||
}, []);
|
||||
|
||||
const fetchAllStatuses = async () => {
|
||||
try {
|
||||
const entries = await Promise.all(
|
||||
Object.entries(STATUS_ENDPOINTS).map(async ([toolId, url]) => {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
return [toolId, data];
|
||||
} catch {
|
||||
return [toolId, null];
|
||||
}
|
||||
})
|
||||
);
|
||||
setToolStatuses(Object.fromEntries(entries));
|
||||
} catch (error) {
|
||||
console.log("Error fetching tool statuses:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadCloudSettings = async () => {
|
||||
try {
|
||||
const [settingsRes, tunnelRes] = await Promise.all([
|
||||
@@ -165,16 +194,17 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
onModelMappingChange={(alias, target) => handleModelMappingChange(toolId, alias, target)}
|
||||
hasActiveProviders={hasActiveProviders}
|
||||
cloudEnabled={cloudEnabled}
|
||||
initialStatus={toolStatuses.claude}
|
||||
/>
|
||||
);
|
||||
case "codex":
|
||||
return <CodexToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />;
|
||||
return <CodexToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.codex} />;
|
||||
case "droid":
|
||||
return <DroidToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />;
|
||||
return <DroidToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.droid} />;
|
||||
case "openclaw":
|
||||
return <OpenClawToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />;
|
||||
return <OpenClawToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.openclaw} />;
|
||||
case "antigravity":
|
||||
return <AntigravityToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />;
|
||||
return <AntigravityToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.antigravity} />;
|
||||
default:
|
||||
return <DefaultToolCard key={toolId} toolId={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ export default function AntigravityToolCard({
|
||||
activeProviders,
|
||||
hasActiveProviders,
|
||||
cloudEnabled,
|
||||
initialStatus,
|
||||
}) {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [status, setStatus] = useState(initialStatus || null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
const [sudoPassword, setSudoPassword] = useState("");
|
||||
@@ -30,12 +31,17 @@ export default function AntigravityToolCard({
|
||||
}
|
||||
}, [apiKeys, selectedApiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialStatus) setStatus(initialStatus);
|
||||
}, [initialStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded && !status) {
|
||||
fetchStatus();
|
||||
loadSavedMappings();
|
||||
}
|
||||
}, [isExpanded, status]);
|
||||
if (isExpanded) loadSavedMappings();
|
||||
}, [isExpanded]);
|
||||
|
||||
const loadSavedMappings = async () => {
|
||||
try {
|
||||
|
||||
@@ -17,8 +17,9 @@ export default function ClaudeToolCard({
|
||||
hasActiveProviders,
|
||||
apiKeys,
|
||||
cloudEnabled,
|
||||
initialStatus,
|
||||
}) {
|
||||
const [claudeStatus, setClaudeStatus] = useState(null);
|
||||
const [claudeStatus, setClaudeStatus] = useState(initialStatus || null);
|
||||
const [checkingClaude, setCheckingClaude] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
@@ -51,12 +52,17 @@ export default function ClaudeToolCard({
|
||||
}
|
||||
}, [apiKeys, selectedApiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialStatus) setClaudeStatus(initialStatus);
|
||||
}, [initialStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded && !claudeStatus) {
|
||||
checkClaudeStatus();
|
||||
fetchModelAliases();
|
||||
}
|
||||
}, [isExpanded, claudeStatus]);
|
||||
if (isExpanded) fetchModelAliases();
|
||||
}, [isExpanded]);
|
||||
|
||||
const fetchModelAliases = async () => {
|
||||
try {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useState, useEffect } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled }) {
|
||||
const [codexStatus, setCodexStatus] = useState(null);
|
||||
export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) {
|
||||
const [codexStatus, setCodexStatus] = useState(initialStatus || null);
|
||||
const [checkingCodex, setCheckingCodex] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
@@ -24,12 +24,17 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
||||
}
|
||||
}, [apiKeys, selectedApiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialStatus) setCodexStatus(initialStatus);
|
||||
}, [initialStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded && !codexStatus) {
|
||||
checkCodexStatus();
|
||||
fetchModelAliases();
|
||||
}
|
||||
}, [isExpanded, codexStatus]);
|
||||
if (isExpanded) fetchModelAliases();
|
||||
}, [isExpanded]);
|
||||
|
||||
const fetchModelAliases = async () => {
|
||||
try {
|
||||
|
||||
@@ -15,8 +15,9 @@ export default function DroidToolCard({
|
||||
apiKeys,
|
||||
activeProviders,
|
||||
cloudEnabled,
|
||||
initialStatus,
|
||||
}) {
|
||||
const [droidStatus, setDroidStatus] = useState(null);
|
||||
const [droidStatus, setDroidStatus] = useState(initialStatus || null);
|
||||
const [checkingDroid, setCheckingDroid] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
@@ -48,12 +49,17 @@ export default function DroidToolCard({
|
||||
}
|
||||
}, [apiKeys, selectedApiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialStatus) setDroidStatus(initialStatus);
|
||||
}, [initialStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded && !droidStatus) {
|
||||
checkDroidStatus();
|
||||
fetchModelAliases();
|
||||
}
|
||||
}, [isExpanded, droidStatus]);
|
||||
if (isExpanded) fetchModelAliases();
|
||||
}, [isExpanded]);
|
||||
|
||||
const fetchModelAliases = async () => {
|
||||
try {
|
||||
|
||||
@@ -13,8 +13,9 @@ export default function OpenClawToolCard({
|
||||
apiKeys,
|
||||
activeProviders,
|
||||
cloudEnabled,
|
||||
initialStatus,
|
||||
}) {
|
||||
const [openclawStatus, setOpenclawStatus] = useState(null);
|
||||
const [openclawStatus, setOpenclawStatus] = useState(initialStatus || null);
|
||||
const [checkingOpenclaw, setCheckingOpenclaw] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
@@ -45,12 +46,17 @@ export default function OpenClawToolCard({
|
||||
}
|
||||
}, [apiKeys, selectedApiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialStatus) setOpenclawStatus(initialStatus);
|
||||
}, [initialStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded && !openclawStatus) {
|
||||
checkOpenclawStatus();
|
||||
fetchModelAliases();
|
||||
}
|
||||
}, [isExpanded, openclawStatus]);
|
||||
if (isExpanded) fetchModelAliases();
|
||||
}, [isExpanded]);
|
||||
|
||||
const fetchModelAliases = async () => {
|
||||
try {
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
ANTIGRAVITY_CONFIG,
|
||||
CODEX_CONFIG,
|
||||
KIRO_CONFIG,
|
||||
QWEN_CONFIG,
|
||||
CLAUDE_CONFIG,
|
||||
} from "@/lib/oauth/constants/oauth";
|
||||
|
||||
// OAuth provider test endpoints
|
||||
const OAUTH_TEST_CONFIG = {
|
||||
claude: { checkExpiry: true },
|
||||
claude: { checkExpiry: true, refreshable: true },
|
||||
codex: { checkExpiry: true, refreshable: true },
|
||||
"gemini-cli": {
|
||||
url: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
@@ -33,18 +35,14 @@ const OAUTH_TEST_CONFIG = {
|
||||
extraHeaders: { "User-Agent": "9Router", "Accept": "application/vnd.github+json" },
|
||||
},
|
||||
iflow: {
|
||||
url: "https://iflow.cn/api/oauth/getUserInfo",
|
||||
// iFlow getUserInfo requires accessToken as query param, not header
|
||||
buildUrl: (token) => `https://iflow.cn/api/oauth/getUserInfo?accessToken=${encodeURIComponent(token)}`,
|
||||
method: "GET",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
},
|
||||
qwen: {
|
||||
url: "https://portal.qwen.ai/v1/models",
|
||||
method: "GET",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
noAuth: true,
|
||||
},
|
||||
qwen: { checkExpiry: true, refreshable: true },
|
||||
kiro: { checkExpiry: true, refreshable: true },
|
||||
cursor: { tokenExists: true },
|
||||
};
|
||||
|
||||
async function refreshOAuthToken(connection) {
|
||||
@@ -85,8 +83,26 @@ async function refreshOAuthToken(connection) {
|
||||
return { accessToken: data.access_token, expiresIn: data.expires_in, refreshToken: data.refresh_token || refreshToken };
|
||||
}
|
||||
|
||||
if (provider === "claude") {
|
||||
const response = await fetch(CLAUDE_CONFIG.tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: CLAUDE_CONFIG.clientId,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
return { accessToken: data.access_token, expiresIn: data.expires_in, refreshToken: data.refresh_token || refreshToken };
|
||||
}
|
||||
|
||||
if (provider === "kiro") {
|
||||
const { clientId, clientSecret, region } = connection;
|
||||
const psd = connection.providerSpecificData || {};
|
||||
const clientId = psd.clientId || connection.clientId;
|
||||
const clientSecret = psd.clientSecret || connection.clientSecret;
|
||||
const region = psd.region || connection.region;
|
||||
if (clientId && clientSecret) {
|
||||
const endpoint = `https://oidc.${region || "us-east-1"}.amazonaws.com/token`;
|
||||
const response = await fetch(endpoint, {
|
||||
@@ -100,7 +116,7 @@ async function refreshOAuthToken(connection) {
|
||||
}
|
||||
const response = await fetch(KIRO_CONFIG.socialRefreshUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json", "User-Agent": "kiro-cli/1.0.0" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
@@ -108,6 +124,21 @@ async function refreshOAuthToken(connection) {
|
||||
return { accessToken: data.accessToken, expiresIn: data.expiresIn || 3600, refreshToken: data.refreshToken || refreshToken };
|
||||
}
|
||||
|
||||
if (provider === "qwen") {
|
||||
const response = await fetch(QWEN_CONFIG.tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: QWEN_CONFIG.clientId,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
return { accessToken: data.access_token, expiresIn: data.expires_in, refreshToken: data.refresh_token || refreshToken };
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.log(`Error refreshing ${provider} token:`, err.message);
|
||||
@@ -127,6 +158,11 @@ async function testOAuthConnection(connection) {
|
||||
if (!config) return { valid: false, error: "Provider test not supported", refreshed: false };
|
||||
if (!connection.accessToken) return { valid: false, error: "No access token", refreshed: false };
|
||||
|
||||
// Cursor uses protobuf API - can only verify token exists, not test endpoint
|
||||
if (config.tokenExists) {
|
||||
return { valid: true, error: null, refreshed: false, newTokens: null };
|
||||
}
|
||||
|
||||
let accessToken = connection.accessToken;
|
||||
let refreshed = false;
|
||||
let newTokens = null;
|
||||
@@ -150,17 +186,24 @@ async function testOAuthConnection(connection) {
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = { [config.authHeader]: `${config.authPrefix}${accessToken}`, ...config.extraHeaders };
|
||||
const res = await fetch(config.url, { method: config.method, headers });
|
||||
const testUrl = config.buildUrl ? config.buildUrl(accessToken) : config.url;
|
||||
const headers = config.noAuth
|
||||
? { ...config.extraHeaders }
|
||||
: { [config.authHeader]: `${config.authPrefix}${accessToken}`, ...config.extraHeaders };
|
||||
const res = await fetch(testUrl, { method: config.method, headers });
|
||||
|
||||
if (res.ok) return { valid: true, error: null, refreshed, newTokens };
|
||||
|
||||
if (res.status === 401 && config.refreshable && !refreshed && connection.refreshToken) {
|
||||
const tokens = await refreshOAuthToken(connection);
|
||||
if (tokens) {
|
||||
const retryRes = await fetch(config.url, {
|
||||
const retryUrl = config.buildUrl ? config.buildUrl(tokens.accessToken) : testUrl;
|
||||
const retryHeaders = config.noAuth
|
||||
? { ...config.extraHeaders }
|
||||
: { [config.authHeader]: `${config.authPrefix}${tokens.accessToken}`, ...config.extraHeaders };
|
||||
const retryRes = await fetch(retryUrl, {
|
||||
method: config.method,
|
||||
headers: { [config.authHeader]: `${config.authPrefix}${tokens.accessToken}`, ...config.extraHeaders },
|
||||
headers: retryHeaders,
|
||||
});
|
||||
if (retryRes.ok) return { valid: true, error: null, refreshed: true, newTokens: tokens };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user