mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat(claude-oauth): 添加 Cookie 自动授权功能
- 新增 Cookie 自动授权流程,使用 sessionKey 自动完成 OAuth - 支持三种授权模式:Cookie 授权、OAuth 登录、文件导入 - 参考 claude-relay-service 实现 Closes #38
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"private": true,
|
||||
"version": "0.17.4",
|
||||
"version": "0.17.5",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Generated
+1
-1
@@ -3377,7 +3377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxycast"
|
||||
version = "0.17.4"
|
||||
version = "0.17.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxycast"
|
||||
version = "0.17.4"
|
||||
version = "0.17.5"
|
||||
description = "AI API Proxy Desktop App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::database::DbConnection;
|
||||
use crate::models::Provider;
|
||||
use crate::models::{AppType, Provider};
|
||||
use crate::services::live_sync::{check_config_sync, sync_from_external, SyncCheckResult};
|
||||
use crate::services::switch::SwitchService;
|
||||
use serde_json::Value;
|
||||
use tauri::State;
|
||||
@@ -63,3 +64,48 @@ pub fn import_default_config(
|
||||
pub fn read_live_provider_settings(app_type: String) -> Result<Value, String> {
|
||||
SwitchService::read_live_settings(&app_type)
|
||||
}
|
||||
|
||||
/// 检查配置同步状态
|
||||
#[tauri::command]
|
||||
pub fn check_config_sync_status(
|
||||
db: State<'_, DbConnection>,
|
||||
app_type: String,
|
||||
) -> Result<SyncCheckResult, String> {
|
||||
// 解析 app_type
|
||||
let app_type_enum: AppType = app_type
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid app type: {}", e))?;
|
||||
|
||||
// 获取当前 ProxyCast 中设置的 provider
|
||||
let current_provider = SwitchService::get_current_provider(&db, &app_type)?
|
||||
.map(|p| p.id)
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// 检查同步状态
|
||||
check_config_sync(&app_type_enum, ¤t_provider)
|
||||
.map_err(|e| format!("Failed to check config sync: {}", e))
|
||||
}
|
||||
|
||||
/// 从外部配置同步到 ProxyCast
|
||||
#[tauri::command]
|
||||
pub fn sync_from_external_config(
|
||||
db: State<'_, DbConnection>,
|
||||
app_type: String,
|
||||
) -> Result<String, String> {
|
||||
// 解析 app_type
|
||||
let app_type_enum: AppType = app_type
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid app type: {}", e))?;
|
||||
|
||||
// 从外部配置获取 provider
|
||||
let external_provider = sync_from_external(&app_type_enum)
|
||||
.map_err(|e| format!("Failed to sync from external: {}", e))?;
|
||||
|
||||
// 切换到外部检测到的 provider
|
||||
SwitchService::switch_provider(&db, &app_type, &external_provider)?;
|
||||
|
||||
Ok(format!(
|
||||
"已同步到外部配置的 provider: {}",
|
||||
external_provider
|
||||
))
|
||||
}
|
||||
|
||||
+52
-4
@@ -1751,15 +1751,61 @@ pub fn run() {
|
||||
let shared_flow_monitor = flow_monitor_clone.clone();
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 先加载凭证
|
||||
// 先加载凭证池中的凭证
|
||||
{
|
||||
logs.write().await.add("info", "[启动] 正在加载凭证池...");
|
||||
|
||||
// 获取凭证池概览信息
|
||||
match pool_service.get_overview(&db) {
|
||||
Ok(overview) => {
|
||||
let mut loaded_types = Vec::new();
|
||||
let mut total_credentials = 0;
|
||||
|
||||
for provider_overview in overview {
|
||||
let count = provider_overview.stats.total_count;
|
||||
if count > 0 {
|
||||
total_credentials += count;
|
||||
let provider_name =
|
||||
match provider_overview.provider_type.as_str() {
|
||||
"kiro" => "Kiro",
|
||||
"gemini" => "Gemini",
|
||||
"qwen" => "通义千问",
|
||||
"antigravity" => "Antigravity",
|
||||
"openai" => "OpenAI",
|
||||
"claude" => "Claude",
|
||||
"codex" => "Codex",
|
||||
"claude_oauth" => "Claude OAuth",
|
||||
"iflow" => "iFlow",
|
||||
_ => &provider_overview.provider_type,
|
||||
};
|
||||
loaded_types.push(format!("{} ({} 个)", provider_name, count));
|
||||
}
|
||||
}
|
||||
|
||||
if loaded_types.is_empty() {
|
||||
logs.write().await.add("warn", "[启动] 未找到任何可用凭证");
|
||||
} else {
|
||||
let message = format!(
|
||||
"[启动] 凭证已加载: {} (共 {} 个)",
|
||||
loaded_types.join(", "),
|
||||
total_credentials
|
||||
);
|
||||
logs.write().await.add("info", &message);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logs.write()
|
||||
.await
|
||||
.add("warn", &format!("[启动] 获取凭证池信息失败: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容性:仍然尝试加载旧的 Kiro 凭证(如果存在)
|
||||
let mut s = state.write().await;
|
||||
if let Err(e) = s.kiro_provider.load_credentials().await {
|
||||
logs.write()
|
||||
.await
|
||||
.add("warn", &format!("[启动] 加载 Kiro 凭证失败: {e}"));
|
||||
} else {
|
||||
logs.write().await.add("info", "[启动] Kiro 凭证已加载");
|
||||
.add("debug", &format!("[启动] 旧版 Kiro 凭证加载失败: {e}"));
|
||||
}
|
||||
}
|
||||
// 启动服务器(使用共享的遥测实例和 Flow Monitor)
|
||||
@@ -1897,6 +1943,8 @@ pub fn run() {
|
||||
commands::switch_cmd::switch_provider,
|
||||
commands::switch_cmd::import_default_config,
|
||||
commands::switch_cmd::read_live_provider_settings,
|
||||
commands::switch_cmd::check_config_sync_status,
|
||||
commands::switch_cmd::sync_from_external_config,
|
||||
// Config commands
|
||||
commands::config_cmd::get_config_status,
|
||||
commands::config_cmd::get_config_dir_path,
|
||||
|
||||
@@ -267,3 +267,169 @@ pub fn read_live_settings(
|
||||
AppType::ProxyCast => Ok(json!({})),
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步状态枚举
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SyncStatus {
|
||||
InSync, // 完全同步
|
||||
OutOfSync, // 有差异但无冲突
|
||||
Conflict, // 有冲突需要用户选择
|
||||
}
|
||||
|
||||
/// 配置冲突信息
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ConfigConflict {
|
||||
pub field: String,
|
||||
pub local_value: String,
|
||||
pub external_value: String,
|
||||
}
|
||||
|
||||
/// 同步检查结果
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SyncCheckResult {
|
||||
pub status: SyncStatus,
|
||||
pub current_provider: String,
|
||||
pub external_provider: String,
|
||||
pub last_modified: Option<String>,
|
||||
pub conflicts: Vec<ConfigConflict>,
|
||||
}
|
||||
|
||||
/// 从外部配置文件解析当前生效的 provider
|
||||
pub fn parse_current_provider_from_live(
|
||||
app_type: &AppType,
|
||||
live_settings: &Value,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
match app_type {
|
||||
AppType::Claude => {
|
||||
// 检查 Claude 配置中的认证信息来判断当前 provider
|
||||
if let Some(env) = live_settings.get("env").and_then(|v| v.as_object()) {
|
||||
// 优先检查 ANTHROPIC_AUTH_TOKEN (OAuth)
|
||||
if let Some(token) = env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()) {
|
||||
if !token.is_empty() {
|
||||
return Ok("claude_oauth".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 ANTHROPIC_API_KEY (API Key)
|
||||
if let Some(api_key) = env.get("ANTHROPIC_API_KEY").and_then(|v| v.as_str()) {
|
||||
if !api_key.is_empty() {
|
||||
return Ok("claude".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
AppType::Codex => {
|
||||
// 检查 Codex 认证信息
|
||||
if let Some(auth) = live_settings.get("auth").and_then(|v| v.as_object()) {
|
||||
if auth
|
||||
.get("access_token")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| !s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok("codex".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
AppType::Gemini => {
|
||||
// 检查 Gemini 环境变量
|
||||
if let Some(env) = live_settings.get("env").and_then(|v| v.as_object()) {
|
||||
if let Some(api_key) = env.get("GOOGLE_API_KEY").and_then(|v| v.as_str()) {
|
||||
if !api_key.is_empty() {
|
||||
return Ok("gemini".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
AppType::ProxyCast => Ok("proxycast".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查配置同步状态
|
||||
pub fn check_config_sync(
|
||||
app_type: &AppType,
|
||||
current_provider: &str,
|
||||
) -> Result<SyncCheckResult, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 读取外部配置文件
|
||||
let live_settings = read_live_settings(app_type)?;
|
||||
|
||||
// 解析外部配置中的当前 provider
|
||||
let external_provider = parse_current_provider_from_live(app_type, &live_settings)?;
|
||||
|
||||
// 获取配置文件的修改时间
|
||||
let last_modified = get_config_last_modified(app_type);
|
||||
|
||||
// 比较配置
|
||||
let status = if current_provider == external_provider {
|
||||
SyncStatus::InSync
|
||||
} else if external_provider == "unknown" {
|
||||
SyncStatus::OutOfSync
|
||||
} else {
|
||||
SyncStatus::Conflict
|
||||
};
|
||||
|
||||
// 检测具体的冲突字段
|
||||
let conflicts = if matches!(status, SyncStatus::Conflict) {
|
||||
vec![ConfigConflict {
|
||||
field: "provider".to_string(),
|
||||
local_value: current_provider.to_string(),
|
||||
external_value: external_provider.clone(),
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(SyncCheckResult {
|
||||
status,
|
||||
current_provider: current_provider.to_string(),
|
||||
external_provider,
|
||||
last_modified,
|
||||
conflicts,
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取配置文件的最后修改时间
|
||||
fn get_config_last_modified(app_type: &AppType) -> Option<String> {
|
||||
let home = dirs::home_dir()?;
|
||||
let path = match app_type {
|
||||
AppType::Claude => home.join(".claude").join("settings.json"),
|
||||
AppType::Codex => home.join(".codex").join("auth.json"),
|
||||
AppType::Gemini => home.join(".gemini").join(".env"),
|
||||
AppType::ProxyCast => return None,
|
||||
};
|
||||
|
||||
if let Ok(metadata) = std::fs::metadata(&path) {
|
||||
if let Ok(modified) = metadata.modified() {
|
||||
if let Ok(datetime) = modified.duration_since(std::time::UNIX_EPOCH) {
|
||||
return Some(datetime.as_secs().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// 从外部配置同步到 ProxyCast 数据库
|
||||
/// 这个函数需要与 switch service 集成来更新数据库中的 provider 记录
|
||||
pub fn sync_from_external(
|
||||
app_type: &AppType,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 读取外部配置
|
||||
let live_settings = read_live_settings(app_type)?;
|
||||
|
||||
// 解析当前生效的 provider
|
||||
let external_provider = parse_current_provider_from_live(app_type, &live_settings)?;
|
||||
|
||||
if external_provider == "unknown" {
|
||||
return Err("无法识别外部配置中的 provider".into());
|
||||
}
|
||||
|
||||
// 返回检测到的 provider,由调用方负责更新数据库
|
||||
Ok(external_provider)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ProxyCast",
|
||||
"version": "0.17.4",
|
||||
"version": "0.17.5",
|
||||
"identifier": "com.proxycast.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { useState } from "react";
|
||||
import { AlertTriangle, CheckCircle, XCircle, RefreshCw } from "lucide-react";
|
||||
import { SyncCheckResult, SyncStatus } from "@/lib/api/switch";
|
||||
|
||||
interface ConfigSyncDialogProps {
|
||||
isOpen: boolean;
|
||||
syncResult: SyncCheckResult | null;
|
||||
onClose: () => void;
|
||||
onSyncFromExternal: () => Promise<void>;
|
||||
onRefreshCheck: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function ConfigSyncDialog({
|
||||
isOpen,
|
||||
syncResult,
|
||||
onClose,
|
||||
onSyncFromExternal,
|
||||
onRefreshCheck,
|
||||
}: ConfigSyncDialogProps) {
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [checking, setChecking] = useState(false);
|
||||
|
||||
if (!isOpen || !syncResult) return null;
|
||||
|
||||
const getSyncStatusIcon = (status: SyncStatus) => {
|
||||
switch (status) {
|
||||
case "InSync":
|
||||
return <CheckCircle className="h-5 w-5 text-green-500" />;
|
||||
case "OutOfSync":
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
|
||||
case "Conflict":
|
||||
return <XCircle className="h-5 w-5 text-red-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getSyncStatusText = (status: SyncStatus) => {
|
||||
switch (status) {
|
||||
case "InSync":
|
||||
return "配置已同步";
|
||||
case "OutOfSync":
|
||||
return "配置有差异";
|
||||
case "Conflict":
|
||||
return "配置冲突";
|
||||
}
|
||||
};
|
||||
|
||||
const getSyncStatusColor = (status: SyncStatus) => {
|
||||
switch (status) {
|
||||
case "InSync":
|
||||
return "text-green-700 bg-green-50 border-green-200";
|
||||
case "OutOfSync":
|
||||
return "text-yellow-700 bg-yellow-50 border-yellow-200";
|
||||
case "Conflict":
|
||||
return "text-red-700 bg-red-50 border-red-200";
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncFromExternal = async () => {
|
||||
setSyncing(true);
|
||||
try {
|
||||
await onSyncFromExternal();
|
||||
onClose();
|
||||
} catch (_e) {
|
||||
// Error is handled in the hook
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshCheck = async () => {
|
||||
setChecking(true);
|
||||
try {
|
||||
await onRefreshCheck();
|
||||
} catch (_e) {
|
||||
// Error is handled in the hook
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-md w-full mx-4">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{getSyncStatusIcon(syncResult.status)}
|
||||
<h3 className="text-lg font-semibold">配置同步状态</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-lg border p-4 mb-4 ${getSyncStatusColor(syncResult.status)}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{getSyncStatusIcon(syncResult.status)}
|
||||
<span className="font-medium">
|
||||
{getSyncStatusText(syncResult.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">ProxyCast 当前配置:</span>{" "}
|
||||
{syncResult.current_provider}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">外部软件当前配置:</span>{" "}
|
||||
{syncResult.external_provider}
|
||||
</div>
|
||||
{syncResult.last_modified && (
|
||||
<div>
|
||||
<span className="font-medium">配置文件修改时间:</span>{" "}
|
||||
{new Date(
|
||||
parseInt(syncResult.last_modified) * 1000,
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{syncResult.conflicts.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-current/20">
|
||||
<div className="font-medium mb-2">冲突详情:</div>
|
||||
{syncResult.conflicts.map((conflict, index) => (
|
||||
<div key={index} className="text-sm space-y-1">
|
||||
<div>字段: {conflict.field}</div>
|
||||
<div>ProxyCast: {conflict.local_value}</div>
|
||||
<div>外部软件: {conflict.external_value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{syncResult.status !== "InSync" && (
|
||||
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-400">
|
||||
{syncResult.status === "OutOfSync" && (
|
||||
<>
|
||||
检测到外部软件的配置与 ProxyCast
|
||||
不同。您可以选择同步外部配置到 ProxyCast。
|
||||
</>
|
||||
)}
|
||||
{syncResult.status === "Conflict" && (
|
||||
<>
|
||||
检测到配置冲突。建议选择使用外部软件的配置,或者手动在
|
||||
ProxyCast 中重新设置。
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
onClick={handleRefreshCheck}
|
||||
disabled={checking || syncing}
|
||||
className="px-3 py-2 text-sm border rounded-lg hover:bg-muted disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${checking ? "animate-spin" : ""}`}
|
||||
/>
|
||||
重新检查
|
||||
</button>
|
||||
|
||||
{syncResult.status !== "InSync" && (
|
||||
<button
|
||||
onClick={handleSyncFromExternal}
|
||||
disabled={syncing || checking}
|
||||
className="px-3 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{syncing && <RefreshCw className="h-4 w-4 animate-spin" />}
|
||||
使用外部配置
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={syncing || checking}
|
||||
className="px-3 py-2 text-sm border rounded-lg hover:bg-muted disabled:opacity-50"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { Plus, RefreshCw, Eye } from "lucide-react";
|
||||
import { AppType } from "@/lib/api/switch";
|
||||
import { Plus, RefreshCw, Eye, GitCompare } from "lucide-react";
|
||||
import { AppType, SyncCheckResult } from "@/lib/api/switch";
|
||||
import { useSwitch } from "@/hooks/useSwitch";
|
||||
import { ProviderCard } from "./ProviderCard";
|
||||
import { ProviderForm } from "./ProviderForm";
|
||||
import { LiveConfigModal } from "./LiveConfigModal";
|
||||
import { ConfigSyncDialog } from "./ConfigSyncDialog";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
|
||||
interface ProviderListProps {
|
||||
@@ -22,6 +23,8 @@ export function ProviderList({ appType }: ProviderListProps) {
|
||||
deleteProvider,
|
||||
switchToProvider,
|
||||
refresh,
|
||||
checkConfigSync,
|
||||
syncFromExternal,
|
||||
} = useSwitch(appType);
|
||||
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@@ -30,6 +33,9 @@ export function ProviderList({ appType }: ProviderListProps) {
|
||||
>(null);
|
||||
const [showLiveConfig, setShowLiveConfig] = useState(false);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||
const [showSyncDialog, setShowSyncDialog] = useState(false);
|
||||
const [syncResult, setSyncResult] = useState<SyncCheckResult | null>(null);
|
||||
const [checkingSync, setCheckingSync] = useState(false);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingProvider(null);
|
||||
@@ -71,6 +77,31 @@ export function ProviderList({ appType }: ProviderListProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckSync = async () => {
|
||||
setCheckingSync(true);
|
||||
try {
|
||||
const result = await checkConfigSync();
|
||||
setSyncResult(result);
|
||||
setShowSyncDialog(true);
|
||||
} catch (_e) {
|
||||
// Error is handled in the hook
|
||||
} finally {
|
||||
setCheckingSync(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncFromExternal = async () => {
|
||||
await syncFromExternal();
|
||||
// 重新检查同步状态
|
||||
const result = await checkConfigSync();
|
||||
setSyncResult(result);
|
||||
};
|
||||
|
||||
const handleRefreshSyncCheck = async () => {
|
||||
const result = await checkConfigSync();
|
||||
setSyncResult(result);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
@@ -109,6 +140,16 @@ export function ProviderList({ appType }: ProviderListProps) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleCheckSync}
|
||||
disabled={checkingSync}
|
||||
className="p-2 rounded-lg hover:bg-muted"
|
||||
title="检查外部配置同步状态"
|
||||
>
|
||||
<GitCompare
|
||||
className={`h-4 w-4 ${checkingSync ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="p-2 rounded-lg hover:bg-muted"
|
||||
@@ -172,6 +213,14 @@ export function ProviderList({ appType }: ProviderListProps) {
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
|
||||
<ConfigSyncDialog
|
||||
isOpen={showSyncDialog}
|
||||
syncResult={syncResult}
|
||||
onClose={() => setShowSyncDialog(false)}
|
||||
onSyncFromExternal={handleSyncFromExternal}
|
||||
onRefreshCheck={handleRefreshSyncCheck}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+31
-1
@@ -1,6 +1,11 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { switchApi, Provider, AppType } from "@/lib/api/switch";
|
||||
import {
|
||||
switchApi,
|
||||
Provider,
|
||||
AppType,
|
||||
SyncCheckResult,
|
||||
} from "@/lib/api/switch";
|
||||
|
||||
export function useSwitch(appType: AppType) {
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
@@ -62,6 +67,29 @@ export function useSwitch(appType: AppType) {
|
||||
toast.success("切换成功");
|
||||
};
|
||||
|
||||
const checkConfigSync = async (): Promise<SyncCheckResult> => {
|
||||
try {
|
||||
const result = await switchApi.checkConfigSync(appType);
|
||||
return result;
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error("检查同步状态失败: " + message);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const syncFromExternal = async (): Promise<void> => {
|
||||
try {
|
||||
const message = await switchApi.syncFromExternal(appType);
|
||||
await fetchProviders(); // 刷新数据
|
||||
toast.success(message);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error("同步失败: " + message);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
providers,
|
||||
currentProvider,
|
||||
@@ -72,5 +100,7 @@ export function useSwitch(appType: AppType) {
|
||||
deleteProvider,
|
||||
switchToProvider,
|
||||
refresh: fetchProviders,
|
||||
checkConfigSync,
|
||||
syncFromExternal,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,25 @@ export interface Provider {
|
||||
// proxycast 保留用于内部配置存储,但不在 UI 的 Tab 中显示
|
||||
export type AppType = "claude" | "codex" | "gemini" | "proxycast";
|
||||
|
||||
// 同步状态枚举
|
||||
export type SyncStatus = "InSync" | "OutOfSync" | "Conflict";
|
||||
|
||||
// 配置冲突信息
|
||||
export interface ConfigConflict {
|
||||
field: string;
|
||||
local_value: string;
|
||||
external_value: string;
|
||||
}
|
||||
|
||||
// 同步检查结果
|
||||
export interface SyncCheckResult {
|
||||
status: SyncStatus;
|
||||
current_provider: string;
|
||||
external_provider: string;
|
||||
last_modified?: string;
|
||||
conflicts: ConfigConflict[];
|
||||
}
|
||||
|
||||
export const switchApi = {
|
||||
getProviders: (appType: AppType): Promise<Provider[]> =>
|
||||
invoke("get_switch_providers", { appType }),
|
||||
@@ -39,4 +58,12 @@ export const switchApi = {
|
||||
/** 读取当前生效的配置(从实际配置文件读取) */
|
||||
readLiveSettings: (appType: AppType): Promise<Record<string, unknown>> =>
|
||||
invoke("read_live_provider_settings", { appType }),
|
||||
|
||||
/** 检查配置同步状态 */
|
||||
checkConfigSync: (appType: AppType): Promise<SyncCheckResult> =>
|
||||
invoke("check_config_sync_status", { appType }),
|
||||
|
||||
/** 从外部配置同步到 ProxyCast */
|
||||
syncFromExternal: (appType: AppType): Promise<string> =>
|
||||
invoke("sync_from_external_config", { appType }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user