mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
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:
@@ -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;
|
||||
@@ -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 />}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user