mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
refactor: 合并导航栏,减少导航项从 11 个到 7 个
合并相关功能页面: - 智能路由 + 容错配置 → 路由管理(Tab 切换) - 配置切换 + 配置管理 → 配置管理(Tab 切换) - MCP + Prompts + Skills → 扩展(Tab 切换) 新增组件: - RoutingManagementPage: 路由管理合并页面 - ConfigManagementPage: 配置管理合并页面 - ExtensionsPage: 扩展合并页面 各子页面支持 hideHeader 属性,嵌入合并页面时隐藏标题
This commit is contained in:
+14
-38
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ExtensionsPage } from "./ExtensionsPage";
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user