diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index 1435df7..572111a 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -1,28 +1,28 @@ import "./styles.css"; import { - render, - useCallback, - useEffect, - useMemo, - useState, + render, + useCallback, + useEffect, + useMemo, + useState, } from "hono/jsx/dom"; import { createApiFetch } from "./core/api"; import { - initialChannelForm, - initialData, - initialSettingsForm, - tabs, + initialChannelForm, + initialData, + initialSettingsForm, + tabs, } from "./core/constants"; import type { - AdminData, - Channel, - ChannelForm, - DashboardData, - Settings, - SettingsForm, - TabId, - Token, - UsageLog, + AdminData, + Channel, + ChannelForm, + DashboardData, + Settings, + SettingsForm, + TabId, + Token, + UsageLog, } from "./core/types"; import { toggleStatus } from "./core/utils"; import { AppLayout } from "./features/AppLayout"; @@ -37,7 +37,7 @@ import { UsageView } from "./features/UsageView"; const root = document.querySelector("#app"); if (!root) { - throw new Error("Missing #app root"); + throw new Error("Missing #app root"); } /** @@ -47,582 +47,574 @@ if (!root) { * Root application JSX element. */ const App = () => { - const [token, setToken] = useState(() => - localStorage.getItem("admin_token"), - ); - const [activeTab, setActiveTab] = useState("dashboard"); - const [loading, setLoading] = useState(false); - const [notice, setNotice] = useState(""); - const [data, setData] = useState(initialData); - const [settingsForm, setSettingsForm] = - useState(initialSettingsForm); - const [channelPage, setChannelPage] = useState(1); - const [channelPageSize, setChannelPageSize] = useState(10); - const [tokenPage, setTokenPage] = useState(1); - const [tokenPageSize, setTokenPageSize] = useState(10); - const [editingChannel, setEditingChannel] = useState(null); - const [channelForm, setChannelForm] = useState(() => ({ - ...initialChannelForm, - })); - const [isChannelModalOpen, setChannelModalOpen] = useState(false); - const [isTokenModalOpen, setTokenModalOpen] = useState(false); + const [token, setToken] = useState(() => + localStorage.getItem("admin_token"), + ); + const [activeTab, setActiveTab] = useState("dashboard"); + const [loading, setLoading] = useState(false); + const [notice, setNotice] = useState(""); + const [data, setData] = useState(initialData); + const [settingsForm, setSettingsForm] = + useState(initialSettingsForm); + const [channelPage, setChannelPage] = useState(1); + const [channelPageSize, setChannelPageSize] = useState(10); + const [tokenPage, setTokenPage] = useState(1); + const [tokenPageSize, setTokenPageSize] = useState(10); + const [editingChannel, setEditingChannel] = useState(null); + const [channelForm, setChannelForm] = useState(() => ({ + ...initialChannelForm, + })); + const [isChannelModalOpen, setChannelModalOpen] = useState(false); + const [isTokenModalOpen, setTokenModalOpen] = useState(false); - const updateToken = useCallback((next: string | null) => { - setToken(next); - if (next) { - localStorage.setItem("admin_token", next); - } else { - localStorage.removeItem("admin_token"); - } - }, []); + const updateToken = useCallback((next: string | null) => { + setToken(next); + if (next) { + localStorage.setItem("admin_token", next); + } else { + localStorage.removeItem("admin_token"); + } + }, []); - const apiFetch = useMemo( - () => createApiFetch(token, () => updateToken(null)), - [token, updateToken], - ); + const apiFetch = useMemo( + () => createApiFetch(token, () => updateToken(null)), + [token, updateToken], + ); - const loadDashboard = useCallback(async () => { - const dashboard = await apiFetch("/api/dashboard"); - setData((prev) => ({ ...prev, dashboard })); - }, [apiFetch]); + const loadDashboard = useCallback(async () => { + const dashboard = await apiFetch("/api/dashboard"); + setData((prev) => ({ ...prev, dashboard })); + }, [apiFetch]); - const loadChannels = useCallback(async () => { - const result = await apiFetch<{ channels: Channel[] }>("/api/channels"); - setData((prev) => ({ ...prev, channels: result.channels })); - }, [apiFetch]); + const loadChannels = useCallback(async () => { + const result = await apiFetch<{ channels: Channel[] }>("/api/channels"); + setData((prev) => ({ ...prev, channels: result.channels })); + }, [apiFetch]); - const loadModels = useCallback(async () => { - const result = await apiFetch<{ - models: Array<{ - id: string; - channels: Array<{ id: string; name: string }>; - }>; - }>("/api/models"); - setData((prev) => ({ ...prev, models: result.models })); - }, [apiFetch]); + const loadModels = useCallback(async () => { + const result = await apiFetch<{ + models: Array<{ + id: string; + channels: Array<{ id: string; name: string }>; + }>; + }>("/api/models"); + setData((prev) => ({ ...prev, models: result.models })); + }, [apiFetch]); - const loadTokens = useCallback(async () => { - const result = await apiFetch<{ tokens: Token[] }>("/api/tokens"); - setData((prev) => ({ ...prev, tokens: result.tokens })); - }, [apiFetch]); + const loadTokens = useCallback(async () => { + const result = await apiFetch<{ tokens: Token[] }>("/api/tokens"); + setData((prev) => ({ ...prev, tokens: result.tokens })); + }, [apiFetch]); - const loadUsage = useCallback(async () => { - const result = await apiFetch<{ logs: UsageLog[] }>("/api/usage?limit=200"); - setData((prev) => ({ ...prev, usage: result.logs })); - }, [apiFetch]); + const loadUsage = useCallback(async () => { + const result = await apiFetch<{ logs: UsageLog[] }>("/api/usage?limit=200"); + setData((prev) => ({ ...prev, usage: result.logs })); + }, [apiFetch]); - const loadSettings = useCallback(async () => { - const settings = await apiFetch("/api/settings"); - setData((prev) => ({ ...prev, settings })); - }, [apiFetch]); + const loadSettings = useCallback(async () => { + const settings = await apiFetch("/api/settings"); + setData((prev) => ({ ...prev, settings })); + }, [apiFetch]); - const loadTab = useCallback( - async (tabId: TabId) => { - setLoading(true); - setNotice(""); - try { - if (tabId === "dashboard") { - await loadDashboard(); - } - if (tabId === "channels") { - await loadChannels(); - } - if (tabId === "models") { - await loadModels(); - } - if (tabId === "tokens") { - await loadTokens(); - } - if (tabId === "usage") { - await loadUsage(); - } - if (tabId === "settings") { - await loadSettings(); - } - } catch (error) { - setNotice((error as Error).message); - } finally { - setLoading(false); - } - }, - [ - loadChannels, - loadDashboard, - loadModels, - loadSettings, - loadTokens, - loadUsage, - ], - ); + const loadTab = useCallback( + async (tabId: TabId) => { + setLoading(true); + setNotice(""); + try { + if (tabId === "dashboard") { + await loadDashboard(); + } + if (tabId === "channels") { + await loadChannels(); + } + if (tabId === "models") { + await loadModels(); + } + if (tabId === "tokens") { + await loadTokens(); + } + if (tabId === "usage") { + await loadUsage(); + } + if (tabId === "settings") { + await loadSettings(); + } + } catch (error) { + setNotice((error as Error).message); + } finally { + setLoading(false); + } + }, + [ + loadChannels, + loadDashboard, + loadModels, + loadSettings, + loadTokens, + loadUsage, + ], + ); - useEffect(() => { - if (token) { - loadTab(activeTab); - } - }, [token, activeTab, loadTab]); + useEffect(() => { + if (token) { + loadTab(activeTab); + } + }, [token, activeTab, loadTab]); - useEffect(() => { - if (!data.settings) { - return; - } - setSettingsForm({ - log_retention_days: String(data.settings.log_retention_days ?? 30), - session_ttl_hours: String(data.settings.session_ttl_hours ?? 12), - admin_password: "", - }); - }, [data.settings]); + useEffect(() => { + if (!data.settings) { + return; + } + setSettingsForm({ + log_retention_days: String(data.settings.log_retention_days ?? 30), + session_ttl_hours: String(data.settings.session_ttl_hours ?? 12), + admin_password: "", + }); + }, [data.settings]); - const handleLogin = useCallback( - async (event: Event) => { - event.preventDefault(); - const form = event.currentTarget as HTMLFormElement; - const formData = new FormData(form); - const password = String(formData.get("password") ?? ""); - try { - const result = await apiFetch<{ token: string }>("/api/auth/login", { - method: "POST", - body: JSON.stringify({ password }), - }); - updateToken(result.token); - setNotice(""); - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, updateToken], - ); + const handleLogin = useCallback( + async (event: Event) => { + event.preventDefault(); + const form = event.currentTarget as HTMLFormElement; + const formData = new FormData(form); + const password = String(formData.get("password") ?? ""); + try { + const result = await apiFetch<{ token: string }>("/api/auth/login", { + method: "POST", + body: JSON.stringify({ password }), + }); + updateToken(result.token); + setNotice(""); + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, updateToken], + ); - const handleLogout = useCallback(async () => { - await apiFetch("/api/auth/logout", { method: "POST" }).catch(() => null); - updateToken(null); - }, [apiFetch, updateToken]); + const handleLogout = useCallback(async () => { + await apiFetch("/api/auth/logout", { method: "POST" }).catch(() => null); + updateToken(null); + }, [apiFetch, updateToken]); - const handleChannelFormChange = useCallback((patch: Partial) => { - setChannelForm((prev) => ({ ...prev, ...patch })); - }, []); + const handleChannelFormChange = useCallback((patch: Partial) => { + setChannelForm((prev) => ({ ...prev, ...patch })); + }, []); - const handleSettingsFormChange = useCallback( - (patch: Partial) => { - setSettingsForm((prev) => ({ ...prev, ...patch })); - }, - [], - ); + const handleSettingsFormChange = useCallback( + (patch: Partial) => { + setSettingsForm((prev) => ({ ...prev, ...patch })); + }, + [], + ); - const handleChannelPageChange = useCallback((next: number) => { - setChannelPage(next); - }, []); + const handleChannelPageChange = useCallback((next: number) => { + setChannelPage(next); + }, []); - const handleChannelPageSizeChange = useCallback((next: number) => { - setChannelPageSize(next); - setChannelPage(1); - }, []); + const handleChannelPageSizeChange = useCallback((next: number) => { + setChannelPageSize(next); + setChannelPage(1); + }, []); - const handleTokenPageChange = useCallback((next: number) => { - setTokenPage(next); - }, []); + const handleTokenPageChange = useCallback((next: number) => { + setTokenPage(next); + }, []); - const handleTokenPageSizeChange = useCallback((next: number) => { - setTokenPageSize(next); - setTokenPage(1); - }, []); + const handleTokenPageSizeChange = useCallback((next: number) => { + setTokenPageSize(next); + setTokenPage(1); + }, []); - const handleTabChange = useCallback((tabId: TabId) => { - setActiveTab(tabId); - }, []); + const handleTabChange = useCallback((tabId: TabId) => { + setActiveTab(tabId); + }, []); - const closeChannelModal = useCallback(() => { - setEditingChannel(null); - setChannelForm({ ...initialChannelForm }); - setChannelModalOpen(false); - }, []); + const closeChannelModal = useCallback(() => { + setEditingChannel(null); + setChannelForm({ ...initialChannelForm }); + setChannelModalOpen(false); + }, []); - const openChannelCreate = useCallback(() => { - setEditingChannel(null); - setChannelForm({ ...initialChannelForm }); - setChannelModalOpen(true); - setNotice(""); - }, []); + const openChannelCreate = useCallback(() => { + setEditingChannel(null); + setChannelForm({ ...initialChannelForm }); + setChannelModalOpen(true); + setNotice(""); + }, []); - const openTokenCreate = useCallback(() => { - setTokenModalOpen(true); - setNotice(""); - }, []); + const openTokenCreate = useCallback(() => { + setTokenModalOpen(true); + setNotice(""); + }, []); - const startChannelEdit = useCallback((channel: Channel) => { - setEditingChannel(channel); - setChannelForm({ - name: channel.name ?? "", - base_url: channel.base_url ?? "", - api_key: channel.api_key ?? "", - weight: channel.weight ?? 1, - }); - setChannelModalOpen(true); - setNotice(""); - }, []); + const startChannelEdit = useCallback((channel: Channel) => { + setEditingChannel(channel); + setChannelForm({ + name: channel.name ?? "", + base_url: channel.base_url ?? "", + api_key: channel.api_key ?? "", + weight: channel.weight ?? 1, + }); + setChannelModalOpen(true); + setNotice(""); + }, []); - const closeTokenModal = useCallback(() => { - setTokenModalOpen(false); - }, []); + const closeTokenModal = useCallback(() => { + setTokenModalOpen(false); + }, []); - const handleChannelSubmit = useCallback( - async (event: Event) => { - event.preventDefault(); - const channelName = channelForm.name.trim(); - const normalizedName = channelName.toLowerCase(); - const nameExists = data.channels.some( - (channel) => - channel.name.trim().toLowerCase() === normalizedName && - channel.id !== editingChannel?.id, - ); - if (nameExists) { - setNotice("渠道名称已存在,请使用其他名称"); - return; - } - try { - const body = { - name: channelName, - base_url: channelForm.base_url.trim(), - api_key: channelForm.api_key.trim(), - weight: Number(channelForm.weight), - }; - if (editingChannel) { - await apiFetch(`/api/channels/${editingChannel.id}`, { - method: "PATCH", - body: JSON.stringify(body), - }); - setNotice("渠道已更新"); - } else { - await apiFetch("/api/channels", { - method: "POST", - body: JSON.stringify(body), - }); - setNotice("渠道已创建"); - } - closeChannelModal(); - await loadChannels(); - } catch (error) { - setNotice((error as Error).message); - } - }, - [ - apiFetch, - channelForm, - closeChannelModal, - data.channels, - editingChannel, - loadChannels, - ], - ); + const handleChannelSubmit = useCallback( + async (event: Event) => { + event.preventDefault(); + const channelName = channelForm.name.trim(); + const normalizedName = channelName.toLowerCase(); + const nameExists = data.channels.some( + (channel) => + channel.name.trim().toLowerCase() === normalizedName && + channel.id !== editingChannel?.id, + ); + if (nameExists) { + setNotice("渠道名称已存在,请使用其他名称"); + return; + } + try { + const body = { + name: channelName, + base_url: channelForm.base_url.trim(), + api_key: channelForm.api_key.trim(), + weight: Number(channelForm.weight), + }; + if (editingChannel) { + await apiFetch(`/api/channels/${editingChannel.id}`, { + method: "PATCH", + body: JSON.stringify(body), + }); + setNotice("渠道已更新"); + } else { + await apiFetch("/api/channels", { + method: "POST", + body: JSON.stringify(body), + }); + setNotice("渠道已创建"); + } + closeChannelModal(); + await loadChannels(); + } catch (error) { + setNotice((error as Error).message); + } + }, + [ + apiFetch, + channelForm, + closeChannelModal, + data.channels, + editingChannel, + loadChannels, + ], + ); - const handleTokenSubmit = useCallback( - async (event: Event) => { - event.preventDefault(); - const form = event.currentTarget as HTMLFormElement; - const formData = new FormData(form); - const payload = Object.fromEntries(formData.entries()) as Record< - string, - FormDataEntryValue - >; - try { - const result = await apiFetch<{ token: string }>("/api/tokens", { - method: "POST", - body: JSON.stringify({ - name: payload.name, - quota_total: payload.quota_total - ? Number(payload.quota_total) - : null, - }), - }); - setNotice(`新令牌: ${result.token}`); - form.reset(); - setTokenModalOpen(false); - setTokenPage(1); - await loadTokens(); - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, loadTokens], - ); + const handleTokenSubmit = useCallback( + async (event: Event) => { + event.preventDefault(); + const form = event.currentTarget as HTMLFormElement; + const formData = new FormData(form); + const payload = Object.fromEntries(formData.entries()) as Record< + string, + FormDataEntryValue + >; + try { + const result = await apiFetch<{ token: string }>("/api/tokens", { + method: "POST", + body: JSON.stringify({ + name: payload.name, + quota_total: payload.quota_total + ? Number(payload.quota_total) + : null, + }), + }); + setNotice(`新令牌: ${result.token}`); + form.reset(); + setTokenModalOpen(false); + setTokenPage(1); + await loadTokens(); + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, loadTokens], + ); - const handleSettingsSubmit = useCallback( - async (event: Event) => { - event.preventDefault(); - const retention = Number(settingsForm.log_retention_days); - const sessionTtlHours = Number(settingsForm.session_ttl_hours); - const payload: Record = { - log_retention_days: retention, - session_ttl_hours: sessionTtlHours, - }; - const password = settingsForm.admin_password.trim(); - if (password) { - payload.admin_password = password; - } - try { - await apiFetch("/api/settings", { - method: "PUT", - body: JSON.stringify(payload), - }); - await loadSettings(); - setSettingsForm((prev) => ({ ...prev, admin_password: "" })); - setNotice("设置已更新"); - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, loadSettings, settingsForm], - ); + const handleSettingsSubmit = useCallback( + async (event: Event) => { + event.preventDefault(); + const retention = Number(settingsForm.log_retention_days); + const sessionTtlHours = Number(settingsForm.session_ttl_hours); + const payload: Record = { + log_retention_days: retention, + session_ttl_hours: sessionTtlHours, + }; + const password = settingsForm.admin_password.trim(); + if (password) { + payload.admin_password = password; + } + try { + await apiFetch("/api/settings", { + method: "PUT", + body: JSON.stringify(payload), + }); + await loadSettings(); + setSettingsForm((prev) => ({ ...prev, admin_password: "" })); + setNotice("设置已更新"); + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, loadSettings, settingsForm], + ); - const handleChannelTest = useCallback( - async (id: string) => { - try { - const result = await apiFetch<{ models: Array<{ id: string }> }>( - `/api/channels/${id}/test`, - { - method: "POST", - }, - ); - await loadChannels(); - setNotice(`连通测试完成,模型数 ${result.models?.length ?? 0}`); - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, loadChannels], - ); + const handleChannelTest = useCallback( + async (id: string) => { + try { + const result = await apiFetch<{ models: Array<{ id: string }> }>( + `/api/channels/${id}/test`, + { + method: "POST", + }, + ); + await loadChannels(); + setNotice(`连通测试完成,模型数 ${result.models?.length ?? 0}`); + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, loadChannels], + ); - const handleChannelDelete = useCallback( - async (id: string) => { - try { - await apiFetch(`/api/channels/${id}`, { method: "DELETE" }); - await loadChannels(); - setNotice("渠道已删除"); - if (editingChannel?.id === id) { - closeChannelModal(); - } - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, closeChannelModal, editingChannel, loadChannels], - ); + const handleChannelDelete = useCallback( + async (id: string) => { + try { + await apiFetch(`/api/channels/${id}`, { method: "DELETE" }); + await loadChannels(); + setNotice("渠道已删除"); + if (editingChannel?.id === id) { + closeChannelModal(); + } + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, closeChannelModal, editingChannel, loadChannels], + ); - const handleChannelToggle = useCallback( - async (id: string, status: string) => { - try { - const next = toggleStatus(status); - await apiFetch(`/api/channels/${id}`, { - method: "PATCH", - body: JSON.stringify({ status: next }), - }); - await loadChannels(); - setNotice(`渠道已${next === "active" ? "启用" : "停用"}`); - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, loadChannels], - ); + const handleChannelToggle = useCallback( + async (id: string, status: string) => { + try { + const next = toggleStatus(status); + await apiFetch(`/api/channels/${id}`, { + method: "PATCH", + body: JSON.stringify({ status: next }), + }); + await loadChannels(); + setNotice(`渠道已${next === "active" ? "启用" : "停用"}`); + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, loadChannels], + ); - const handleTokenDelete = useCallback( - async (id: string) => { - try { - await apiFetch(`/api/tokens/${id}`, { method: "DELETE" }); - await loadTokens(); - setNotice("令牌已删除"); - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, loadTokens], - ); + const handleTokenDelete = useCallback( + async (id: string) => { + try { + await apiFetch(`/api/tokens/${id}`, { method: "DELETE" }); + await loadTokens(); + setNotice("令牌已删除"); + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, loadTokens], + ); - const handleTokenReveal = useCallback( - async (id: string) => { - try { - const result = await apiFetch<{ token: string | null }>( - `/api/tokens/${id}/reveal`, - ); - if (!result.token) { - setNotice("未找到令牌"); - return; - } - try { - await navigator.clipboard.writeText(result.token); - setNotice(`令牌已复制到剪贴板:${result.token}`); - } catch (clipboardError) { - setNotice(`令牌: ${result.token}`); - } - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch], - ); + const handleTokenReveal = useCallback( + async (id: string) => { + try { + const result = await apiFetch<{ token: string | null }>( + `/api/tokens/${id}/reveal`, + ); + if (!result.token) { + setNotice("未找到令牌"); + return; + } + try { + await navigator.clipboard.writeText(result.token); + setNotice(`令牌已复制到剪贴板:${result.token}`); + } catch (clipboardError) { + setNotice(`令牌: ${result.token}`); + } + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch], + ); - const handleTokenToggle = useCallback( - async (id: string, status: string) => { - try { - const next = toggleStatus(status); - await apiFetch(`/api/tokens/${id}`, { - method: "PATCH", - body: JSON.stringify({ status: next }), - }); - await loadTokens(); - setNotice(`令牌已${next === "active" ? "启用" : "停用"}`); - } catch (error) { - setNotice((error as Error).message); - } - }, - [apiFetch, loadTokens], - ); + const handleTokenToggle = useCallback( + async (id: string, status: string) => { + try { + const next = toggleStatus(status); + await apiFetch(`/api/tokens/${id}`, { + method: "PATCH", + body: JSON.stringify({ status: next }), + }); + await loadTokens(); + setNotice(`令牌已${next === "active" ? "启用" : "停用"}`); + } catch (error) { + setNotice((error as Error).message); + } + }, + [apiFetch, loadTokens], + ); - const handleUsageRefresh = useCallback(async () => { - try { - await loadUsage(); - setNotice("日志已刷新"); - } catch (error) { - setNotice((error as Error).message); - } - }, [loadUsage]); + const handleUsageRefresh = useCallback(async () => { + try { + await loadUsage(); + setNotice("日志已刷新"); + } catch (error) { + setNotice((error as Error).message); + } + }, [loadUsage]); - const channelTotal = data.channels.length; - const channelTotalPages = useMemo( - () => Math.max(1, Math.ceil(channelTotal / channelPageSize)), - [channelTotal, channelPageSize], - ); - const pagedChannels = useMemo(() => { - const start = (channelPage - 1) * channelPageSize; - return data.channels.slice(start, start + channelPageSize); - }, [channelPage, channelPageSize, data.channels]); - const tokenTotal = data.tokens.length; - const tokenTotalPages = useMemo( - () => Math.max(1, Math.ceil(tokenTotal / tokenPageSize)), - [tokenTotal, tokenPageSize], - ); - const pagedTokens = useMemo(() => { - const start = (tokenPage - 1) * tokenPageSize; - return data.tokens.slice(start, start + tokenPageSize); - }, [data.tokens, tokenPage, tokenPageSize]); + const channelTotal = data.channels.length; + const channelTotalPages = useMemo( + () => Math.max(1, Math.ceil(channelTotal / channelPageSize)), + [channelTotal, channelPageSize], + ); + const pagedChannels = useMemo(() => { + const start = (channelPage - 1) * channelPageSize; + return data.channels.slice(start, start + channelPageSize); + }, [channelPage, channelPageSize, data.channels]); + const tokenTotal = data.tokens.length; + const tokenTotalPages = useMemo( + () => Math.max(1, Math.ceil(tokenTotal / tokenPageSize)), + [tokenTotal, tokenPageSize], + ); + const pagedTokens = useMemo(() => { + const start = (tokenPage - 1) * tokenPageSize; + return data.tokens.slice(start, start + tokenPageSize); + }, [data.tokens, tokenPage, tokenPageSize]); - useEffect(() => { - setChannelPage((prev) => Math.min(prev, channelTotalPages)); - }, [channelTotalPages]); + useEffect(() => { + setChannelPage((prev) => Math.min(prev, channelTotalPages)); + }, [channelTotalPages]); - useEffect(() => { - setTokenPage((prev) => Math.min(prev, tokenTotalPages)); - }, [tokenTotalPages]); + useEffect(() => { + setTokenPage((prev) => Math.min(prev, tokenTotalPages)); + }, [tokenTotalPages]); - const activeLabel = useMemo( - () => tabs.find((tab) => tab.id === activeTab)?.label ?? "管理台", - [activeTab], - ); + const activeLabel = useMemo( + () => tabs.find((tab) => tab.id === activeTab)?.label ?? "管理台", + [activeTab], + ); - const renderContent = () => { - if (loading) { - return ( -
- 加载中... -
- ); - } - if (activeTab === "dashboard") { - return ; - } - if (activeTab === "channels") { - return ( - - ); - } - if (activeTab === "models") { - return ; - } - if (activeTab === "tokens") { - return ( - - ); - } - if (activeTab === "usage") { - return ( - - ); - } - if (activeTab === "settings") { - return ( - - ); - } - return ( -
- 未知模块 -
- ); - }; + const renderContent = () => { + if (loading) { + return ( +
+ 加载中... +
+ ); + } + if (activeTab === "dashboard") { + return ; + } + if (activeTab === "channels") { + return ( + + ); + } + if (activeTab === "models") { + return ; + } + if (activeTab === "tokens") { + return ( + + ); + } + if (activeTab === "usage") { + return ; + } + if (activeTab === "settings") { + return ( + + ); + } + return ( +
+ 未知模块 +
+ ); + }; - return ( -
- {token ? ( - - {renderContent()} - - ) : ( - - )} -
- ); + return ( +
+ {token ? ( + + {renderContent()} + + ) : ( + + )} +
+ ); }; render(, root); diff --git a/apps/worker/src/routes/newapiChannels.ts b/apps/worker/src/routes/newapiChannels.ts index 0d774c9..079756c 100644 --- a/apps/worker/src/routes/newapiChannels.ts +++ b/apps/worker/src/routes/newapiChannels.ts @@ -11,6 +11,7 @@ import { normalizeChannelInput, normalizeModelsInput, toNewApiChannel, + withNewApiDefaults, } from "../services/newapi"; import { generateToken } from "../utils/crypto"; import { safeJsonParse } from "../utils/json"; @@ -220,7 +221,7 @@ newapi.get("/", async (c) => { const items = (rows.results ?? []).map((row) => { const { key: _key, ...rest } = toNewApiChannel(row); - return rest; + return withNewApiDefaults(rest); }); return success(c, { @@ -280,7 +281,7 @@ newapi.get("/search", async (c) => { const offset = (page - 1) * pageSize; const items = filtered.slice(offset, offset + pageSize).map((row) => { const { key: _key, ...rest } = toNewApiChannel(row); - return rest; + return withNewApiDefaults(rest); }); return success(c, { @@ -658,7 +659,7 @@ newapi.get("/:id", async (c) => { is_multi_key: false, multi_key_mode: "random", }; - const output = toNewApiChannel(channel); + const output = withNewApiDefaults(toNewApiChannel(channel)); return success(c, { ...output, model_mapping: modelMapping, diff --git a/apps/worker/src/services/newapi.ts b/apps/worker/src/services/newapi.ts index f609efc..99efcec 100644 --- a/apps/worker/src/services/newapi.ts +++ b/apps/worker/src/services/newapi.ts @@ -35,6 +35,31 @@ export type NewApiChannel = { key?: string; created_time?: number; updated_time?: number; + openai_organization?: string; + test_model?: string; + other?: string; + balance?: number; + balance_updated_time?: number; + used_quota?: number; + model_mapping?: string; + status_code_mapping?: string; + auto_ban?: number; + other_info?: string; + tag?: string; + setting?: string; + param_override?: string; + header_override?: string; + remark?: string; + channel_info?: ChannelInfo; + settings?: string; +}; + +export type ChannelInfo = { + is_multi_key: boolean; + multi_key_size: number; + multi_key_status_list: unknown[] | null; + multi_key_polling_index: number; + multi_key_mode: string; }; export type ParsedChannelInput = { @@ -55,6 +80,35 @@ export type ParsedChannelInput = { type ModelLike = { id?: unknown }; +const DEFAULT_CHANNEL_INFO: ChannelInfo = { + is_multi_key: false, + multi_key_size: 0, + multi_key_status_list: null, + multi_key_polling_index: 0, + multi_key_mode: "", +}; + +const DEFAULT_NEWAPI_FIELDS = { + key: "", + openai_organization: "", + test_model: "", + other: "", + balance: 0, + balance_updated_time: 0, + used_quota: 0, + model_mapping: "", + status_code_mapping: "", + auto_ban: 1, + other_info: "", + tag: "", + setting: "", + param_override: "", + header_override: "", + remark: "", + channel_info: DEFAULT_CHANNEL_INFO, + settings: "", +} as const; + const KNOWN_KEYS = new Set([ "id", "name", @@ -77,6 +131,30 @@ const KNOWN_KEYS = new Set([ "priority", ]); +export function withNewApiDefaults>( + channel: T, +): T & typeof DEFAULT_NEWAPI_FIELDS { + const merged: Record = { ...channel }; + for (const [key, fallback] of Object.entries(DEFAULT_NEWAPI_FIELDS)) { + if (merged[key] === undefined || merged[key] === null) { + merged[key] = fallback; + } + } + + const channelInfo = merged.channel_info; + if ( + channelInfo && + typeof channelInfo === "object" && + !Array.isArray(channelInfo) + ) { + merged.channel_info = { ...DEFAULT_CHANNEL_INFO, ...channelInfo }; + } else { + merged.channel_info = { ...DEFAULT_CHANNEL_INFO }; + } + + return merged as T & typeof DEFAULT_NEWAPI_FIELDS; +} + function toModelId(item: unknown): string { if (item && typeof item === "object" && "id" in item) { const value = (item as ModelLike).id;