feat(stage-1): 删除 OAuth 插件 UI 组件

删除内容:
- 删除 OAuthPluginTab.tsx - OAuth 插件标签页
- 删除 OAuthPluginContainer.tsx - 插件容器组件
- 从 ProviderPoolPage 移除 "OAuth 插件" 标签按钮
- 移除 CategoryType 中的 "plugins" 类型
- 更新 index.ts 导出,移除已删除组件的引用

保留内容:
- ClaudeOAuthForm.tsx - 被内置 OAuth 表单使用
- OAuthUrlDisplay.tsx - 被内置 OAuth 表单使用
- 所有内置 OAuth 凭证管理功能

验证:
- TypeScript 编译通过,无错误
- 前端代码结构完整

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
coso
2026-01-24 23:43:53 +08:00
co-authored by Claude Opus 4.5
parent 2e41683134
commit 769709a467
6 changed files with 286 additions and 1449 deletions
+285
View File
@@ -0,0 +1,285 @@
# OAuth 插件系统删除实施计划
## 目标说明
**删除内容**:OAuth 插件管理系统("OAuth 插件" 标签页及其相关功能)
**保留内容**:5 个内置 OAuth 提供者的凭证管理功能(Kiro, Gemini, Antigravity, Codex, Claude)
## 架构确认
### ✅ 保留 - 内置 OAuth 提供者
这些提供者有完整的内置实现,不依赖插件系统:
1. **Kiro** - `src-tauri/src/providers/kiro.rs`
2. **Gemini** - `src-tauri/src/providers/gemini.rs`
3. **Antigravity** - `src-tauri/src/providers/antigravity.rs`
4. **Codex** - `src-tauri/src/providers/codex.rs`
5. **Claude** - `src-tauri/src/providers/claude_oauth.rs`
### ❌ 删除 - OAuth 插件系统
这些是可扩展的插件管理系统,允许安装/卸载第三方 OAuth 提供者插件:
- 插件加载器:`oauth_plugin_loader.rs`
- 插件注册表:`credential/registry.rs` 中的插件部分
- 插件管理命令:`oauth_plugin_cmd.rs`
- 插件 UI 组件:`OAuthPluginTab.tsx`, `OAuthPluginContainer.tsx`
- 插件 API:`oauthPlugin.ts`, `useOAuthPlugins.ts`
---
## Stage 1: 删除前端插件 UI 组件
**Goal**: 删除 "OAuth 插件" 标签页及相关 UI 组件
**Success Criteria**: 前端编译无错误,UI 中不再显示 "OAuth 插件" 标签
**Tests**: 应用启动正常,OAuth 凭证管理功能正常
**Status**: ✅ Complete
### 需要删除的文件:
- `src/components/provider-pool/OAuthPluginTab.tsx` - OAuth 插件标签页
- `src/components/plugins/OAuthPluginContainer.tsx` - 插件容器组件
- `src/components/provider-pool/credential-forms/ClaudeOAuthForm.tsx` - 如果仅用于插件
- `src/components/provider-pool/credential-forms/OAuthUrlDisplay.tsx` - 如果仅用于插件
### 需要修改的文件:
- `src/components/provider-pool/ProviderPoolPage.tsx` 或类似的父组件
- 移除 "OAuth 插件" 标签页的引用
- 保留 "OAuth 凭证" 标签页
---
## Stage 2: 删除前端插件 API 和 Hooks
**Goal**: 删除插件管理相关的前端 API 调用和 Hooks
**Success Criteria**: 前端编译无错误,无未使用的导入
**Tests**: 其他 API 调用正常工作
**Status**: Not Started
### 需要删除的文件:
- `src/lib/api/oauthPlugin.ts` - 插件管理 API
- `src/hooks/useOAuthPlugins.ts` - 插件管理 Hook
- `src/hooks/useOAuthCredentials.ts` - 如果仅用于插件凭证
### 需要保留的文件:
- `src/lib/api/credentials.ts` - 保留内置 OAuth 凭证 API
- 其他与 Kiro/Gemini/Antigravity/Codex/Claude 凭证管理相关的 API
---
## Stage 3: 删除后端插件命令
**Goal**: 删除插件管理相关的 Tauri 命令
**Success Criteria**: 后端编译无错误
**Tests**: 内置 OAuth 命令正常工作
**Status**: Not Started
### 需要删除的文件:
- `src-tauri/src/commands/oauth_plugin_cmd.rs` - 完整删除
### 需要保留的文件:
- `src-tauri/src/commands/oauth_cmd.rs` - 保留(内置 OAuth 凭证命令)
### 需要修改的文件:
- `src-tauri/src/commands/mod.rs`
- 移除 `oauth_plugin_cmd` 模块引用
- 移除所有插件相关命令的注册:
- `init_oauth_plugin_system`
- `list_oauth_plugins`
- `get_oauth_plugin`
- `enable_oauth_plugin`
- `disable_oauth_plugin`
- `install_oauth_plugin`
- `uninstall_oauth_plugin`
- `check_oauth_plugin_updates`
- `update_oauth_plugin`
- `reload_oauth_plugins`
- `get_oauth_plugin_config`
- `update_oauth_plugin_config`
- `scan_oauth_plugin_directory`
- `plugin_credential_*` 系列命令
- `plugin_database_*` 系列命令
- `plugin_http_request`
- `plugin_crypto_*` 系列命令
- `plugin_notification`
- `plugin_storage_*` 系列命令
- `plugin_config_*` 系列命令
- `read_plugin_ui_file`
- `src-tauri/src/main.rs` 或 `src-tauri/src/lib.rs`
- 移除 `OAuthPluginManagerState` 的初始化和注册
---
## Stage 4: 删除插件加载器和注册表
**Goal**: 删除插件系统的核心组件
**Success Criteria**: 后端编译无错误
**Tests**: 内置 OAuth 提供者正常工作
**Status**: Not Started
### 需要删除的文件:
- `src-tauri/src/credential/oauth_plugin_loader.rs` - 插件加载器
- `src-tauri/src/credential/plugin.rs` - 插件接口定义
- `src-tauri/src/credential/unified.rs` - 统一凭证接口(如果仅用于插件)
- `src-tauri/src/credential/sdk.rs` - 插件 SDK
### 需要修改的文件:
- `src-tauri/src/credential/mod.rs`
- 移除插件相关模块的导出
- 移除 `get_global_registry`, `init_global_registry` 等插件注册表函数
- `src-tauri/src/credential/registry.rs`
- 移除插件注册表相关代码
- 保留基础凭证注册功能(如果有)
---
## Stage 5: 清理数据库层
**Goal**: 删除插件相关的数据库表和 DAO
**Success Criteria**: 数据库迁移成功,后端编译无错误
**Tests**: 内置 OAuth 凭证的数据库操作正常
**Status**: Not Started
### 需要删除的表:
从 `src-tauri/src/database/schema.rs` 中删除:
- `credential_provider_plugins` - 插件元数据表
- `plugin_credentials` - 插件凭证表
- `plugin_storage` - 插件存储表
- `plugin_event_logs` - 插件事件日志表
### 需要保留的表:
- `provider_pool_credentials` - 内置 OAuth 凭证表(保留)
- 其他与内置提供者相关的表
### 需要删除的文件:
- `src-tauri/src/database/dao/plugin_credential.rs` - 插件凭证 DAO
### 需要修改的文件:
- `src-tauri/src/database/dao/mod.rs`
- 移除 `plugin_credential` 模块引用
- `src-tauri/src/database/mod.rs`
- 移除插件相关的数据库初始化代码
### 数据库迁移:
创建迁移脚本删除插件相关表:
```sql
DROP TABLE IF EXISTS plugin_event_logs;
DROP TABLE IF EXISTS plugin_storage;
DROP TABLE IF EXISTS plugin_credentials;
DROP TABLE IF EXISTS credential_provider_plugins;
```
---
## Stage 6: 清理类型定义
**Goal**: 删除插件相关的类型定义
**Success Criteria**: 代码编译无错误
**Tests**: 应用正常运行
**Status**: Not Started
### 需要修改的文件:
- `src-tauri/crates/core/src/models/provider_type.rs`
- 检查是否有插件特定的 provider 类型,如有则移除
- 保留 Kiro, Gemini, Antigravity, Codex, Claude 的类型定义
- `src-tauri/crates/core/src/models/provider_pool_model.rs`
- 移除插件凭证相关的类型定义
- 保留内置 OAuth 凭证类型
- `src/lib/plugin-sdk/types.ts`
- 如果整个目录仅用于插件 SDK,则删除整个目录
- 否则移除插件相关的类型定义
---
## Stage 7: 清理依赖项
**Goal**: 移除插件系统相关的依赖包
**Success Criteria**: 依赖安装成功,无冗余依赖
**Tests**: 应用正常启动和运行
**Status**: Not Started
### 前端 (package.json):
- 检查是否有插件系统专用的依赖,如有则移除
- 保留 OAuth 凭证管理所需的依赖
### 后端 (Cargo.toml):
- 检查 `src-tauri/Cargo.toml` 中是否有插件系统专用的 crate
- 可能需要移除的依赖:
- 动态加载相关的 crate(如 `libloading`, `dlopen` 等)
- 插件沙箱相关的 crate
---
## Stage 8: 清理文档和脚本
**Goal**: 删除插件相关的文档和脚本
**Success Criteria**: 文档目录清理完成
**Tests**: 无
**Status**: Not Started
### 需要检查和修改的文档:
- `docs/plugins/` 目录 - 如果整个目录仅用于插件文档,则删除
- `docs/content/03.providers/1.overview.md` - 移除插件系统相关的说明
- `docs/content/02.user-guide/4.configuration-example.md` - 移除插件配置示例
- `README.md` - 移除插件系统相关的说明
### 需要保留的文档:
- 关于 Kiro, Gemini, Antigravity, Codex, Claude 的 OAuth 配置文档
### 需要删除的脚本:
- 检查 `scripts/` 目录中是否有插件相关的脚本
---
## Stage 9: 最终验证和清理
**Goal**: 确保所有插件系统代码已删除,内置 OAuth 功能正常
**Success Criteria**: 所有验证项通过
**Tests**: 完整的功能测试
**Status**: Not Started
### 验证清单:
- [ ] 前端应用正常启动
- [ ] 后端应用正常启动
- [ ] 无编译错误或警告
- [ ] UI 中不再显示 "OAuth 插件" 标签页
- [ ] "OAuth 凭证" 标签页正常显示
- [ ] Kiro OAuth 凭证管理正常
- [ ] Gemini OAuth 凭证管理正常
- [ ] Antigravity OAuth 凭证管理正常
- [ ] Codex OAuth 凭证管理正常
- [ ] Claude OAuth 凭证管理正常
- [ ] 数据库迁移成功
- [ ] 代码库中不再有插件系统相关的引用
### 代码搜索验证:
使用以下关键词搜索,确保没有遗漏:
- `oauth_plugin`
- `OAuthPlugin`
- `plugin_credential`
- `PluginCredential`
- `credential_provider_plugins`
- `plugin_storage`
- `plugin_event_logs`
- `OAuthPluginLoader`
- `PluginRegistry`
---
## 注意事项
1. **备份**: 在开始之前,建议创建数据库备份和代码分支
2. **依赖检查**: 仔细检查是否有非插件代码依赖插件模块
3. **测试**: 每个阶段完成后进行编译测试
4. **提交**: 每个阶段完成后创建一个 commit
5. **渐进式**: 按阶段顺序执行,不要跳跃
## 关键区别
### 插件系统(删除)
- 可扩展架构,支持第三方插件
- 插件加载器和注册表
- 插件安装/卸载功能
- 插件 SDK 和权限系统
- 动态加载外部代码
### 内置 OAuth(保留)
- 硬编码的 5 个提供者
- 直接在代码中实现
- 不支持动态加载
- 凭证管理功能
- OAuth 流程实现
@@ -1,581 +0,0 @@
/**
* @file OAuth Provider 插件容器组件
* @description 专门用于 OAuth Provider 插件的容器,整合 SDK 和 UI 系统
* @module components/plugins/OAuthPluginContainer
*/
import React, { useState, useCallback } from "react";
import {
Loader2,
AlertCircle,
RefreshCw,
Settings2,
Key,
Plus,
FileText,
FolderOpen,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Modal } from "@/components/Modal";
import { usePluginSDK } from "@/lib/plugin-sdk";
import { PluginUIContainer } from "@/lib/plugin-ui";
import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
import type { PluginId } from "@/lib/plugin-sdk/types";
import type { CredentialInfo } from "@/lib/plugin-sdk/types";
interface OAuthPluginContainerProps {
/** 插件 ID */
pluginId: PluginId;
/** 插件显示名称 */
displayName: string;
/** 插件描述 */
description?: string;
/** 插件版本 */
version?: string;
/** 是否启用 */
enabled?: boolean;
/** 自定义类名 */
className?: string;
/** 启用/禁用回调 */
onToggleEnabled?: (enabled: boolean) => void;
}
/**
* 凭证卡片组件
*/
const CredentialCard: React.FC<{
credential: CredentialInfo;
onRefresh: () => void;
onDelete: () => void;
}> = ({ credential, onRefresh, onDelete }) => {
const statusColors: Record<string, string> = {
active: "bg-green-500",
inactive: "bg-gray-500",
expired: "bg-yellow-500",
error: "bg-red-500",
};
return (
<Card className="relative">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
{credential.displayName || credential.id}
</CardTitle>
<Badge variant="outline" className="text-xs">
<span
className={`w-2 h-2 rounded-full mr-1 ${statusColors[credential.status] || "bg-gray-500"}`}
/>
{credential.status}
</Badge>
</div>
<CardDescription className="text-xs">
认证类型: {credential.authType}
</CardDescription>
</CardHeader>
<CardContent className="pt-2">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>
{credential.lastUsedAt
? `最后使用: ${new Date(credential.lastUsedAt).toLocaleDateString()}`
: "未使用"}
</span>
</div>
<div className="flex gap-2 mt-3">
<Button variant="outline" size="sm" onClick={onRefresh}>
<RefreshCw className="h-3 w-3 mr-1" />
刷新
</Button>
<Button variant="destructive" size="sm" onClick={onDelete}>
删除
</Button>
</div>
</CardContent>
</Card>
);
};
// ============================================================================
// 添加凭证模态框
// ============================================================================
type AddCredentialMode = "json" | "file";
interface AddCredentialDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
pluginId: string;
pluginName: string;
onAdd: (authType: string, config: Record<string, unknown>) => Promise<void>;
}
const AddCredentialDialog: React.FC<AddCredentialDialogProps> = ({
open,
onOpenChange,
pluginName,
onAdd,
}) => {
const [mode, setMode] = useState<AddCredentialMode>("json");
const [name, setName] = useState("");
const [jsonContent, setJsonContent] = useState("");
const [filePath, setFilePath] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 重置表单
const resetForm = useCallback(() => {
setName("");
setJsonContent("");
setFilePath("");
setError(null);
setLoading(false);
}, []);
// 关闭时重置
const handleClose = useCallback(() => {
resetForm();
onOpenChange(false);
}, [resetForm, onOpenChange]);
// 选择文件
const handleSelectFile = async () => {
try {
const selected = await openFileDialog({
multiple: false,
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (selected) {
setFilePath(selected as string);
}
} catch (e) {
console.error("Failed to open file dialog:", e);
}
};
// 提交 JSON 模式
const handleJsonSubmit = async () => {
if (!jsonContent.trim()) {
setError("请粘贴凭证 JSON 内容");
return;
}
// 验证 JSON 格式
let parsedConfig: Record<string, unknown>;
try {
parsedConfig = JSON.parse(jsonContent);
} catch {
setError("JSON 格式无效,请检查内容");
return;
}
setLoading(true);
setError(null);
try {
// 添加显示名称到配置
if (name.trim()) {
parsedConfig.displayName = name.trim();
}
await onAdd("oauth", parsedConfig);
handleClose();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
};
// 提交文件模式
const handleFileSubmit = async () => {
if (!filePath) {
setError("请选择凭证文件");
return;
}
setLoading(true);
setError(null);
try {
// 读取文件内容并解析
const config: Record<string, unknown> = {
filePath,
displayName: name.trim() || undefined,
};
await onAdd("oauth", config);
handleClose();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
};
return (
<Modal isOpen={open} onClose={handleClose} maxWidth="max-w-md">
{/* Header */}
<div className="flex items-center justify-between border-b px-6 py-4">
<h3 className="text-lg font-semibold">添加 {pluginName} 凭证</h3>
</div>
{/* Content */}
<div className="space-y-4 px-6 py-4">
{/* 模式选择器 */}
<div className="grid grid-cols-2 gap-1 p-1 bg-muted/50 rounded-xl border">
<button
type="button"
onClick={() => {
setMode("json");
setError(null);
}}
disabled={loading}
className={`py-2 px-3 text-sm rounded-lg transition-all duration-200 font-medium ${
mode === "json"
? "bg-background text-foreground shadow-sm ring-1 ring-black/5"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
>
<FileText className="inline h-4 w-4 mr-1" />
粘贴 JSON
</button>
<button
type="button"
onClick={() => {
setMode("file");
setError(null);
}}
disabled={loading}
className={`py-2 px-3 text-sm rounded-lg transition-all duration-200 font-medium ${
mode === "file"
? "bg-background text-foreground shadow-sm ring-1 ring-black/5"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
>
<FolderOpen className="inline h-4 w-4 mr-1" />
导入文件
</button>
</div>
{/* 名称字段 */}
<div>
<label className="mb-1 block text-sm font-medium">名称 (可选)</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="给这个凭证起个名字..."
disabled={loading}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
/>
</div>
{/* JSON 模式 */}
{mode === "json" && (
<div>
<label className="mb-1 block text-sm font-medium">
凭证 JSON <span className="text-red-500">*</span>
</label>
<textarea
value={jsonContent}
onChange={(e) => setJsonContent(e.target.value)}
placeholder={`粘贴凭证 JSON 内容,例如:
{
"accessToken": "...",
"refreshToken": "...",
...
}`}
disabled={loading}
className="w-full h-48 rounded-lg border bg-background px-3 py-2 text-sm font-mono resize-none"
/>
</div>
)}
{/* 文件模式 */}
{mode === "file" && (
<div>
<label className="mb-1 block text-sm font-medium">
凭证文件路径 <span className="text-red-500">*</span>
</label>
<div className="flex gap-2">
<input
type="text"
value={filePath}
onChange={(e) => setFilePath(e.target.value)}
placeholder="选择凭证文件..."
disabled={loading}
className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm"
/>
<button
type="button"
onClick={handleSelectFile}
disabled={loading}
className="flex items-center gap-1 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
<FolderOpen className="h-4 w-4" />
浏览
</button>
</div>
</div>
)}
{/* 错误提示 */}
{error && (
<div className="rounded-lg border border-red-500 bg-red-50 p-3 text-sm text-red-700 dark:bg-red-950/30">
{error}
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-end gap-2 border-t px-6 py-4">
<button
onClick={handleClose}
disabled={loading}
className="rounded-lg border px-4 py-2 text-sm hover:bg-muted"
>
取消
</button>
<button
onClick={mode === "json" ? handleJsonSubmit : handleFileSubmit}
disabled={loading}
className="rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{loading ? "添加中..." : "添加凭证"}
</button>
</div>
</Modal>
);
};
/**
* OAuth Provider 插件容器
*
* 提供:
* - 凭证列表管理
* - 插件设置
* - 插件 UI 渲染
*/
export const OAuthPluginContainer: React.FC<OAuthPluginContainerProps> = ({
pluginId,
displayName,
description,
version,
enabled = true,
className,
onToggleEnabled,
}) => {
const { sdk, credentials, loading, error, refresh } = usePluginSDK(pluginId);
const [activeTab, setActiveTab] = useState("credentials");
const [addCredentialDialogOpen, setAddCredentialDialogOpen] = useState(false);
// 添加凭证
const handleAddCredential = useCallback(
async (authType: string, config: Record<string, unknown>) => {
try {
await sdk.credential.create(authType, config);
sdk.notification.success("凭证添加成功");
refresh();
} catch (e) {
const msg = e instanceof Error ? e.message : "未知错误";
sdk.notification.error(`添加失败: ${msg}`);
throw e; // 重新抛出以便模态框显示错误
}
},
[sdk, refresh],
);
// 刷新凭证
const handleRefreshCredential = useCallback(
async (credentialId: string) => {
try {
await sdk.credential.refresh(credentialId);
sdk.notification.success("凭证刷新成功");
refresh();
} catch (e) {
const msg = e instanceof Error ? e.message : "未知错误";
sdk.notification.error(`刷新失败: ${msg}`);
}
},
[sdk, refresh],
);
// 删除凭证
const handleDeleteCredential = useCallback(
async (credentialId: string) => {
try {
await sdk.credential.delete(credentialId);
sdk.notification.success("凭证已删除");
refresh();
} catch (e) {
const msg = e instanceof Error ? e.message : "未知错误";
sdk.notification.error(`删除失败: ${msg}`);
}
},
[sdk, refresh],
);
// 加载状态
if (loading) {
return (
<div className={`flex items-center justify-center p-8 ${className}`}>
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">加载插件...</span>
</div>
);
}
// 错误状态
if (error) {
return (
<div
className={`flex flex-col items-center justify-center p-8 ${className}`}
>
<AlertCircle className="h-8 w-8 text-red-500 mb-2" />
<p className="text-red-600 mb-4">{error}</p>
<Button variant="outline" size="sm" onClick={refresh}>
<RefreshCw className="h-4 w-4 mr-2" />
重试
</Button>
</div>
);
}
return (
<div className={`space-y-4 ${className}`}>
{/* 插件头部信息 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
{displayName}
{version && (
<Badge variant="secondary" className="text-xs">
v{version}
</Badge>
)}
</CardTitle>
{description && (
<CardDescription className="mt-1">
{description}
</CardDescription>
)}
</div>
<div className="flex items-center gap-2">
<Badge variant={enabled ? "default" : "secondary"}>
{enabled ? "已启用" : "已禁用"}
</Badge>
{onToggleEnabled && (
<Button
variant="outline"
size="sm"
onClick={() => onToggleEnabled(!enabled)}
>
{enabled ? "禁用" : "启用"}
</Button>
)}
</div>
</div>
</CardHeader>
</Card>
{/* 标签页 */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="credentials" className="flex items-center gap-1">
<Key className="h-4 w-4" />
凭证 ({credentials.length})
</TabsTrigger>
<TabsTrigger value="ui" className="flex items-center gap-1">
插件 UI
</TabsTrigger>
<TabsTrigger value="settings" className="flex items-center gap-1">
<Settings2 className="h-4 w-4" />
设置
</TabsTrigger>
</TabsList>
{/* 凭证列表 */}
<TabsContent value="credentials" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-sm font-medium">已配置的凭证</h3>
<Button size="sm" onClick={() => setAddCredentialDialogOpen(true)}>
<Plus className="h-4 w-4 mr-1" />
添加凭证
</Button>
</div>
{credentials.length === 0 ? (
<div className="flex flex-col items-center justify-center p-8 text-muted-foreground border rounded-lg border-dashed">
<Key className="h-8 w-8 mb-2" />
<p>暂无凭证</p>
<p className="text-xs mt-1">点击"添加凭证"开始配置</p>
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={() => setAddCredentialDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-1" />
添加第一个凭证
</Button>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2">
{credentials.map((cred) => (
<CredentialCard
key={cred.id}
credential={cred}
onRefresh={() => handleRefreshCredential(cred.id)}
onDelete={() => handleDeleteCredential(cred.id)}
/>
))}
</div>
)}
</TabsContent>
{/* 插件 UI */}
<TabsContent value="ui">
<PluginUIContainer
pluginId={pluginId}
emptyMessage="该插件没有提供自定义 UI"
/>
</TabsContent>
{/* 设置 */}
<TabsContent value="settings">
<Card>
<CardHeader>
<CardTitle className="text-base">插件设置</CardTitle>
<CardDescription>配置插件的行为和参数</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
此插件暂无可配置的设置项
</p>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 添加凭证模态框 */}
<AddCredentialDialog
open={addCredentialDialogOpen}
onOpenChange={setAddCredentialDialogOpen}
pluginId={pluginId}
pluginName={displayName}
onAdd={handleAddCredential}
/>
</div>
);
};
export default OAuthPluginContainer;
-1
View File
@@ -2,6 +2,5 @@ export { PluginManager } from "./PluginManager";
export { PluginInstallDialog } from "./PluginInstallDialog";
export { PluginUninstallDialog } from "./PluginUninstallDialog";
export { PluginUIRenderer } from "./PluginUIRenderer";
export { OAuthPluginContainer } from "./OAuthPluginContainer";
export type { Page } from "./PluginUIRenderer";
export { default } from "./PluginManager";
@@ -1,843 +0,0 @@
/**
* @file OAuth Provider 插件管理标签页
* @description 显示和管理所有 OAuth Provider 插件
* @module components/provider-pool/OAuthPluginTab
*/
import React, { useState, useCallback, useMemo } from "react";
import {
Loader2,
AlertCircle,
RefreshCw,
Plus,
Download,
Search,
Package,
Power,
PowerOff,
Trash2,
ArrowUpCircle,
Cloud,
Sparkles,
Bot,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useOAuthPlugins } from "@/hooks/useOAuthPlugins";
import { PluginUIRenderer } from "@/lib/plugin-loader/PluginUIRenderer";
import { usePluginSDK } from "@/lib/plugin-sdk";
import type {
OAuthPluginInfo,
PluginUpdate,
PluginSource,
} from "@/lib/api/oauthPlugin";
// ============================================================================
// 推荐插件配置
// ============================================================================
/** 推荐插件配置 */
interface RecommendedOAuthPlugin {
/** 插件 ID */
id: string;
/** 插件名称 */
name: string;
/** 插件描述 */
description: string;
/** 图标组件 */
icon: React.ComponentType<{ className?: string }>;
/** 目标协议 */
targetProtocol: string;
/** 安装来源 */
source: PluginSource;
/** 下载 URL(用于一键安装) */
downloadUrl: string;
/** 标签 */
tags?: string[];
/** 是否推荐 */
recommended?: boolean;
/** 是否可安装(false 表示即将推出) */
available?: boolean;
}
/**
* 推荐的 OAuth Provider 插件列表
*/
const recommendedOAuthPlugins: RecommendedOAuthPlugin[] = [
{
id: "kiro-provider",
name: "Kiro (CodeWhisperer)",
description: "Kiro (AWS CodeWhisperer) OAuth Provider - 支持 Claude 模型",
icon: Cloud,
targetProtocol: "anthropic",
source: {
type: "git_hub",
owner: "aiclientproxy",
repo: "kiro-provider",
version: "v0.3.0",
},
downloadUrl:
"https://github.com/aiclientproxy/kiro-provider/releases/download/v0.3.0/kiro-provider-plugin.zip",
tags: ["anthropic", "免费"],
recommended: true,
available: true,
},
{
id: "antigravity-provider",
name: "Antigravity (Gemini CLI)",
description:
"Antigravity (Google Gemini CLI) OAuth Provider - 支持 Gemini 和 Claude 模型",
icon: Sparkles,
targetProtocol: "dynamic",
source: {
type: "git_hub",
owner: "aiclientproxy",
repo: "antigravity-provider",
version: "v0.4.0",
},
downloadUrl:
"https://github.com/aiclientproxy/antigravity-provider/releases/download/v0.4.0/antigravity-provider-plugin.zip",
tags: ["gemini", "claude", "免费"],
recommended: true,
available: true,
},
{
id: "claude-provider",
name: "Claude Provider",
description: "Claude OAuth Provider - 支持 Claude.ai 官方 OAuth 认证",
icon: Bot,
targetProtocol: "anthropic",
source: {
type: "git_hub",
owner: "aiclientproxy",
repo: "claude-provider",
version: "v0.3.0",
},
downloadUrl:
"https://github.com/aiclientproxy/claude-provider/releases/download/v0.3.0/claude-provider-plugin.zip",
tags: ["anthropic", "官方"],
recommended: false,
available: true,
},
{
id: "droid-provider",
name: "Droid Provider",
description:
"Factory.ai Droid OAuth Provider - 支持 Anthropic 和 OpenAI 模型",
icon: Bot,
targetProtocol: "dynamic",
source: {
type: "git_hub",
owner: "aiclientproxy",
repo: "droid-provider",
version: "v0.3.0",
},
downloadUrl:
"https://github.com/aiclientproxy/droid-provider/releases/download/v0.3.0/droid-provider-plugin.zip",
tags: ["anthropic", "openai"],
recommended: false,
available: true,
},
{
id: "gemini-provider",
name: "Gemini Provider",
description: "Google Gemini OAuth Provider - 支持 OAuth 和 API Key 认证",
icon: Sparkles,
targetProtocol: "gemini",
source: {
type: "git_hub",
owner: "aiclientproxy",
repo: "gemini-provider",
version: "v0.4.0",
},
downloadUrl:
"https://github.com/aiclientproxy/gemini-provider/releases/download/v0.4.0/gemini-provider-plugin.zip",
tags: ["gemini", "API Key"],
recommended: false,
available: true,
},
{
id: "codex-provider",
name: "Codex Provider",
description: "Codex OAuth Provider - 支持 OpenAI 模型 (GPT-*, O1-*, O3-*)",
icon: Bot,
targetProtocol: "openai",
source: {
type: "git_hub",
owner: "aiclientproxy",
repo: "codex-provider",
version: "v0.1.0",
},
downloadUrl:
"https://github.com/aiclientproxy/codex-provider/releases/download/v0.1.0/codex-provider-plugin.zip",
tags: ["openai", "免费"],
recommended: false,
available: true,
},
];
// ============================================================================
// 推荐插件卡片组件
// ============================================================================
/**
* 推荐插件卡片组件
*/
const RecommendedPluginCard: React.FC<{
plugin: RecommendedOAuthPlugin;
onInstall: () => void;
installing?: boolean;
}> = ({ plugin, onInstall, installing }) => {
const protocolColors: Record<string, string> = {
anthropic: "bg-orange-500",
openai: "bg-green-500",
gemini: "bg-blue-500",
qwen: "bg-purple-500",
};
return (
<Card
className={`relative transition-shadow hover:shadow-md ${!plugin.available ? "opacity-70" : ""}`}
>
{plugin.recommended && (
<Badge
className="absolute -top-2 -right-2 bg-green-500"
variant="default"
>
推荐
</Badge>
)}
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<div className="p-2 bg-primary/10 rounded-lg">
<plugin.icon className="h-5 w-5 text-primary" />
</div>
{plugin.name}
</CardTitle>
</div>
<CardDescription className="text-xs line-clamp-2">
{plugin.description}
</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-2">
<div className="flex items-center gap-2 flex-wrap">
<Badge
variant="secondary"
className={`text-xs text-white ${
protocolColors[plugin.targetProtocol.toLowerCase()] ||
"bg-gray-500"
}`}
>
{plugin.targetProtocol}
</Badge>
{plugin.tags?.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</CardContent>
<CardFooter className="pt-2">
{plugin.available ? (
<Button className="w-full" onClick={onInstall} disabled={installing}>
{installing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
安装中...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
一键安装
</>
)}
</Button>
) : (
<Button className="w-full" variant="secondary" disabled>
即将推出
</Button>
)}
</CardFooter>
</Card>
);
};
/**
* 插件卡片组件
*/
const PluginCard: React.FC<{
plugin: OAuthPluginInfo;
update?: PluginUpdate;
onSelect: () => void;
onToggle: () => void;
onUninstall: () => void;
onUpdate?: () => void;
}> = ({ plugin, update, onSelect, onToggle, onUninstall, onUpdate }) => {
const protocolColors: Record<string, string> = {
anthropic: "bg-orange-500",
openai: "bg-green-500",
gemini: "bg-blue-500",
qwen: "bg-purple-500",
};
return (
<Card
className={`relative cursor-pointer transition-shadow hover:shadow-md ${
!plugin.enabled ? "opacity-60" : ""
}`}
onClick={onSelect}
>
{update && (
<Badge
className="absolute -top-2 -right-2 bg-blue-500"
variant="default"
>
有更新
</Badge>
)}
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Package className="h-4 w-4" />
{plugin.displayName}
</CardTitle>
<Badge variant="outline" className="text-xs">
v{plugin.version}
</Badge>
</div>
<CardDescription className="text-xs line-clamp-2">
{plugin.description || "无描述"}
</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-2">
<div className="flex items-center gap-2 flex-wrap">
<Badge
variant="secondary"
className={`text-xs text-white ${
protocolColors[plugin.targetProtocol.toLowerCase()] ||
"bg-gray-500"
}`}
>
{plugin.targetProtocol}
</Badge>
<Badge variant="outline" className="text-xs">
{plugin.credentialCount} 凭证
</Badge>
<Badge
variant={plugin.enabled ? "default" : "secondary"}
className="text-xs"
>
{plugin.enabled ? "已启用" : "已禁用"}
</Badge>
</div>
</CardContent>
<CardFooter className="pt-2 flex gap-2">
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
onToggle();
}}
>
{plugin.enabled ? (
<>
<PowerOff className="h-3 w-3 mr-1" />
禁用
</>
) : (
<>
<Power className="h-3 w-3 mr-1" />
启用
</>
)}
</Button>
{update && onUpdate && (
<Button
variant="default"
size="sm"
onClick={(e) => {
e.stopPropagation();
onUpdate();
}}
>
<ArrowUpCircle className="h-3 w-3 mr-1" />
更新
</Button>
)}
<Button
variant="destructive"
size="sm"
onClick={(e) => {
e.stopPropagation();
onUninstall();
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</CardFooter>
</Card>
);
};
// ============================================================================
// 插件详情视图组件
// ============================================================================
/**
* 插件详情视图
* 动态加载并渲染插件自己的 UI 组件
*/
const PluginDetailView: React.FC<{
plugin: OAuthPluginInfo;
onBack: () => void;
onToggle: (enabled: boolean) => void;
}> = ({ plugin, onBack, onToggle }) => {
// 获取插件 SDK
const { sdk } = usePluginSDK(plugin.id);
// 插件目录路径(从配置获取)
const pluginsDir =
plugin.installPath?.replace(`/${plugin.id}`, "") ||
`~/Library/Application Support/proxycast/plugins`;
return (
<div className="space-y-4">
{/* 头部 */}
<div className="flex items-center justify-between">
<Button variant="outline" onClick={onBack}>
返回列表
</Button>
<div className="flex items-center gap-2">
<Badge variant={plugin.enabled ? "default" : "secondary"}>
{plugin.enabled ? "已启用" : "已禁用"}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => onToggle(!plugin.enabled)}
>
{plugin.enabled ? (
<>
<PowerOff className="h-4 w-4 mr-1" />
禁用
</>
) : (
<>
<Power className="h-4 w-4 mr-1" />
启用
</>
)}
</Button>
</div>
</div>
{/* 插件信息卡片 */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Package className="h-5 w-5" />
{plugin.displayName}
</CardTitle>
<Badge variant="outline">v{plugin.version}</Badge>
</div>
<CardDescription>{plugin.description}</CardDescription>
</CardHeader>
</Card>
{/* 插件 UI - 动态加载 */}
<Card>
<CardContent className="pt-6">
<PluginUIRenderer
pluginsDir={pluginsDir}
pluginId={plugin.id}
uiEntry={plugin.uiEntry || "dist/index.js"}
sdk={sdk}
fallback={
<div className="flex flex-col items-center justify-center p-8 text-muted-foreground">
<Package className="h-12 w-12 mb-4 opacity-50" />
<p className="text-center">该插件暂无 UI 界面</p>
<p className="text-center text-sm mt-2 opacity-70">
请通过凭证池页面的「OAuth 凭证」标签管理此插件的凭证
</p>
</div>
}
/>
</CardContent>
</Card>
</div>
);
};
/**
* 安装插件对话框
*/
const InstallPluginDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
onInstall: (path: string) => void;
}> = ({ open, onOpenChange, onInstall }) => {
const [path, setPath] = useState("");
const handleInstall = () => {
if (path.trim()) {
onInstall(path.trim());
setPath("");
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>安装 OAuth Provider 插件</DialogTitle>
<DialogDescription>
输入本地插件目录路径或 GitHub 仓库地址
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">插件路径</label>
<Input
placeholder="例如: /path/to/plugin 或 owner/repo"
value={path}
onChange={(e) => setPath(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
取消
</Button>
<Button onClick={handleInstall} disabled={!path.trim()}>
<Download className="h-4 w-4 mr-2" />
安装
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
/**
* OAuth Provider 插件管理标签页
*/
export const OAuthPluginTab: React.FC = () => {
const {
plugins,
loading,
error,
updates,
refresh,
enable,
disable,
install,
uninstall,
update,
checkUpdates,
reload,
} = useOAuthPlugins();
const [searchQuery, setSearchQuery] = useState("");
const [selectedPluginId, setSelectedPluginId] = useState<string | null>(null);
const [installDialogOpen, setInstallDialogOpen] = useState(false);
const [uninstallDialogOpen, setUninstallDialogOpen] = useState(false);
const [pluginToUninstall, setPluginToUninstall] = useState<string | null>(
null,
);
const [installingPluginId, setInstallingPluginId] = useState<string | null>(
null,
);
// 已安装插件的 ID 集合
const installedPluginIds = useMemo(
() => new Set(plugins.map((p) => p.id)),
[plugins],
);
// 过滤出未安装的推荐插件
const uninstalledRecommendedPlugins = useMemo(
() => recommendedOAuthPlugins.filter((p) => !installedPluginIds.has(p.id)),
[installedPluginIds],
);
// 过滤插件
const filteredPlugins = plugins.filter(
(p) =>
p.displayName.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.targetProtocol.toLowerCase().includes(searchQuery.toLowerCase()),
);
// 处理安装
const handleInstall = useCallback(
async (path: string) => {
const source = path.includes("/")
? { type: "local_file" as const, path }
: { type: "local_file" as const, path };
await install(source);
},
[install],
);
// 处理推荐插件安装
const handleRecommendedInstall = useCallback(
async (plugin: RecommendedOAuthPlugin) => {
setInstallingPluginId(plugin.id);
try {
await install(plugin.source);
} finally {
setInstallingPluginId(null);
}
},
[install],
);
// 处理卸载确认
const handleUninstallConfirm = useCallback(async () => {
if (pluginToUninstall) {
await uninstall(pluginToUninstall);
setPluginToUninstall(null);
setUninstallDialogOpen(false);
if (selectedPluginId === pluginToUninstall) {
setSelectedPluginId(null);
}
}
}, [pluginToUninstall, uninstall, selectedPluginId]);
// 处理切换
const handleToggle = useCallback(
async (plugin: OAuthPluginInfo) => {
if (plugin.enabled) {
await disable(plugin.id);
} else {
await enable(plugin.id);
}
},
[enable, disable],
);
// 获取选中的插件
const selectedPlugin = plugins.find((p) => p.id === selectedPluginId);
// 加载状态
if (loading) {
return (
<div className="flex items-center justify-center p-16">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<span className="ml-3 text-muted-foreground">加载插件列表...</span>
</div>
);
}
// 错误状态
if (error) {
return (
<div className="flex flex-col items-center justify-center p-16">
<AlertCircle className="h-12 w-12 text-red-500 mb-4" />
<p className="text-red-600 mb-4">{error}</p>
<Button variant="outline" onClick={refresh}>
<RefreshCw className="h-4 w-4 mr-2" />
重试
</Button>
</div>
);
}
// 显示插件详情
if (selectedPlugin) {
return (
<PluginDetailView
plugin={selectedPlugin}
onBack={() => setSelectedPluginId(null)}
onToggle={(enabled) =>
enabled ? enable(selectedPlugin.id) : disable(selectedPlugin.id)
}
/>
);
}
return (
<div className="space-y-4">
{/* 工具栏 */}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2 flex-1">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索插件..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={checkUpdates}>
检查更新
</Button>
<Button variant="outline" size="sm" onClick={reload}>
<RefreshCw className="h-4 w-4 mr-1" />
刷新
</Button>
<Button size="sm" onClick={() => setInstallDialogOpen(true)}>
<Plus className="h-4 w-4 mr-1" />
安装插件
</Button>
</div>
</div>
{/* 更新提示 */}
{updates.length > 0 && (
<Card className="border-blue-200 bg-blue-50">
<CardContent className="py-3">
<div className="flex items-center justify-between">
<span className="text-sm text-blue-700">
有 {updates.length} 个插件可更新
</span>
<Button variant="ghost" size="sm" className="text-blue-700">
查看全部
</Button>
</div>
</CardContent>
</Card>
)}
{/* 已安装插件 - 放在上面 */}
{filteredPlugins.length > 0 && (
<div className="rounded-lg border bg-card">
<div className="p-4 border-b">
<h4 className="font-semibold flex items-center gap-2">
<Package className="h-4 w-4" />
已安装插件
</h4>
<p className="text-sm text-muted-foreground mt-1">
点击卡片进入插件详情,管理凭证
</p>
</div>
<div className="p-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredPlugins.map((plugin) => (
<PluginCard
key={plugin.id}
plugin={plugin}
update={updates.find((u) => u.pluginId === plugin.id)}
onSelect={() => setSelectedPluginId(plugin.id)}
onToggle={() => handleToggle(plugin)}
onUninstall={() => {
setPluginToUninstall(plugin.id);
setUninstallDialogOpen(true);
}}
onUpdate={
updates.find((u) => u.pluginId === plugin.id)
? () => update(plugin.id)
: undefined
}
/>
))}
</div>
</div>
</div>
)}
{/* 推荐插件 - 放在下面 */}
{uninstalledRecommendedPlugins.length > 0 && (
<div className="rounded-lg border bg-card">
<div className="p-4 border-b">
<h4 className="font-semibold flex items-center gap-2">
<Download className="h-4 w-4" />
可安装的 OAuth Provider 插件
</h4>
<p className="text-sm text-muted-foreground mt-1">
一键安装 OAuth Provider 插件,快速扩展支持的 AI 服务
</p>
</div>
<div className="p-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{uninstalledRecommendedPlugins.map((plugin) => (
<RecommendedPluginCard
key={plugin.id}
plugin={plugin}
onInstall={() => handleRecommendedInstall(plugin)}
installing={installingPluginId === plugin.id}
/>
))}
</div>
</div>
</div>
)}
{/* 空状态 - 没有已安装插件且没有推荐插件 */}
{filteredPlugins.length === 0 &&
uninstalledRecommendedPlugins.length === 0 && (
<div className="flex flex-col items-center justify-center p-16 border rounded-lg border-dashed">
<Package className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-2">
{searchQuery ? "没有找到匹配的插件" : "暂无可用的插件"}
</p>
{!searchQuery && (
<Button
variant="outline"
onClick={() => setInstallDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-1" />
手动安装插件
</Button>
)}
</div>
)}
{/* 安装对话框 */}
<InstallPluginDialog
open={installDialogOpen}
onOpenChange={setInstallDialogOpen}
onInstall={handleInstall}
/>
{/* 卸载确认对话框 */}
<Dialog open={uninstallDialogOpen} onOpenChange={setUninstallDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader className="px-6 pt-6">
<DialogTitle>确认卸载插件</DialogTitle>
<DialogDescription>
此操作将删除插件及其所有凭证数据,此操作无法撤销。
</DialogDescription>
</DialogHeader>
<DialogFooter className="px-6 pb-6 pt-4">
<Button
variant="outline"
onClick={() => setUninstallDialogOpen(false)}
>
取消
</Button>
<Button variant="destructive" onClick={handleUninstallConfirm}>
确认卸载
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default OAuthPluginTab;
@@ -36,7 +36,6 @@ import { getConfig } from "@/hooks/useTauri";
import { ProviderIcon } from "@/icons/providers";
import { ApiKeyProviderSection, AddCustomProviderModal } from "./api-key";
import type { ApiKeyProviderSectionRef } from "./api-key";
// import { OAuthPluginTab } from "./OAuthPluginTab"; // 暂时禁用
import { RelayProvidersSection } from "./RelayProvidersSection";
import { ModelRegistryTab } from "./ModelRegistryTab";
import type { AddCustomProviderRequest } from "@/lib/api/apiKeyProvider";
@@ -83,7 +82,7 @@ const isConfigTab = (tab: TabType): tab is ConfigTabType => {
};
// 分类类型
type CategoryType = "oauth" | "apikey" | "plugins" | "connect" | "models";
type CategoryType = "oauth" | "apikey" | "connect" | "models";
export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
(_props, ref) => {
@@ -399,20 +398,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
>
API Key
</button>
<button
onClick={() => {
// setActiveCategory("plugins"); // 暂时禁用
}}
disabled
className={`relative px-4 py-2 text-sm font-medium rounded-lg border transition-colors opacity-50 cursor-not-allowed border-border bg-card text-muted-foreground`}
data-testid="plugins-category-tab"
title="功能开发中"
>
OAuth 插件
<span className="absolute -top-1.5 -right-1.5 px-1 py-0.5 text-[10px] font-medium bg-gray-400 text-white rounded">
开发中
</span>
</button>
<button
onClick={() => {
setActiveCategory("connect");
@@ -502,13 +487,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
</div>
)}
{/* OAuth 插件分类已隐藏 */}
{/* {activeCategory === "plugins" && (
<div className="min-h-[400px]" data-testid="plugins-section">
<OAuthPluginTab />
</div>
)} */}
{/* 模型库分类 */}
{activeCategory === "models" && <ModelRegistryTab />}
-1
View File
@@ -7,5 +7,4 @@ export { VertexAISection } from "./VertexAISection";
export { CodexSection } from "./CodexSection";
export { AmpConfigSection } from "./AmpConfigSection";
export { UsageDisplay } from "./UsageDisplay";
export { OAuthPluginTab } from "./OAuthPluginTab";
export { ModelRegistryTab } from "./ModelRegistryTab";