refactor: 合并导航栏,减少导航项从 11 个到 7 个

合并相关功能页面:
- 智能路由 + 容错配置 → 路由管理(Tab 切换)
- 配置切换 + 配置管理 → 配置管理(Tab 切换)
- MCP + Prompts + Skills → 扩展(Tab 切换)

新增组件:
- RoutingManagementPage: 路由管理合并页面
- ConfigManagementPage: 配置管理合并页面
- ExtensionsPage: 扩展合并页面

各子页面支持 hideHeader 属性,嵌入合并页面时隐藏标题
This commit is contained in:
coso
2025-12-18 01:47:17 +08:00
parent 341839a0c2
commit 0fdf0c0f2e
15 changed files with 922 additions and 588 deletions
+14 -38
View File
@@ -1,33 +1,21 @@
import { useState } from "react";
import { Sidebar } from "./components/Sidebar";
import { Dashboard } from "./components/Dashboard";
import { Providers } from "./components/Providers";
import { SettingsPage } from "./components/settings";
import { SwitchPage } from "./components/switch";
import { ClientsPage } from "./components/clients";
import { McpPage } from "./components/mcp";
import { PromptsPage } from "./components/prompts";
import { ApiServerPage } from "./components/api-server/ApiServerPage";
import { SkillsPage } from "./components/skills";
import { ProviderPoolPage } from "./components/provider-pool";
import { RoutingPage } from "./components/routing";
import { ResiliencePage } from "./components/resilience";
import { ConfigPage } from "./components/config";
import { RoutingManagementPage } from "./components/routing/RoutingManagementPage";
import { ConfigManagementPage } from "./components/config/ConfigManagementPage";
import { ExtensionsPage } from "./components/extensions";
type Page =
| "dashboard"
| "clients"
| "api-server"
| "providers"
| "settings"
| "switch"
| "mcp"
| "prompts"
| "skills"
| "provider-pool"
| "routing"
| "resilience"
| "config";
| "routing-management"
| "config-management"
| "extensions"
| "api-server"
| "settings";
function App() {
const [currentPage, setCurrentPage] = useState<Page>("dashboard");
@@ -38,28 +26,16 @@ function App() {
return <Dashboard />;
case "provider-pool":
return <ProviderPoolPage />;
case "clients":
return <ClientsPage />;
case "routing-management":
return <RoutingManagementPage />;
case "config-management":
return <ConfigManagementPage />;
case "extensions":
return <ExtensionsPage />;
case "api-server":
return <ApiServerPage />;
case "providers":
return <Providers />;
case "settings":
return <SettingsPage />;
case "switch":
return <SwitchPage />;
case "mcp":
return <McpPage />;
case "prompts":
return <PromptsPage />;
case "skills":
return <SkillsPage />;
case "routing":
return <RoutingPage />;
case "resilience":
return <ResiliencePage />;
case "config":
return <ConfigPage />;
default:
return <Dashboard />;
}
+9 -23
View File
@@ -1,32 +1,22 @@
import {
LayoutDashboard,
Settings,
Plug,
MessageSquare,
Monitor,
Globe,
Boxes,
Database,
Route,
Shield,
FileCode,
Puzzle,
} from "lucide-react";
import { cn } from "@/lib/utils";
type Page =
| "dashboard"
| "clients"
| "api-server"
| "providers"
| "settings"
| "switch"
| "mcp"
| "prompts"
| "skills"
| "provider-pool"
| "routing"
| "resilience"
| "config";
| "routing-management"
| "config-management"
| "extensions"
| "api-server"
| "settings";
interface SidebarProps {
currentPage: Page;
@@ -36,14 +26,10 @@ interface SidebarProps {
const navItems = [
{ id: "dashboard" as Page, label: "仪表盘", icon: LayoutDashboard },
{ id: "provider-pool" as Page, label: "凭证池", icon: Database },
{ id: "routing" as Page, label: "智能路由", icon: Route },
{ id: "resilience" as Page, label: "容错配置", icon: Shield },
{ id: "config" as Page, label: "配置管理", icon: FileCode },
{ id: "clients" as Page, label: "配置切换", icon: Monitor },
{ id: "routing-management" as Page, label: "路由管理", icon: Route },
{ id: "config-management" as Page, label: "配置管理", icon: FileCode },
{ id: "extensions" as Page, label: "扩展", icon: Puzzle },
{ id: "api-server" as Page, label: "API Server", icon: Globe },
{ id: "mcp" as Page, label: "MCP", icon: Plug },
{ id: "prompts" as Page, label: "Prompts", icon: MessageSquare },
{ id: "skills" as Page, label: "Skills", icon: Boxes },
{ id: "settings" as Page, label: "设置", icon: Settings },
];
+14 -8
View File
@@ -4,18 +4,24 @@ import { AppTabs } from "./AppTabs";
import { ProviderList } from "./ProviderList";
import { HelpTip } from "@/components/HelpTip";
export function ClientsPage() {
interface ClientsPageProps {
hideHeader?: boolean;
}
export function ClientsPage({ hideHeader = false }: ClientsPageProps) {
const [activeApp, setActiveApp] = useState<AppType>("claude");
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">配置切换</h2>
<p className="text-muted-foreground">
一键切换 Claude Code / Codex / Gemini CLI 的 API 配置,快速在不同
Provider 间切换
</p>
</div>
{!hideHeader && (
<div>
<h2 className="text-2xl font-bold">配置切换</h2>
<p className="text-muted-foreground">
一键切换 Claude Code / Codex / Gemini CLI 的 API 配置,快速在不同
Provider 间切换
</p>
</div>
)}
<HelpTip title="关于 ProxyCast 本地代理" variant="blue">
<p className="text-sm text-blue-700 dark:text-blue-400">
@@ -0,0 +1,50 @@
import { useState } from "react";
import { Monitor, FileCode } from "lucide-react";
import { cn } from "@/lib/utils";
import { ClientsPage } from "../clients/ClientsPage";
import { ConfigPage } from "./ConfigPage";
type Tab = "switch" | "config";
const tabs = [
{ id: "switch" as Tab, label: "配置切换", icon: Monitor },
{ id: "config" as Tab, label: "配置文件", icon: FileCode },
];
export function ConfigManagementPage() {
const [activeTab, setActiveTab] = useState<Tab>("switch");
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">配置管理</h2>
<p className="text-muted-foreground">管理客户端配置和配置文件</p>
</div>
{/* Tab 切换 */}
<div className="flex gap-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Tab 内容 */}
<div className="pt-2">
{activeTab === "switch" && <ClientsPage hideHeader />}
{activeTab === "config" && <ConfigPage hideHeader />}
</div>
</div>
);
}
+178 -148
View File
@@ -17,160 +17,190 @@ export interface ConfigPageRef {
type TabType = "editor" | "import-export" | "settings";
export const ConfigPage = forwardRef<ConfigPageRef>((_props, ref) => {
const [activeTab, setActiveTab] = useState<TabType>("editor");
const [config, setConfig] = useState<Config | null>(null);
const [pathInfo, setPathInfo] = useState<ConfigPathInfo | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
interface ConfigPageProps {
hideHeader?: boolean;
}
const loadConfig = async () => {
setIsLoading(true);
setError(null);
try {
// Load current config from app state
const currentConfig = await invoke<Config>("get_config");
setConfig(currentConfig);
export const ConfigPage = forwardRef<ConfigPageRef, ConfigPageProps>(
({ hideHeader = false }, ref) => {
const [activeTab, setActiveTab] = useState<TabType>("editor");
const [config, setConfig] = useState<Config | null>(null);
const [pathInfo, setPathInfo] = useState<ConfigPathInfo | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Load path info
const paths = await configApi.getConfigPaths();
setPathInfo(paths);
} catch (err) {
setError(`加载配置失败: ${err}`);
} finally {
setIsLoading(false);
const loadConfig = async () => {
setIsLoading(true);
setError(null);
try {
// Load current config from app state
const currentConfig = await invoke<Config>("get_config");
setConfig(currentConfig);
// Load path info
const paths = await configApi.getConfigPaths();
setPathInfo(paths);
} catch (err) {
setError(`加载配置失败: ${err}`);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadConfig();
}, []);
useImperativeHandle(ref, () => ({
refresh: loadConfig,
}));
const handleConfigChange = async (newConfig: Config) => {
setConfig(newConfig);
// Save config to backend
try {
await invoke("save_config", { config: newConfig });
} catch (err) {
setError(`保存配置失败: ${err}`);
}
};
const handleOpenConfigFolder = async () => {
try {
await invoke("open_config_folder", { appType: "ProxyCast" });
} catch (err) {
setError(`打开文件夹失败: ${err}`);
}
};
const tabs: { id: TabType; label: string; icon?: React.ReactNode }[] = [
{ id: "editor", label: "YAML 编辑器" },
{ id: "import-export", label: "导入/导出" },
{ id: "settings", label: "设置", icon: <Settings className="h-4 w-4" /> },
];
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">加载中...</div>
</div>
);
}
};
useEffect(() => {
loadConfig();
}, []);
useImperativeHandle(ref, () => ({
refresh: loadConfig,
}));
const handleConfigChange = async (newConfig: Config) => {
setConfig(newConfig);
// Save config to backend
try {
await invoke("save_config", { config: newConfig });
} catch (err) {
setError(`保存配置失败: ${err}`);
}
};
const handleOpenConfigFolder = async () => {
try {
await invoke("open_config_folder", { appType: "ProxyCast" });
} catch (err) {
setError(`打开文件夹失败: ${err}`);
}
};
const tabs: { id: TabType; label: string; icon?: React.ReactNode }[] = [
{ id: "editor", label: "YAML 编辑器" },
{ id: "import-export", label: "导入/导出" },
{ id: "settings", label: "设置", icon: <Settings className="h-4 w-4" /> },
];
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">加载中...</div>
<div className="space-y-6">
{!hideHeader && (
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<FileCode className="h-6 w-6" />
配置管理
</h2>
<p className="text-muted-foreground">
编辑 YAML 配置文件,导入导出配置
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleOpenConfigFolder}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<FolderOpen className="h-4 w-4" />
打开配置目录
</button>
<button
onClick={loadConfig}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<RefreshCw className="h-4 w-4" />
刷新
</button>
</div>
</div>
)}
{hideHeader && (
<div className="flex items-center justify-end gap-2">
<button
onClick={handleOpenConfigFolder}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<FolderOpen className="h-4 w-4" />
打开配置目录
</button>
<button
onClick={loadConfig}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<RefreshCw className="h-4 w-4" />
刷新
</button>
</div>
)}
{/* Config path info */}
{pathInfo && (
<div className="rounded-lg border bg-muted/50 p-3 text-sm">
<div className="flex items-center gap-4">
<span className="text-muted-foreground">配置文件:</span>
<code className="rounded bg-muted px-2 py-0.5">
{pathInfo.yaml_path}
</code>
{pathInfo.yaml_exists ? (
<span className="text-green-600 text-xs">存在</span>
) : (
<span className="text-yellow-600 text-xs">不存在</span>
)}
</div>
</div>
)}
{/* Error display */}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-400">
{error}
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{/* Tab content */}
<div className="py-4">
{activeTab === "editor" && (
<ConfigEditor config={config} onConfigChange={handleConfigChange} />
)}
{activeTab === "import-export" && (
<ImportExport
config={config}
onConfigImported={handleConfigChange}
/>
)}
{activeTab === "settings" && (
<AuthDirSettings
config={config}
onConfigChange={handleConfigChange}
/>
)}
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<FileCode className="h-6 w-6" />
配置管理
</h2>
<p className="text-muted-foreground">
编辑 YAML 配置文件,导入导出配置
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleOpenConfigFolder}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<FolderOpen className="h-4 w-4" />
打开配置目录
</button>
<button
onClick={loadConfig}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<RefreshCw className="h-4 w-4" />
刷新
</button>
</div>
</div>
{/* Config path info */}
{pathInfo && (
<div className="rounded-lg border bg-muted/50 p-3 text-sm">
<div className="flex items-center gap-4">
<span className="text-muted-foreground">配置文件:</span>
<code className="rounded bg-muted px-2 py-0.5">
{pathInfo.yaml_path}
</code>
{pathInfo.yaml_exists ? (
<span className="text-green-600 text-xs">存在</span>
) : (
<span className="text-yellow-600 text-xs">不存在</span>
)}
</div>
</div>
)}
{/* Error display */}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-400">
{error}
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{/* Tab content */}
<div className="py-4">
{activeTab === "editor" && (
<ConfigEditor config={config} onConfigChange={handleConfigChange} />
)}
{activeTab === "import-export" && (
<ImportExport config={config} onConfigImported={handleConfigChange} />
)}
{activeTab === "settings" && (
<AuthDirSettings
config={config}
onConfigChange={handleConfigChange}
/>
)}
</div>
</div>
);
});
},
);
ConfigPage.displayName = "ConfigPage";
+1
View File
@@ -2,3 +2,4 @@ export { ConfigPage } from "./ConfigPage";
export { ConfigEditor } from "./ConfigEditor";
export { ImportExport } from "./ImportExport";
export { AuthDirSettings } from "./AuthDirSettings";
export { ConfigManagementPage } from "./ConfigManagementPage";
@@ -0,0 +1,55 @@
import { useState } from "react";
import { Plug, MessageSquare, Boxes } from "lucide-react";
import { cn } from "@/lib/utils";
import { McpPage } from "../mcp/McpPage";
import { PromptsPage } from "../prompts/PromptsPage";
import { SkillsPage } from "../skills/SkillsPage";
type Tab = "mcp" | "prompts" | "skills";
const tabs = [
{ id: "mcp" as Tab, label: "MCP", icon: Plug },
{ id: "prompts" as Tab, label: "Prompts", icon: MessageSquare },
{ id: "skills" as Tab, label: "Skills", icon: Boxes },
];
export function ExtensionsPage() {
const [activeTab, setActiveTab] = useState<Tab>("mcp");
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">扩展</h2>
<p className="text-muted-foreground">
管理 MCP 服务器、Prompts 和 Skills
</p>
</div>
{/* Tab 切换 */}
<div className="flex gap-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Tab 内容 */}
<div className="pt-2">
{activeTab === "mcp" && <McpPage hideHeader />}
{activeTab === "prompts" && <PromptsPage hideHeader />}
{activeTab === "skills" && <SkillsPage hideHeader />}
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { ExtensionsPage } from "./ExtensionsPage";
+71 -9
View File
@@ -59,7 +59,11 @@ const defaultServerConfig = JSON.stringify(
2,
);
export function McpPage() {
interface McpPageProps {
hideHeader?: boolean;
}
export function McpPage({ hideHeader = false }: McpPageProps) {
const {
servers,
loading,
@@ -234,14 +238,72 @@ export function McpPage() {
return (
<div className="h-full flex flex-col">
<div className="mb-4 flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold">MCP 服务器</h2>
<p className="text-muted-foreground">
管理 Model Context Protocol 服务器配置,同步到外部应用
</p>
{!hideHeader && (
<div className="mb-4 flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold">MCP 服务器</h2>
<p className="text-muted-foreground">
管理 Model Context Protocol 服务器配置,同步到外部应用
</p>
</div>
<div className="flex items-center gap-2">
{/* 从外部导入按钮 */}
<div className="relative">
<button
onClick={() => setShowImportMenu(!showImportMenu)}
disabled={importing}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border hover:bg-muted text-sm"
title="从外部应用导入 MCP 配置"
>
<Download
className={cn("h-4 w-4", importing && "animate-pulse")}
/>
{importing ? "导入中..." : "导入"}
</button>
{showImportMenu && (
<div className="absolute right-0 top-full mt-1 w-40 py-1 bg-popover border rounded-lg shadow-lg z-10">
<button
onClick={() => handleImport()}
className="w-full px-3 py-1.5 text-left text-sm hover:bg-muted"
>
全部导入
</button>
<button
onClick={() => handleImport("claude")}
className="w-full px-3 py-1.5 text-left text-sm hover:bg-muted"
>
从 Claude Code
</button>
<button
onClick={() => handleImport("codex")}
className="w-full px-3 py-1.5 text-left text-sm hover:bg-muted"
>
从 Codex
</button>
<button
onClick={() => handleImport("gemini")}
className="w-full px-3 py-1.5 text-left text-sm hover:bg-muted"
>
从 Gemini CLI
</button>
</div>
)}
</div>
{/* 同步到外部按钮 */}
<button
onClick={handleSyncToLive}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-primary text-primary-foreground text-sm"
title="同步配置到所有外部应用"
>
<Upload className="h-4 w-4" />
同步
</button>
</div>
</div>
<div className="flex items-center gap-2">
)}
{hideHeader && (
<div className="mb-4 flex items-center justify-end gap-2">
{/* 从外部导入按钮 */}
<div className="relative">
<button
@@ -294,7 +356,7 @@ export function McpPage() {
同步
</button>
</div>
</div>
)}
<HelpTip title="什么是 MCP?" variant="blue">
<ul className="list-disc list-inside space-y-1 text-sm text-blue-700 dark:text-blue-400">
+13 -7
View File
@@ -55,7 +55,11 @@ function ToggleSwitch({
);
}
export function PromptsPage() {
interface PromptsPageProps {
hideHeader?: boolean;
}
export function PromptsPage({ hideHeader = false }: PromptsPageProps) {
const [activeApp, setActiveApp] = useState<AppType>("claude");
const {
prompts,
@@ -179,12 +183,14 @@ export function PromptsPage() {
return (
<div className="h-full flex flex-col">
<div className="mb-4">
<h2 className="text-2xl font-bold">Prompts</h2>
<p className="text-muted-foreground">
管理不同应用的系统提示词({currentApp.filename})
</p>
</div>
{!hideHeader && (
<div className="mb-4">
<h2 className="text-2xl font-bold">Prompts</h2>
<p className="text-muted-foreground">
管理不同应用的系统提示词({currentApp.filename})
</p>
</div>
)}
<HelpTip title="什么是 Prompts?" variant="amber">
<ul className="list-disc list-inside space-y-1 text-sm text-amber-700 dark:text-amber-400">
+37 -16
View File
@@ -9,7 +9,14 @@ export interface ResiliencePageRef {
type TabType = "retry" | "failover";
export const ResiliencePage = forwardRef<ResiliencePageRef>((_props, ref) => {
interface ResiliencePageProps {
hideHeader?: boolean;
}
export const ResiliencePage = forwardRef<
ResiliencePageRef,
ResiliencePageProps
>(({ hideHeader = false }, ref) => {
const [activeTab, setActiveTab] = useState<TabType>("retry");
const [refreshKey, setRefreshKey] = useState(0);
@@ -28,22 +35,36 @@ export const ResiliencePage = forwardRef<ResiliencePageRef>((_props, ref) => {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Shield className="h-6 w-6" />
容错配置
</h2>
<p className="text-muted-foreground">配置重试机制和故障转移策略</p>
{!hideHeader && (
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Shield className="h-6 w-6" />
容错配置
</h2>
<p className="text-muted-foreground">配置重试机制和故障转移策略</p>
</div>
<button
onClick={refresh}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<RefreshCw className="h-4 w-4" />
刷新
</button>
</div>
<button
onClick={refresh}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<RefreshCw className="h-4 w-4" />
刷新
</button>
</div>
)}
{hideHeader && (
<div className="flex items-center justify-end">
<button
onClick={refresh}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<RefreshCw className="h-4 w-4" />
刷新
</button>
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b">
@@ -0,0 +1,68 @@
import { useState } from "react";
import { Route, Shield } from "lucide-react";
import { cn } from "@/lib/utils";
import { RoutingPage } from "./RoutingPage";
import { ResiliencePage } from "../resilience/ResiliencePage";
type Tab = "routing" | "resilience";
const tabs = [
{ id: "routing" as Tab, label: "智能路由", icon: Route },
{ id: "resilience" as Tab, label: "容错配置", icon: Shield },
];
export function RoutingManagementPage() {
const [activeTab, setActiveTab] = useState<Tab>("routing");
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">路由管理</h2>
<p className="text-muted-foreground">配置智能路由规则和容错策略</p>
</div>
{/* Tab 切换 */}
<div className="flex gap-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Tab 内容 */}
<div className="pt-2">
{activeTab === "routing" && <RoutingPageContent />}
{activeTab === "resilience" && <ResiliencePageContent />}
</div>
</div>
);
}
// 路由页面内容(去掉标题)
function RoutingPageContent() {
return (
<div className="routing-content">
<RoutingPage hideHeader />
</div>
);
}
// 容错页面内容(去掉标题)
function ResiliencePageContent() {
return (
<div className="resilience-content">
<ResiliencePage hideHeader />
</div>
);
}
+376 -330
View File
@@ -21,367 +21,413 @@ export interface RoutingPageRef {
type TabType = "aliases" | "rules" | "exclusions" | "injection";
export const RoutingPage = forwardRef<RoutingPageRef>((_props, ref) => {
const [activeTab, setActiveTab] = useState<TabType>("aliases");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
interface RoutingPageProps {
hideHeader?: boolean;
}
// Data state
const [aliases, setAliases] = useState<ModelAlias[]>([]);
const [rules, setRules] = useState<RoutingRule[]>([]);
const [exclusions, setExclusions] = useState<Record<ProviderType, string[]>>(
{} as Record<ProviderType, string[]>,
);
const [injectionRules, setInjectionRules] = useState<InjectionRule[]>([]);
const [injectionEnabled, setInjectionEnabled] = useState(false);
export const RoutingPage = forwardRef<RoutingPageRef, RoutingPageProps>(
({ hideHeader = false }, ref) => {
const [activeTab, setActiveTab] = useState<TabType>("aliases");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Presets state
const [presets, setPresets] = useState<RecommendedPreset[]>([]);
const [showPresets, setShowPresets] = useState(false);
const [applyingPreset, setApplyingPreset] = useState<string | null>(null);
// Data state
const [aliases, setAliases] = useState<ModelAlias[]>([]);
const [rules, setRules] = useState<RoutingRule[]>([]);
const [exclusions, setExclusions] = useState<
Record<ProviderType, string[]>
>({} as Record<ProviderType, string[]>);
const [injectionRules, setInjectionRules] = useState<InjectionRule[]>([]);
const [injectionEnabled, setInjectionEnabled] = useState(false);
const refresh = async () => {
setLoading(true);
setError(null);
try {
const [
aliasesData,
rulesData,
exclusionsData,
injectionConfig,
presetsData,
] = await Promise.all([
routerApi.getModelAliases(),
routerApi.getRoutingRules(),
routerApi.getExclusions(),
injectionApi.getInjectionConfig(),
routerApi.getRecommendedPresets(),
]);
setAliases(aliasesData);
setRules(rulesData);
setExclusions(exclusionsData);
setInjectionRules(injectionConfig.rules);
setInjectionEnabled(injectionConfig.enabled);
setPresets(presetsData);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
};
// Presets state
const [presets, setPresets] = useState<RecommendedPreset[]>([]);
const [showPresets, setShowPresets] = useState(false);
const [applyingPreset, setApplyingPreset] = useState<string | null>(null);
const handleApplyPreset = async (
presetId: string,
merge: boolean = false,
) => {
setApplyingPreset(presetId);
try {
await routerApi.applyRecommendedPreset(presetId, merge);
const refresh = async () => {
setLoading(true);
setError(null);
try {
const [
aliasesData,
rulesData,
exclusionsData,
injectionConfig,
presetsData,
] = await Promise.all([
routerApi.getModelAliases(),
routerApi.getRoutingRules(),
routerApi.getExclusions(),
injectionApi.getInjectionConfig(),
routerApi.getRecommendedPresets(),
]);
setAliases(aliasesData);
setRules(rulesData);
setExclusions(exclusionsData);
setInjectionRules(injectionConfig.rules);
setInjectionEnabled(injectionConfig.enabled);
setPresets(presetsData);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
};
const handleApplyPreset = async (
presetId: string,
merge: boolean = false,
) => {
setApplyingPreset(presetId);
try {
await routerApi.applyRecommendedPreset(presetId, merge);
await refresh();
setShowPresets(false);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setApplyingPreset(null);
}
};
const handleClearAll = async () => {
if (!confirm("确定要清空所有路由配置吗?此操作不可撤销。")) return;
try {
await routerApi.clearAllRoutingConfig();
await refresh();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
};
useImperativeHandle(ref, () => ({
refresh,
}));
useEffect(() => {
refresh();
}, []);
// Alias handlers
const handleAddAlias = async (alias: string, actual: string) => {
await routerApi.addModelAlias(alias, actual);
await refresh();
setShowPresets(false);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setApplyingPreset(null);
}
};
};
const handleClearAll = async () => {
if (!confirm("确定要清空所有路由配置吗?此操作不可撤销。")) return;
try {
await routerApi.clearAllRoutingConfig();
const handleRemoveAlias = async (alias: string) => {
await routerApi.removeModelAlias(alias);
await refresh();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
};
};
useImperativeHandle(ref, () => ({
refresh,
}));
// Rule handlers
const handleAddRule = async (rule: RoutingRule) => {
await routerApi.addRoutingRule(rule);
await refresh();
};
useEffect(() => {
refresh();
}, []);
const handleRemoveRule = async (pattern: string) => {
await routerApi.removeRoutingRule(pattern);
await refresh();
};
// Alias handlers
const handleAddAlias = async (alias: string, actual: string) => {
await routerApi.addModelAlias(alias, actual);
await refresh();
};
const handleUpdateRule = async (pattern: string, rule: RoutingRule) => {
await routerApi.updateRoutingRule(pattern, rule);
await refresh();
};
const handleRemoveAlias = async (alias: string) => {
await routerApi.removeModelAlias(alias);
await refresh();
};
// Exclusion handlers
const handleAddExclusion = async (
provider: ProviderType,
pattern: string,
) => {
await routerApi.addExclusion(provider, pattern);
await refresh();
};
// Rule handlers
const handleAddRule = async (rule: RoutingRule) => {
await routerApi.addRoutingRule(rule);
await refresh();
};
const handleRemoveExclusion = async (
provider: ProviderType,
pattern: string,
) => {
await routerApi.removeExclusion(provider, pattern);
await refresh();
};
const handleRemoveRule = async (pattern: string) => {
await routerApi.removeRoutingRule(pattern);
await refresh();
};
// Injection handlers
const handleToggleInjection = async (enabled: boolean) => {
await injectionApi.setInjectionEnabled(enabled);
setInjectionEnabled(enabled);
};
const handleUpdateRule = async (pattern: string, rule: RoutingRule) => {
await routerApi.updateRoutingRule(pattern, rule);
await refresh();
};
const handleAddInjectionRule = async (rule: InjectionRule) => {
await injectionApi.addInjectionRule(rule);
await refresh();
};
// Exclusion handlers
const handleAddExclusion = async (
provider: ProviderType,
pattern: string,
) => {
await routerApi.addExclusion(provider, pattern);
await refresh();
};
const handleRemoveInjectionRule = async (id: string) => {
await injectionApi.removeInjectionRule(id);
await refresh();
};
const handleRemoveExclusion = async (
provider: ProviderType,
pattern: string,
) => {
await routerApi.removeExclusion(provider, pattern);
await refresh();
};
const handleUpdateInjectionRule = async (
id: string,
rule: InjectionRule,
) => {
await injectionApi.updateInjectionRule(id, rule);
await refresh();
};
// Injection handlers
const handleToggleInjection = async (enabled: boolean) => {
await injectionApi.setInjectionEnabled(enabled);
setInjectionEnabled(enabled);
};
const tabs: { id: TabType; label: string; count: number }[] = [
{ id: "aliases", label: "模型别名", count: aliases.length },
{ id: "rules", label: "路由规则", count: rules.length },
{
id: "exclusions",
label: "排除列表",
count: Object.values(exclusions).flat().length,
},
{ id: "injection", label: "参数注入", count: injectionRules.length },
];
const handleAddInjectionRule = async (rule: InjectionRule) => {
await injectionApi.addInjectionRule(rule);
await refresh();
};
const handleRemoveInjectionRule = async (id: string) => {
await injectionApi.removeInjectionRule(id);
await refresh();
};
const handleUpdateInjectionRule = async (id: string, rule: InjectionRule) => {
await injectionApi.updateInjectionRule(id, rule);
await refresh();
};
const tabs: { id: TabType; label: string; count: number }[] = [
{ id: "aliases", label: "模型别名", count: aliases.length },
{ id: "rules", label: "路由规则", count: rules.length },
{
id: "exclusions",
label: "排除列表",
count: Object.values(exclusions).flat().length,
},
{ id: "injection", label: "参数注入", count: injectionRules.length },
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Route className="h-6 w-6" />
智能路由
</h2>
<p className="text-muted-foreground">
配置模型映射、路由规则和排除列表
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowPresets(true)}
className="flex items-center gap-2 rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90"
>
<Sparkles className="h-4 w-4" />
推荐配置
</button>
<button
onClick={handleClearAll}
disabled={loading || (aliases.length === 0 && rules.length === 0)}
className="flex items-center gap-2 rounded-lg border border-red-300 px-3 py-2 text-sm text-red-600 hover:bg-red-50 disabled:opacity-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950/30"
>
<Trash2 className="h-4 w-4" />
清空
</button>
<button
onClick={refresh}
disabled={loading}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
刷新
</button>
</div>
</div>
{/* Presets Modal */}
{showPresets && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-2xl rounded-lg bg-background p-6 shadow-xl max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
推荐配置
</h3>
return (
<div className="space-y-6">
{!hideHeader && (
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Route className="h-6 w-6" />
智能路由
</h2>
<p className="text-muted-foreground">
配置模型映射、路由规则和排除列表
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowPresets(false)}
className="text-muted-foreground hover:text-foreground"
onClick={() => setShowPresets(true)}
className="flex items-center gap-2 rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90"
>
✕
<Sparkles className="h-4 w-4" />
推荐配置
</button>
<button
onClick={handleClearAll}
disabled={
loading || (aliases.length === 0 && rules.length === 0)
}
className="flex items-center gap-2 rounded-lg border border-red-300 px-3 py-2 text-sm text-red-600 hover:bg-red-50 disabled:opacity-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950/30"
>
<Trash2 className="h-4 w-4" />
清空
</button>
<button
onClick={refresh}
disabled={loading}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
>
<RefreshCw
className={`h-4 w-4 ${loading ? "animate-spin" : ""}`}
/>
刷新
</button>
</div>
<p className="text-sm text-muted-foreground mb-4">
选择一个预设配置快速设置路由规则和模型别名
</p>
<div className="space-y-3">
{presets.map((preset) => (
<div
key={preset.id}
className="rounded-lg border p-4 hover:border-primary/50 transition-colors"
</div>
)}
{/* 当隐藏标题时,显示操作按钮 */}
{hideHeader && (
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setShowPresets(true)}
className="flex items-center gap-2 rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90"
>
<Sparkles className="h-4 w-4" />
推荐配置
</button>
<button
onClick={handleClearAll}
disabled={loading || (aliases.length === 0 && rules.length === 0)}
className="flex items-center gap-2 rounded-lg border border-red-300 px-3 py-2 text-sm text-red-600 hover:bg-red-50 disabled:opacity-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950/30"
>
<Trash2 className="h-4 w-4" />
清空
</button>
<button
onClick={refresh}
disabled={loading}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
>
<RefreshCw
className={`h-4 w-4 ${loading ? "animate-spin" : ""}`}
/>
刷新
</button>
</div>
)}
{/* Presets Modal */}
{showPresets && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-2xl rounded-lg bg-background p-6 shadow-xl max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
推荐配置
</h3>
<button
onClick={() => setShowPresets(false)}
className="text-muted-foreground hover:text-foreground"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium">{preset.name}</h4>
<p className="text-sm text-muted-foreground mt-1">
{preset.description}
</p>
<div className="flex gap-4 mt-2 text-xs text-muted-foreground">
<span>{preset.aliases.length} 个别名</span>
<span>{preset.rules.length} 条规则</span>
✕
</button>
</div>
<p className="text-sm text-muted-foreground mb-4">
选择一个预设配置快速设置路由规则和模型别名
</p>
<div className="space-y-3">
{presets.map((preset) => (
<div
key={preset.id}
className="rounded-lg border p-4 hover:border-primary/50 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium">{preset.name}</h4>
<p className="text-sm text-muted-foreground mt-1">
{preset.description}
</p>
<div className="flex gap-4 mt-2 text-xs text-muted-foreground">
<span>{preset.aliases.length} 个别名</span>
<span>{preset.rules.length} 条规则</span>
</div>
</div>
<div className="flex gap-2 ml-4">
<button
onClick={() => handleApplyPreset(preset.id, true)}
disabled={applyingPreset !== null}
className="flex items-center gap-1 rounded px-3 py-1.5 text-sm border hover:bg-muted disabled:opacity-50"
>
{applyingPreset === preset.id ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
合并
</button>
<button
onClick={() => handleApplyPreset(preset.id, false)}
disabled={applyingPreset !== null}
className="flex items-center gap-1 rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{applyingPreset === preset.id ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
应用
</button>
</div>
</div>
<div className="flex gap-2 ml-4">
<button
onClick={() => handleApplyPreset(preset.id, true)}
disabled={applyingPreset !== null}
className="flex items-center gap-1 rounded px-3 py-1.5 text-sm border hover:bg-muted disabled:opacity-50"
>
{applyingPreset === preset.id ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
合并
</button>
<button
onClick={() => handleApplyPreset(preset.id, false)}
disabled={applyingPreset !== null}
className="flex items-center gap-1 rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{applyingPreset === preset.id ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
应用
</button>
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
)}
<HelpTip title="智能路由说明" variant="blue">
<ul className="list-disc list-inside space-y-1 text-sm text-blue-700 dark:text-blue-400">
<li>
<span className="font-medium">模型别名</span>
:使用熟悉的模型名(如 gpt-4)映射到实际模型
</li>
<li>
<span className="font-medium">路由规则</span>
:将特定模型路由到指定 Provider,支持通配符匹配
</li>
<li>
<span className="font-medium">排除列表</span>
:从特定 Provider 排除某些模型
</li>
<li>精确匹配规则优先于通配符规则</li>
</ul>
</HelpTip>
{error && (
<div className="rounded-lg border border-red-500 bg-red-50 p-4 text-red-700 dark:bg-red-950/30">
{error}
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px flex items-center gap-2 ${
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
{tab.label}
{tab.count > 0 && (
<span className="rounded-full bg-muted px-1.5 py-0.5 text-xs">
{tab.count}
</span>
)}
</button>
))}
</div>
)}
<HelpTip title="智能路由说明" variant="blue">
<ul className="list-disc list-inside space-y-1 text-sm text-blue-700 dark:text-blue-400">
<li>
<span className="font-medium">模型别名</span>
:使用熟悉的模型名(如 gpt-4)映射到实际模型
</li>
<li>
<span className="font-medium">路由规则</span>
:将特定模型路由到指定 Provider,支持通配符匹配
</li>
<li>
<span className="font-medium">排除列表</span>
:从特定 Provider 排除某些模型
</li>
<li>精确匹配规则优先于通配符规则</li>
</ul>
</HelpTip>
{error && (
<div className="rounded-lg border border-red-500 bg-red-50 p-4 text-red-700 dark:bg-red-950/30">
{error}
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px flex items-center gap-2 ${
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
{tab.label}
{tab.count > 0 && (
<span className="rounded-full bg-muted px-1.5 py-0.5 text-xs">
{tab.count}
</span>
{/* Tab content */}
{loading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="py-4">
{activeTab === "aliases" && (
<ModelMapping
aliases={aliases}
onAdd={handleAddAlias}
onRemove={handleRemoveAlias}
loading={loading}
/>
)}
</button>
))}
{activeTab === "rules" && (
<RoutingRules
rules={rules}
onAdd={handleAddRule}
onRemove={handleRemoveRule}
onUpdate={handleUpdateRule}
loading={loading}
/>
)}
{activeTab === "exclusions" && (
<ExclusionList
exclusions={exclusions}
onAdd={handleAddExclusion}
onRemove={handleRemoveExclusion}
loading={loading}
/>
)}
{activeTab === "injection" && (
<InjectionRules
rules={injectionRules}
enabled={injectionEnabled}
onToggleEnabled={handleToggleInjection}
onAdd={handleAddInjectionRule}
onRemove={handleRemoveInjectionRule}
onUpdate={handleUpdateInjectionRule}
loading={loading}
/>
)}
</div>
)}
</div>
{/* Tab content */}
{loading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="py-4">
{activeTab === "aliases" && (
<ModelMapping
aliases={aliases}
onAdd={handleAddAlias}
onRemove={handleRemoveAlias}
loading={loading}
/>
)}
{activeTab === "rules" && (
<RoutingRules
rules={rules}
onAdd={handleAddRule}
onRemove={handleRemoveRule}
onUpdate={handleUpdateRule}
loading={loading}
/>
)}
{activeTab === "exclusions" && (
<ExclusionList
exclusions={exclusions}
onAdd={handleAddExclusion}
onRemove={handleRemoveExclusion}
loading={loading}
/>
)}
{activeTab === "injection" && (
<InjectionRules
rules={injectionRules}
enabled={injectionEnabled}
onToggleEnabled={handleToggleInjection}
onAdd={handleAddInjectionRule}
onRemove={handleRemoveInjectionRule}
onUpdate={handleUpdateInjectionRule}
loading={loading}
/>
)}
</div>
)}
</div>
);
});
);
},
);
RoutingPage.displayName = "RoutingPage";
+1
View File
@@ -4,3 +4,4 @@ export { ExclusionList } from "./ExclusionList";
export { InjectionRules } from "./InjectionRules";
export { RoutingPage } from "./RoutingPage";
export type { RoutingPageRef } from "./RoutingPage";
export { RoutingManagementPage } from "./RoutingManagementPage";
+34 -9
View File
@@ -8,6 +8,7 @@ import type { AppType } from "@/lib/api/skills";
interface SkillsPageProps {
initialApp?: AppType;
hideHeader?: boolean;
}
export interface SkillsPageRef {
@@ -16,7 +17,7 @@ export interface SkillsPageRef {
}
export const SkillsPage = forwardRef<SkillsPageRef, SkillsPageProps>(
({ initialApp = "claude" }, ref) => {
({ initialApp = "claude", hideHeader = false }, ref) => {
const [app] = useState<AppType>(initialApp);
const [searchQuery, setSearchQuery] = useState("");
const [filterStatus, setFilterStatus] = useState<
@@ -92,14 +93,38 @@ export const SkillsPage = forwardRef<SkillsPageRef, SkillsPageProps>(
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">Skills</h2>
<p className="text-muted-foreground">
浏览和安装 Claude Code Skills
</p>
{!hideHeader && (
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">Skills</h2>
<p className="text-muted-foreground">
浏览和安装 Claude Code Skills
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={refresh}
disabled={loading}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
>
<RefreshCw
className={`h-4 w-4 ${loading ? "animate-spin" : ""}`}
/>
刷新
</button>
<button
onClick={() => setRepoManagerOpen(true)}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<Settings className="h-4 w-4" />
仓库管理
</button>
</div>
</div>
<div className="flex items-center gap-2">
)}
{hideHeader && (
<div className="flex items-center justify-end gap-2">
<button
onClick={refresh}
disabled={loading}
@@ -118,7 +143,7 @@ export const SkillsPage = forwardRef<SkillsPageRef, SkillsPageProps>(
仓库管理
</button>
</div>
</div>
)}
<HelpTip title="什么是 Skills?" variant="green">
<ul className="list-disc list-inside space-y-1 text-sm text-green-700 dark:text-green-400">