AI_PROJECT / src /ui-app /App.jsx
chenchenaoyang's picture
Deploy from Codex
f395b70 verified
Raw
History Blame Contribute Delete
43.9 kB
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import { Alert, Button, Form, InputNumber, Layout, Menu, Segmented, Select, Space, Spin, Switch, Tabs, Tag, Tooltip, Typography, message } from "antd";
import {
ApiOutlined,
AppstoreOutlined,
BugOutlined,
DatabaseOutlined,
RadarChartOutlined,
SettingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from "@ant-design/icons";
import { PoolEditorDrawer, PoolImportModal, ProbeLogModal } from "./components/UiShared.jsx";
import {
API_POOL_SUBTABS,
TOOL_ORDER,
buildPreview,
collectDefaults,
copyPoolItem,
friendlyToolName,
formatTime,
inferScheduledSwitchPreset,
makeNewPoolItem,
presetValueToIntervalMs,
SCHEDULED_SWITCH_PRESET_OPTIONS,
summarizeApiPoolEndpoints,
summarizeProxyAccounts,
} from "./view-helpers.js";
const PoolManagePage = lazy(() => import("./pages/PoolManagePage.jsx"));
const ProxyPage = lazy(() => import("./pages/ProxyPage.jsx"));
const ProbePage = lazy(() => import("./pages/ProbePage.jsx"));
const ConfigSwitchPage = lazy(() => import("./pages/ConfigSwitchPage.jsx"));
const RemoteServicePage = lazy(() => import("./pages/RemoteServicePage.jsx"));
const { Header, Sider, Content } = Layout;
const { Title, Text } = Typography;
function iconForTool(toolId) {
if (toolId === "pool.manage") return <DatabaseOutlined />;
if (toolId === "api-pool.start") return <ApiOutlined />;
if (toolId === "proxy.start") return <RadarChartOutlined />;
if (toolId === "llm.probe") return <BugOutlined />;
if (toolId === "config.switch") return <SettingOutlined />;
return <AppstoreOutlined />;
}
function contentFallback() {
return (
<div className="page-loading">
<Spin size="large" />
</div>
);
}
function inferApiBase() {
if (typeof window !== "undefined" && window.location.pathname.startsWith("/admin")) {
return "/admin/api";
}
return "/api";
}
function mergeSecretPreservingDraft(poolId, previous, next) {
if (!previous) return copyPoolItem(next);
if (poolId === "codex-accounts") {
return {
...previous,
...next,
OPENAI_API_KEY:
next?.OPENAI_API_KEY || previous?.OPENAI_API_KEY || "",
OPENAI_API_KEY_MASKED:
previous?.OPENAI_API_KEY_MASKED || next?.OPENAI_API_KEY_MASKED || "",
tokens: {
...(previous?.tokens || {}),
...(next?.tokens || {}),
access_token:
next?.tokens?.access_token || previous?.tokens?.access_token || "",
id_token:
next?.tokens?.id_token || previous?.tokens?.id_token || "",
refresh_token:
next?.tokens?.refresh_token || previous?.tokens?.refresh_token || "",
access_token_masked:
previous?.tokens?.access_token_masked || next?.tokens?.access_token_masked || "",
id_token_masked:
previous?.tokens?.id_token_masked || next?.tokens?.id_token_masked || "",
refresh_token_masked:
previous?.tokens?.refresh_token_masked || next?.tokens?.refresh_token_masked || "",
},
};
}
return {
...previous,
...next,
apiKey: next?.apiKey || previous?.apiKey || "",
apiKeyMasked: previous?.apiKeyMasked || next?.apiKeyMasked || "",
};
}
export default function App() {
const defaultApiBase = inferApiBase();
const [messageApi, contextHolder] = message.useMessage();
const [tools, setTools] = useState([]);
const [activeTab, setActiveTab] = useState("pool.manage");
const [appConfig, setAppConfig] = useState({
mode: "local",
apiBase: defaultApiBase,
environment: "Local Node + React",
user: null,
readOnly: false,
readOnlyReason: "",
});
const [forms, setForms] = useState({});
const [history, setHistory] = useState([]);
const [runState, setRunState] = useState({});
const [proxyState, setProxyState] = useState({ running: false, recentLogs: [] });
const [activeApiPoolSubTab, setActiveApiPoolSubTab] = useState("codex");
const [apiPoolStateCodex, setApiPoolStateCodex] = useState({ running: false, recentLogs: [] });
const [apiPoolStateClaude, setApiPoolStateClaude] = useState({ running: false, recentLogs: [] });
const [apiPoolRemoteConfig, setApiPoolRemoteConfig] = useState({
enableScheduledSwitch: true,
scheduledSwitchIntervalMs: 900000,
scheduledSwitchPreset: "custom",
});
const [pools, setPools] = useState({});
const [activePoolCategory, setActivePoolCategory] = useState("accounts");
const [activePoolId, setActivePoolId] = useState("codex-accounts");
const [editingPool, setEditingPool] = useState(null);
const [editingDraft, setEditingDraft] = useState(null);
const [poolValidationErrors, setPoolValidationErrors] = useState([]);
const [poolSaveBusy, setPoolSaveBusy] = useState(false);
const [poolProbeModal, setPoolProbeModal] = useState({
open: false,
title: "",
runId: null,
status: "",
logs: [],
error: "",
});
const [poolImportModal, setPoolImportModal] = useState({
open: false,
poolId: "",
text: "",
busy: false,
error: "",
});
const [busy, setBusy] = useState({});
const [errors, setErrors] = useState({});
const [navCollapsed, setNavCollapsed] = useState(true);
const apiBase = appConfig.apiBase || defaultApiBase;
const isRemoteMode = appConfig.mode === "remote";
function apiPath(relativePath) {
return `${apiBase}${relativePath}`;
}
async function loadPool(poolId) {
const response = await fetch(apiPath(`/pools/${poolId}`));
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "加载池失败");
setPools((current) => ({ ...current, [poolId]: payload }));
return payload;
}
useEffect(() => {
let cancelled = false;
async function loadInitialData() {
const baseRequests = [
fetch(`${defaultApiBase}/app-config`),
fetch(`${defaultApiBase}/tools`),
fetch(`${defaultApiBase}/history`),
fetch(`${defaultApiBase}/proxy/status`),
fetch(`${defaultApiBase}/api-pool/codex/status`),
fetch(`${defaultApiBase}/api-pool/claude-code/status`),
fetch(`${defaultApiBase}/pools`),
];
const remoteConfigRequest = defaultApiBase.startsWith("/admin")
? fetch(`${defaultApiBase}/api-pool/config`)
: Promise.resolve(null);
const [configRes, toolsRes, historyRes, proxyRes, apiPoolCodexRes, apiPoolClaudeRes, poolsRes, apiPoolConfigRes] =
await Promise.all([
...baseRequests,
remoteConfigRequest,
]);
const configData = await configRes.json();
const toolsData = await toolsRes.json();
const historyData = await historyRes.json();
const proxyData = await proxyRes.json();
const apiPoolCodexData = await apiPoolCodexRes.json();
const apiPoolClaudeData = await apiPoolClaudeRes.json();
const poolsData = await poolsRes.json();
const apiPoolConfigData = apiPoolConfigRes ? await apiPoolConfigRes.json() : null;
const poolDetails = await Promise.all(
(poolsData.items || []).map((item) =>
fetch(`${defaultApiBase}/pools/${item.id}`).then((res) => res.json()),
),
);
if (cancelled) return;
const sortedTools = [...toolsData.tools].sort(
(left, right) => TOOL_ORDER.indexOf(left.id) - TOOL_ORDER.indexOf(right.id),
);
const baseForms = collectDefaults(sortedTools);
if (baseForms["api-pool.start"]) {
baseForms["api-pool.start"] = {
...baseForms["api-pool.start"],
provider: "codex",
port: 8790,
poolDir: "api_pool/codex",
scheduledSwitchPreset: inferScheduledSwitchPreset(
baseForms["api-pool.start"].scheduledSwitchIntervalMs,
),
};
}
const nextPools = {};
for (const item of poolDetails) {
nextPools[item.pool.id] = item;
}
setAppConfig((current) => ({
...current,
...configData,
apiBase: configData.apiBase || defaultApiBase,
}));
setTools(sortedTools);
setForms(baseForms);
setHistory(historyData.items || []);
setProxyState(proxyData);
setApiPoolStateCodex(apiPoolCodexData);
setApiPoolStateClaude(apiPoolClaudeData);
if (apiPoolConfigData && !apiPoolConfigData.error) {
setApiPoolRemoteConfig({
...apiPoolConfigData,
scheduledSwitchPreset: inferScheduledSwitchPreset(
apiPoolConfigData.scheduledSwitchIntervalMs,
),
});
}
setPools(nextPools);
}
loadInitialData().catch((error) => {
if (!cancelled) setErrors((current) => ({ ...current, global: error.message }));
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
const timer = setInterval(async () => {
if (isRemoteMode) {
const [proxyRes, apiPoolCodexRes, apiPoolClaudeRes] = await Promise.all([
fetch(apiPath("/proxy/status")),
fetch(apiPath("/api-pool/codex/status")),
fetch(apiPath("/api-pool/claude-code/status")),
]);
setProxyState(await proxyRes.json());
setApiPoolStateCodex(await apiPoolCodexRes.json());
setApiPoolStateClaude(await apiPoolClaudeRes.json());
return;
}
const activeRuns = Object.entries(runState).filter(([, value]) =>
value?.runId && (value.status === "queued" || value.status === "running"),
);
for (const [toolId, item] of activeRuns) {
const [runRes, logsRes] = await Promise.all([
fetch(apiPath(`/runs/${item.runId}`)),
fetch(apiPath(`/runs/${item.runId}/logs`)),
]);
const runData = await runRes.json();
const logsData = await logsRes.json();
setRunState((current) => ({
...current,
[toolId]: {
...current[toolId],
status: runData.run.status,
exitCode: runData.run.exitCode,
error: runData.run.error,
logs: logsData.logs || [],
},
}));
if (runData.run.status === "succeeded" || runData.run.status === "failed") {
const historyRes = await fetch(apiPath("/history"));
const historyData = await historyRes.json();
setHistory(historyData.items || []);
}
}
const [proxyRes, apiPoolCodexRes, apiPoolClaudeRes] = await Promise.all([
fetch(apiPath("/proxy/status")),
fetch(apiPath("/api-pool/codex/status")),
fetch(apiPath("/api-pool/claude-code/status")),
]);
setProxyState(await proxyRes.json());
setApiPoolStateCodex(await apiPoolCodexRes.json());
setApiPoolStateClaude(await apiPoolClaudeRes.json());
if (
poolProbeModal.open &&
poolProbeModal.runId &&
(poolProbeModal.status === "queued" || poolProbeModal.status === "running")
) {
const [runRes, logsRes] = await Promise.all([
fetch(apiPath(`/runs/${poolProbeModal.runId}`)),
fetch(apiPath(`/runs/${poolProbeModal.runId}/logs`)),
]);
const runData = await runRes.json();
const logsData = await logsRes.json();
setPoolProbeModal((current) => ({
...current,
status: runData.run.status,
error: runData.run.error || "",
logs: logsData.logs || [],
}));
}
}, 1500);
return () => clearInterval(timer);
}, [apiBase, isRemoteMode, runState, poolProbeModal.open, poolProbeModal.runId, poolProbeModal.status]);
const activeTool = tools.find((tool) => tool.id === activeTab);
const activeForm = forms[activeTab] || {};
const activeRun = runState[activeTab] || { logs: [] };
const currentApiPoolState =
activeApiPoolSubTab === "claude-code" ? apiPoolStateClaude : apiPoolStateCodex;
const apiPoolEndpoints = summarizeApiPoolEndpoints(currentApiPoolState);
const activeApiPoolEndpoint = currentApiPoolState?.proxyStatus?.body?.active || null;
const apiPoolSchedule = currentApiPoolState?.proxyStatus?.body || {};
const proxyAccounts = summarizeProxyAccounts(proxyState);
const activeProxyAccount = proxyState?.proxyStatus?.body?.active || null;
const activeHistory = history
.filter((item) => {
if (activeTab === "pool.manage") return false;
if (activeTab === "api-pool.start") {
return (
item.toolId === "api-pool.start" &&
String(item.paramsSummary || "").includes(`provider=${activeApiPoolSubTab}`)
);
}
return item.toolId === activeTab;
})
.slice(0, 8);
const menuItems = useMemo(
() =>
tools.map((tool) => ({
key: tool.id,
icon: iconForTool(tool.id),
label: tool.tabTitle,
})),
[tools],
);
function updateField(toolId, fieldName, value) {
if (toolId === "api-pool.start" && fieldName === "provider") {
const nextPoolDir = value === "claude-code" ? "api_pool/claude-code" : "api_pool/codex";
setForms((current) => ({
...current,
[toolId]: {
...current[toolId],
provider: value,
poolDir: nextPoolDir,
},
}));
return;
}
setForms((current) => ({
...current,
[toolId]: {
...current[toolId],
[fieldName]: value,
},
}));
}
async function refreshHistory() {
const response = await fetch(apiPath("/history"));
const data = await response.json();
setHistory(data.items || []);
}
async function runTool(tool) {
setBusy((current) => ({ ...current, [tool.id]: true }));
setErrors((current) => ({ ...current, [tool.id]: "" }));
try {
const response = await fetch(apiPath("/runs"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
toolId: tool.id,
params: forms[tool.id] || {},
confirmed: false,
}),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "运行失败");
setRunState((current) => ({
...current,
[tool.id]: {
toolId: tool.id,
runId: payload.runId,
status: payload.status,
logs: [],
exitCode: null,
error: null,
},
}));
await refreshHistory();
} catch (error) {
setErrors((current) => ({ ...current, [tool.id]: error.message }));
} finally {
setBusy((current) => ({ ...current, [tool.id]: false }));
}
}
async function startProxy() {
setBusy((current) => ({ ...current, "proxy.start": true }));
setErrors((current) => ({ ...current, "proxy.start": "" }));
try {
const response = await fetch(apiPath("/proxy/start"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ params: forms["proxy.start"] || {} }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "启动代理失败");
setProxyState(payload.status);
await refreshHistory();
} catch (error) {
setErrors((current) => ({ ...current, "proxy.start": error.message }));
} finally {
setBusy((current) => ({ ...current, "proxy.start": false }));
}
}
async function stopProxy() {
setBusy((current) => ({ ...current, "proxy.start": true }));
try {
const response = await fetch(apiPath("/proxy/stop"), { method: "POST" });
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "停止代理失败");
setProxyState(payload.status);
await refreshHistory();
} catch (error) {
setErrors((current) => ({ ...current, "proxy.start": error.message }));
} finally {
setBusy((current) => ({ ...current, "proxy.start": false }));
}
}
async function startApiPoolProxy(provider) {
setBusy((current) => ({ ...current, "api-pool.start": true }));
setErrors((current) => ({ ...current, "api-pool.start": "" }));
try {
const params = {
...(forms["api-pool.start"] || {}),
provider,
port: provider === "claude-code" ? 8789 : 8790,
poolDir: provider === "claude-code" ? "api_pool/claude-code" : "api_pool/codex",
};
const response = await fetch(apiPath("/api-pool/start"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ params }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "启动 API 池代理失败");
if (provider === "claude-code") setApiPoolStateClaude(payload.status);
else setApiPoolStateCodex(payload.status);
await refreshHistory();
} catch (error) {
setErrors((current) => ({ ...current, "api-pool.start": error.message }));
} finally {
setBusy((current) => ({ ...current, "api-pool.start": false }));
}
}
async function stopApiPoolProxy(provider) {
setBusy((current) => ({ ...current, "api-pool.start": true }));
try {
const response = await fetch(apiPath("/api-pool/stop"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ params: { provider } }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "停止 API 池代理失败");
if (provider === "claude-code") setApiPoolStateClaude(payload.status);
else setApiPoolStateCodex(payload.status);
await refreshHistory();
} catch (error) {
setErrors((current) => ({ ...current, "api-pool.start": error.message }));
} finally {
setBusy((current) => ({ ...current, "api-pool.start": false }));
}
}
async function loadPoolAndStore(poolId) {
try {
const payload = await loadPool(poolId);
const poolLabel = payload?.pool?.label || pools[poolId]?.pool?.label || "当前池";
messageApi.success(`${poolLabel} 已重新加载`);
} catch (error) {
setErrors((current) => ({ ...current, poolManage: error.message }));
}
}
function switchApiPoolSubTab(nextTab) {
setActiveApiPoolSubTab(nextTab);
setForms((current) => ({
...current,
"api-pool.start": {
...(current["api-pool.start"] || {}),
provider: nextTab,
port: nextTab === "claude-code" ? 8789 : 8790,
poolDir: nextTab === "claude-code" ? "api_pool/claude-code" : "api_pool/codex",
},
}));
}
function openEditor(poolId, index = null) {
const poolData = pools[poolId];
const item = index == null ? makeNewPoolItem(poolId) : copyPoolItem(poolData.items[index]);
setEditingPool({ poolId, index });
setEditingDraft(item);
}
async function persistPoolItems(poolId, items, successMessage = "已保存") {
setPoolSaveBusy(true);
setPoolValidationErrors([]);
setErrors((current) => ({ ...current, poolManage: "" }));
try {
const validationRes = await fetch(apiPath(`/pools/${poolId}/validate`), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items }),
});
const validation = await validationRes.json();
if (!validationRes.ok) {
setPoolValidationErrors(validation.errors || []);
throw new Error("校验未通过,请先修正条目。");
}
const saveRes = await fetch(apiPath(`/pools/${poolId}`), {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items }),
});
const saved = await saveRes.json();
if (!saveRes.ok) throw new Error(saved.error || "保存失败");
setPools((current) => ({ ...current, [poolId]: saved }));
const poolLabel = saved?.pool?.label || pools[poolId]?.pool?.label || "当前池";
if (successMessage) {
messageApi.success(`${poolLabel}${successMessage}`);
}
return saved;
} catch (error) {
setErrors((current) => ({ ...current, poolManage: error.message }));
throw error;
} finally {
setPoolSaveBusy(false);
}
}
async function applyDraft(values) {
if (!editingPool) return;
const { poolId, index } = editingPool;
const target = pools[poolId];
const items = [...(target?.items || [])];
if (index == null) {
items.push(copyPoolItem(values));
} else {
const previous = items[index];
items[index] = mergeSecretPreservingDraft(poolId, previous, values);
}
await persistPoolItems(poolId, items, index == null ? " 已新增" : " 已更新");
setEditingPool(null);
setEditingDraft(null);
}
async function deletePoolItem(poolId, index) {
const target = pools[poolId];
const items = [...(target?.items || [])];
items.splice(index, 1);
await persistPoolItems(poolId, items, " 已更新");
}
async function probePoolItem(poolId, index) {
const item = pools[poolId]?.items?.[index];
if (!item?.baseUrl || !item?.apiKey) {
setPoolProbeModal({
open: true,
title: "LLM 探测日志",
runId: null,
status: "failed",
logs: [],
error: "该条目缺少 baseUrl 或 apiKey,无法探测。",
});
return;
}
try {
const response = await fetch(apiPath("/runs"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
toolId: "llm.probe",
params: {
baseUrl: item.baseUrl,
key: item.apiKey,
skipAnthropic: false,
skipOpenAI: false,
skipPublic: false,
},
confirmed: false,
}),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || "启动 LLM 探测失败");
}
setPoolProbeModal({
open: true,
title: `${item.name || item.baseUrl} · LLM 探测日志`,
runId: payload.runId,
status: payload.status,
logs: [],
error: "",
});
} catch (error) {
setPoolProbeModal({
open: true,
title: `${item.name || item.baseUrl} · LLM 探测日志`,
runId: null,
status: "failed",
logs: [],
error: error.message,
});
}
}
async function savePool(poolId) {
const items = pools[poolId]?.items || [];
await persistPoolItems(poolId, items, " 已保存");
}
async function updatePoolItemFromLocalAuth(poolId, index) {
setErrors((current) => ({ ...current, poolManage: "" }));
try {
const response = await fetch(apiPath(`/pools/${poolId}/${index}/update-local-token`), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "更新本地 token 失败");
setPools((current) => ({ ...current, [poolId]: payload }));
const targetAccount = payload?.items?.[index];
messageApi.success(
`已从 ~/.codex/auth.json 更新 ${targetAccount?.email || targetAccount?.tokens?.account_id || "当前条目"} 的 token`,
);
} catch (error) {
setErrors((current) => ({ ...current, poolManage: error.message }));
}
}
function openImportPool(poolId) {
setPoolImportModal({
open: true,
poolId,
text: "",
busy: false,
error: "",
});
}
async function importPool() {
if (!poolImportModal.poolId) return;
setPoolImportModal((current) => ({ ...current, busy: true, error: "" }));
try {
const parsed = JSON.parse(poolImportModal.text || "[]");
if (!Array.isArray(parsed)) {
throw new Error("导入内容必须是 JSON 数组。");
}
const response = await fetch(apiPath(`/pools/${poolImportModal.poolId}/import`), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items: parsed }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "导入失败");
setPools((current) => ({ ...current, [poolImportModal.poolId]: payload }));
setPoolImportModal({
open: false,
poolId: "",
text: "",
busy: false,
error: "",
});
} catch (error) {
setPoolImportModal((current) => ({ ...current, busy: false, error: error.message }));
}
}
async function reloadRemoteServices() {
const busyKey = activeTab === "api-pool.start" ? "api-pool.start" : "proxy.start";
setBusy((current) => ({ ...current, [busyKey]: true }));
setErrors((current) => ({ ...current, [busyKey]: "" }));
try {
const response = await fetch(apiPath("/reload"), {
method: "POST",
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "reload 失败");
if (payload.results?.["codex-account"]) {
setProxyState(payload.results["codex-account"]);
}
if (payload.results?.["codex-api"]) {
setApiPoolStateCodex(payload.results["codex-api"]);
}
if (payload.results?.["claude-api"]) {
setApiPoolStateClaude(payload.results["claude-api"]);
}
} catch (error) {
setErrors((current) => ({ ...current, [busyKey]: error.message }));
} finally {
setBusy((current) => ({ ...current, [busyKey]: false }));
}
}
async function saveRemoteApiPoolConfig() {
setBusy((current) => ({ ...current, "api-pool.start": true }));
setErrors((current) => ({ ...current, "api-pool.start": "" }));
try {
const { scheduledSwitchPreset, ...configToSave } = apiPoolRemoteConfig;
const response = await fetch(apiPath("/api-pool/config"), {
method: "PUT",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
...configToSave,
provider: activeApiPoolSubTab,
}),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "保存远端 API 池配置失败");
setApiPoolRemoteConfig((current) => ({
...current,
...(payload.config || configToSave),
scheduledSwitchPreset: inferScheduledSwitchPreset(
(payload.config || configToSave).scheduledSwitchIntervalMs,
),
}));
if (activeApiPoolSubTab === "claude-code") setApiPoolStateClaude(payload.status);
else setApiPoolStateCodex(payload.status);
} catch (error) {
setErrors((current) => ({ ...current, "api-pool.start": error.message }));
} finally {
setBusy((current) => ({ ...current, "api-pool.start": false }));
}
}
const activePoolError = errors.poolManage || "";
const poolTool = activeTool;
const proxySummaryItems = [
{ title: "运行状态", value: proxyState.running ? "运行中" : "未运行" },
{ title: "监听地址", value: proxyState.endpoint || "-" },
{ title: "账号总数", value: proxyAccounts.total },
{ title: "健康账号", value: proxyAccounts.healthy },
];
const apiSummaryItems = [
{ title: "运行状态", value: currentApiPoolState.running ? "运行中" : "未运行" },
{ title: "监听地址", value: currentApiPoolState.endpoint || "-" },
{ title: "节点总数", value: apiPoolEndpoints.total },
{ title: "健康节点", value: apiPoolEndpoints.healthy },
{ title: "在途请求", value: apiPoolSchedule.inflightRequests ?? 0 },
{
title: "定时切换",
value: apiPoolSchedule.scheduledSwitchEnabled === false ? "关闭" : "开启",
},
{
title: "下次切换",
value: formatTime(apiPoolSchedule.nextScheduledSwitchAt),
plain: true,
singleLine: true,
},
{
title: "最近切换",
value: formatTime(apiPoolSchedule.lastScheduledSwitchAt),
plain: true,
singleLine: true,
},
];
return (
<Layout className="dashboard-layout">
{contextHolder}
<Sider
width={280}
collapsedWidth={88}
collapsible
trigger={null}
collapsed={navCollapsed}
className="dashboard-sider"
>
<div className="brand-panel">
<div className="brand-mark">AI</div>
<div className={navCollapsed ? "brand-copy brand-copy-hidden" : "brand-copy"}>
<Title level={3} style={{ margin: 0 }}>AI 控制台</Title>
<Text type="secondary">Local Ops Console</Text>
</div>
</div>
<Menu
mode="inline"
selectedKeys={[activeTab]}
items={menuItems}
onClick={({ key }) => setActiveTab(key)}
className="dashboard-menu"
/>
<div className="sider-footer">
<Tooltip title={navCollapsed ? "展开导航" : "收起导航"} placement="right">
<Button
type="text"
className={navCollapsed ? "nav-toggle nav-toggle-sider nav-toggle-collapsed" : "nav-toggle nav-toggle-sider"}
icon={navCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setNavCollapsed((current) => !current)}
>
{navCollapsed ? null : "收起导航"}
</Button>
</Tooltip>
</div>
</Sider>
<Layout>
<Header className="dashboard-header">
<div className="dashboard-header-main">
<div className="header-title-center">
<Title level={3} style={{ margin: 10 }}>{friendlyToolName(activeTab)}</Title>
<Text type="secondary">
{isRemoteMode ? "统一管理远端加密池和常驻代理" : "统一管理池文件、代理和探测"}
</Text>
</div>
</div>
<Space size={16}>
<Tag color="cyan">{appConfig.environment || "Local Node + React"}</Tag>
{isRemoteMode ? (
<Tag color="gold">管理员 {appConfig.user?.displayName || appConfig.user?.username || "-"}</Tag>
) : (
<Tag color="geekblue">最近 {history.length} 条记录</Tag>
)}
</Space>
</Header>
<Content className="dashboard-content">
{errors.global ? <Alert type="error" showIcon message={errors.global} style={{ marginBottom: 20 }} /> : null}
{!poolTool ? (
contentFallback()
) : (
<Suspense fallback={contentFallback()}>
{activeTab === "pool.manage" ? (
<PoolManagePage
pools={pools}
activePoolCategory={activePoolCategory}
activePoolId={activePoolId}
setActivePoolCategory={(nextCategory) => {
setActivePoolCategory(nextCategory);
setActivePoolId(nextCategory === "accounts" ? "codex-accounts" : "codex-api");
}}
setActivePoolId={setActivePoolId}
onReload={loadPoolAndStore}
onAddItem={openEditor}
onEditItem={openEditor}
onDeleteItem={deletePoolItem}
onProbeItem={probePoolItem}
onUpdateLocalToken={updatePoolItemFromLocalAuth}
onImportPool={isRemoteMode ? openImportPool : null}
poolError={activePoolError}
validationErrors={poolValidationErrors}
readOnly={Boolean(appConfig.readOnly)}
readOnlyReason={appConfig.readOnlyReason}
remoteMode={isRemoteMode}
importBusy={poolImportModal.busy}
allowProbe={!isRemoteMode}
/>
) : isRemoteMode && activeTab === "proxy.start" ? (
<RemoteServicePage
title={poolTool.tabTitle}
description={poolTool.description}
onReload={reloadRemoteServices}
reloadBusy={busy["proxy.start"]}
error={errors["proxy.start"] || proxyState.lastError}
summaryItems={proxySummaryItems}
activeInfo={{
"公开路径": proxyState.endpoint || "/proxy/codex-account",
"鉴权 Key": proxyState.authEnvName || "CODEX_ACCOUNT_PROXY_KEY",
"当前账号": activeProxyAccount?.email || activeProxyAccount?.id || "-",
"Account ID": activeProxyAccount?.accountId || "-",
"最近验证时间": formatTime(activeProxyAccount?.lastValidation),
"最近失败原因": activeProxyAccount?.lastFailureReason || "-",
}}
logs={proxyState.recentLogs || []}
note="客户端 base URL 应指向 /proxy/codex-account,对应 Bearer Key 单独配置。"
/>
) : activeTab === "proxy.start" ? (
<ProxyPage
tool={poolTool}
formValues={activeForm}
onFieldChange={(field, value) => updateField(poolTool.id, field, value)}
onStart={startProxy}
onStop={stopProxy}
runningState={proxyState}
summaryItems={proxySummaryItems}
activeInfo={{
"账号文件": activeProxyAccount?.id || "-",
"Account ID": activeProxyAccount?.accountId || "-",
"最近验证时间": formatTime(activeProxyAccount?.lastValidation),
"最近失败原因": activeProxyAccount?.lastFailureReason || "-",
"池文件": pools["codex-accounts"]?.pool?.filePath || "-",
}}
actionBusy={busy["proxy.start"]}
error={errors["proxy.start"]}
logs={proxyState.recentLogs || []}
historyItems={activeHistory}
/>
) : isRemoteMode && activeTab === "api-pool.start" ? (
<RemoteServicePage
title={poolTool.tabTitle}
description={poolTool.description}
onReload={reloadRemoteServices}
reloadBusy={busy["api-pool.start"]}
error={errors["api-pool.start"] || currentApiPoolState.lastError}
summaryItems={apiSummaryItems}
activeInfo={{
"公开路径": currentApiPoolState.endpoint || (activeApiPoolSubTab === "claude-code" ? "/proxy/claude-api" : "/proxy/codex-api"),
"鉴权 Key": currentApiPoolState.authEnvName || (activeApiPoolSubTab === "claude-code" ? "CLAUDE_API_PROXY_KEY" : "CODEX_API_PROXY_KEY"),
"当前池": activeApiPoolSubTab === "claude-code" ? "Claude Code API " : "Codex API ",
"节点名": activeApiPoolEndpoint?.name || "-",
"Base URL": activeApiPoolEndpoint?.baseUrl || "-",
"定时切换间隔": apiPoolSchedule.scheduledSwitchIntervalMs ? `${apiPoolSchedule.scheduledSwitchIntervalMs} ms` : "-",
"最近跳过原因": apiPoolSchedule.lastScheduledSwitchReason || "-",
"最近验证时间": formatTime(activeApiPoolEndpoint?.lastValidation),
"最近失败原因": activeApiPoolEndpoint?.lastFailureReason || "-",
}}
subHeader={
<Tabs
activeKey={activeApiPoolSubTab}
items={API_POOL_SUBTABS.map((item) => ({ key: item.id, label: item.label }))}
onChange={switchApiPoolSubTab}
/>
}
logs={currentApiPoolState.recentLogs || []}
operations={
<Form
layout="vertical"
style={{ maxWidth: 420 }}
>
<Form.Item label="启用定时切换" style={{ marginBottom: 12 }}>
<Switch
checked={apiPoolRemoteConfig.enableScheduledSwitch !== false}
onChange={(checked) =>
setApiPoolRemoteConfig((current) => ({
...current,
enableScheduledSwitch: checked,
}))
}
/>
</Form.Item>
<Form.Item label="定时切换间隔" style={{ marginBottom: 12 }}>
<Space direction="vertical" size={12} style={{ width: "100%" }}>
<Select
value={
apiPoolRemoteConfig.scheduledSwitchPreset ||
inferScheduledSwitchPreset(apiPoolRemoteConfig.scheduledSwitchIntervalMs)
}
options={SCHEDULED_SWITCH_PRESET_OPTIONS.map((option) => ({
label: option.label,
value: option.value,
}))}
onChange={(value) =>
setApiPoolRemoteConfig((current) => ({
...current,
scheduledSwitchPreset: value,
scheduledSwitchIntervalMs:
value === "custom"
? Number(current.scheduledSwitchIntervalMs || 900000)
: presetValueToIntervalMs(value, current.scheduledSwitchIntervalMs),
}))
}
/>
{(apiPoolRemoteConfig.scheduledSwitchPreset ||
inferScheduledSwitchPreset(apiPoolRemoteConfig.scheduledSwitchIntervalMs)) === "custom" ? (
<InputNumber
min={1000}
step={1000}
style={{ width: "100%" }}
addonAfter="毫秒"
value={apiPoolRemoteConfig.scheduledSwitchIntervalMs}
onChange={(value) =>
setApiPoolRemoteConfig((current) => ({
...current,
scheduledSwitchPreset: "custom",
scheduledSwitchIntervalMs: Number(value || 900000),
}))
}
/>
) : null}
</Space>
</Form.Item>
<Button type="primary" onClick={saveRemoteApiPoolConfig} loading={busy["api-pool.start"]}>
保存并重载当前服务
</Button>
</Form>
}
note={
activeApiPoolSubTab === "claude-code"
? "Claude 客户端请指向 /proxy/claude-api,并使用 CLAUDE_API_PROXY_KEY。"
: "OpenAI/Codex 客户端请指向 /proxy/codex-api,并使用 CODEX_API_PROXY_KEY。"
}
/>
) : activeTab === "api-pool.start" ? (
<ProxyPage
tool={poolTool}
formValues={activeForm}
onFieldChange={(field, value) => updateField(poolTool.id, field, value)}
onStart={() => startApiPoolProxy(activeApiPoolSubTab)}
onStop={() => stopApiPoolProxy(activeApiPoolSubTab)}
runningState={currentApiPoolState}
summaryItems={apiSummaryItems}
activeInfo={{
"当前池": activeApiPoolSubTab === "claude-code" ? "Claude Code API 池" : "Codex API 池",
"节点名": activeApiPoolEndpoint?.name || "-",
"Base URL": activeApiPoolEndpoint?.baseUrl || "-",
"定时切换间隔": apiPoolSchedule.scheduledSwitchIntervalMs ? `${apiPoolSchedule.scheduledSwitchIntervalMs} ms` : "-",
"最近跳过原因": apiPoolSchedule.lastScheduledSwitchReason || "-",
"最近验证时间": formatTime(activeApiPoolEndpoint?.lastValidation),
"最近失败原因": activeApiPoolEndpoint?.lastFailureReason || "-",
}}
actionBusy={busy["api-pool.start"]}
error={errors["api-pool.start"]}
subHeader={
<Tabs
activeKey={activeApiPoolSubTab}
items={API_POOL_SUBTABS.map((item) => ({ key: item.id, label: item.label }))}
onChange={switchApiPoolSubTab}
/>
}
extraControls={
<Segmented
value={activeApiPoolSubTab}
options={API_POOL_SUBTABS.map((item) => ({ label: item.label, value: item.id }))}
onChange={(value) => switchApiPoolSubTab(String(value))}
/>
}
logs={currentApiPoolState.recentLogs || []}
historyItems={activeHistory}
/>
) : activeTab === "config.switch" ? (
<ConfigSwitchPage apiBase={apiBase} />
) : (
<ProbePage
tool={poolTool}
formValues={activeForm}
onFieldChange={(field, value) => updateField(poolTool.id, field, value)}
onRun={() => runTool(poolTool)}
busy={busy[poolTool.id]}
error={errors[poolTool.id]}
logs={activeRun.logs || []}
historyItems={activeHistory}
activeRun={activeRun}
/>
)}
</Suspense>
)}
</Content>
</Layout>
<PoolEditorDrawer
poolId={editingPool?.poolId}
item={editingDraft}
visible={Boolean(editingDraft)}
remoteMode={isRemoteMode}
onClose={() => {
setEditingPool(null);
setEditingDraft(null);
}}
onSave={applyDraft}
/>
<ProbeLogModal
open={poolProbeModal.open}
title={poolProbeModal.title}
status={poolProbeModal.status}
logs={poolProbeModal.logs}
error={poolProbeModal.error}
onClose={() =>
setPoolProbeModal({
open: false,
title: "",
runId: null,
status: "",
logs: [],
error: "",
})
}
/>
<PoolImportModal
open={poolImportModal.open}
poolLabel={pools[poolImportModal.poolId]?.pool?.label}
importText={poolImportModal.text}
onChange={(text) => setPoolImportModal((current) => ({ ...current, text, error: "" }))}
onClose={() =>
setPoolImportModal({
open: false,
poolId: "",
text: "",
busy: false,
error: "",
})
}
onImport={importPool}
busy={poolImportModal.busy}
error={poolImportModal.error}
/>
</Layout>
);
}