From e8fe999d1636ea2a19e4ad8cbbc060ce2fcc0dba Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 5 Sep 2025 23:51:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=8D=E5=90=8C=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E4=B8=AD=E8=BD=AC=E7=AB=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/relay_adapters.rs | 5 + src-tauri/src/commands/relay_stations.rs | 8 + src/components/RelayStationManager.tsx | 880 ++++++++++++++++++++--- src/lib/api.ts | 4 + src/locales/en/common.json | 6 + src/locales/zh/common.json | 6 + 6 files changed, 798 insertions(+), 111 deletions(-) diff --git a/src-tauri/src/commands/relay_adapters.rs b/src-tauri/src/commands/relay_adapters.rs index 66a6b44..6bcdb6d 100644 --- a/src-tauri/src/commands/relay_adapters.rs +++ b/src-tauri/src/commands/relay_adapters.rs @@ -838,6 +838,11 @@ impl StationAdapter for CustomAdapter { pub fn create_adapter(adapter_type: &RelayStationAdapter) -> Box { match adapter_type { RelayStationAdapter::Packycode => Box::new(PackycodeAdapter), + // DeepSeek、GLM、Qwen、Kimi 都使用类似 NewAPI 的适配器 + RelayStationAdapter::Deepseek => Box::new(NewApiAdapter), + RelayStationAdapter::Glm => Box::new(NewApiAdapter), + RelayStationAdapter::Qwen => Box::new(NewApiAdapter), + RelayStationAdapter::Kimi => Box::new(NewApiAdapter), RelayStationAdapter::Newapi => Box::new(NewApiAdapter), RelayStationAdapter::Oneapi => Box::new(NewApiAdapter), // OneAPI 兼容 NewAPI RelayStationAdapter::Yourapi => Box::new(YourApiAdapter::new()), diff --git a/src-tauri/src/commands/relay_stations.rs b/src-tauri/src/commands/relay_stations.rs index 48f0d47..10b6a96 100644 --- a/src-tauri/src/commands/relay_stations.rs +++ b/src-tauri/src/commands/relay_stations.rs @@ -15,6 +15,10 @@ use crate::claude_config; #[serde(rename_all = "snake_case")] pub enum RelayStationAdapter { Packycode, // PackyCode 平台(放在第一位) + Deepseek, // DeepSeek v3.1 + Glm, // 智谱GLM + Qwen, // 千问Qwen + Kimi, // Kimi k2 Newapi, // NewAPI 兼容平台 Oneapi, // OneAPI 兼容平台 Yourapi, // YourAPI 特定平台 @@ -25,6 +29,10 @@ impl RelayStationAdapter { pub fn as_str(&self) -> &str { match self { RelayStationAdapter::Packycode => "packycode", + RelayStationAdapter::Deepseek => "deepseek", + RelayStationAdapter::Glm => "glm", + RelayStationAdapter::Qwen => "qwen", + RelayStationAdapter::Kimi => "kimi", RelayStationAdapter::Newapi => "newapi", RelayStationAdapter::Oneapi => "oneapi", RelayStationAdapter::Yourapi => "yourapi", diff --git a/src/components/RelayStationManager.tsx b/src/components/RelayStationManager.tsx index 4eeb7fc..1512785 100644 --- a/src/components/RelayStationManager.tsx +++ b/src/components/RelayStationManager.tsx @@ -1,4 +1,6 @@ import React, { useState, useEffect } from 'react'; +import { open } from '@tauri-apps/plugin-shell'; +import MonacoEditor from '@monaco-editor/react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -35,7 +37,12 @@ import { Server, ArrowLeft, Settings, - RefreshCw + RefreshCw, + ExternalLink, + Eye, + Edit3, + Save, + X } from 'lucide-react'; interface RelayStationManagerProps { @@ -53,6 +60,10 @@ const RelayStationManager: React.FC = ({ onBack }) => const [togglingEnable, setTogglingEnable] = useState>({}); const [currentConfig, setCurrentConfig] = useState>({}); const [loadingConfig, setLoadingConfig] = useState(false); + const [jsonConfigView, setJsonConfigView] = useState(false); + const [editingConfig, setEditingConfig] = useState(false); + const [configJson, setConfigJson] = useState(''); + const [savingConfig, setSavingConfig] = useState(false); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); // PackyCode 额度相关状态 @@ -84,10 +95,27 @@ const RelayStationManager: React.FC = ({ onBack }) => const loadCurrentConfig = async () => { try { setLoadingConfig(true); - const config = await api.relayStationGetCurrentConfig(); - setCurrentConfig(config); + // 读取完整的 ~/.claude/settings.json 文件 + const settings = await api.getClaudeSettings(); + + // 保存配置用于简单视图显示 + setCurrentConfig({ + api_url: settings.env?.ANTHROPIC_BASE_URL || '', + api_token: settings.env?.ANTHROPIC_AUTH_TOKEN || '' + }); + + // 格式化完整的JSON字符串 + setConfigJson(JSON.stringify(settings, null, 2)); } catch (error) { console.error('Failed to load current config:', error); + // 如果失败,尝试获取中转站配置 + try { + const config = await api.relayStationGetCurrentConfig(); + setCurrentConfig(config); + setConfigJson(JSON.stringify(config, null, 2)); + } catch (fallbackError) { + console.error('Failed to load fallback config:', fallbackError); + } } finally { setLoadingConfig(false); } @@ -105,6 +133,31 @@ const RelayStationManager: React.FC = ({ onBack }) => } }; + // 保存JSON配置 + const saveJsonConfig = async () => { + try { + setSavingConfig(true); + // 验证JSON格式 + const parsedConfig = JSON.parse(configJson); + + // 保存配置到 ~/.claude/settings.json + await api.saveClaudeSettings(parsedConfig); + + showToast(t('relayStation.configSaved'), "success"); + setEditingConfig(false); + loadCurrentConfig(); + } catch (error) { + if (error instanceof SyntaxError) { + showToast(t('relayStation.invalidJson'), "error"); + } else { + console.error('Failed to save config:', error); + showToast(t('relayStation.saveFailed'), "error"); + } + } finally { + setSavingConfig(false); + } + }; + // 查询 PackyCode 额度 const fetchPackycodeQuota = async (stationId: string) => { @@ -146,6 +199,10 @@ const RelayStationManager: React.FC = ({ onBack }) => const getAdapterDisplayName = (adapter: RelayStationAdapter): string => { switch (adapter) { case 'packycode': return 'PackyCode'; + case 'deepseek': return 'DeepSeek v3.1'; + case 'glm': return '智谱GLM'; + case 'qwen': return '千问Qwen'; + case 'kimi': return 'Kimi k2'; case 'newapi': return 'NewAPI'; case 'oneapi': return 'OneAPI'; case 'yourapi': return 'YourAPI'; @@ -235,7 +292,7 @@ const RelayStationManager: React.FC = ({ onBack }) => {t('relayStation.create')} - + { setShowCreateDialog(false); @@ -274,23 +331,113 @@ const RelayStationManager: React.FC = ({ onBack }) => -
-
- API URL: - - {currentConfig.api_url || t('relayStation.notConfigured')} - + {jsonConfigView ? ( +
+
+
+ +
+
+ {!editingConfig ? ( + + ) : ( + <> + + + + )} +
+
+
+ setConfigJson(value || '')} + options={{ + readOnly: !editingConfig, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + fontSize: 12, + wordWrap: 'on', + formatOnPaste: true, + formatOnType: true, + automaticLayout: true, + }} + /> +
-
- API Token: - - {currentConfig.api_token || t('relayStation.notConfigured')} - + ) : ( +
+
+ {t('relayStation.configPreview')} + +
+
+
+ API URL: + + {currentConfig.api_url || t('relayStation.notConfigured')} + +
+
+ API Token: + + {currentConfig.api_token || t('relayStation.notConfigured')} + +
+
+ {t('relayStation.configLocation')}: ~/.claude/settings.json +
+
-
- {t('relayStation.configLocation')}: ~/.claude/settings.json -
-
+ )} @@ -499,7 +646,6 @@ const RelayStationManager: React.FC = ({ onBack }) => setShowEditDialog(true); }} > - 测试 + + + + + + {/* 第二行:更多适配器 */} + + + + + + + {/* 第三行:其他适配器 */} + + + + + +
+
+ + + {/* 仅在选择 Custom 时显示名称输入框 */} + {formData.adapter === 'custom' && (
- + setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder={t('relayStation.namePlaceholder')} className="w-full" />
- -
- - -
- + )} {formData.adapter === 'packycode' && ( -
+
- +
@@ -751,7 +1103,7 @@ const CreateStationDialog: React.FC<{ @@ -865,7 +1217,7 @@ const CreateStationDialog: React.FC<{ {formData.adapter !== 'packycode' && (
- + - +
+ + {getApiKeyUrl(formData.adapter, packycodeService) && ( + + )} +
- +
+ + {getApiKeyUrl(formData.adapter) && ( + + )} +
)} -
+
-
-
+ {/* 仅在选择 Custom 时显示名称输入框 */} + {formData.adapter === 'custom' && ( +
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder={t('relayStation.namePlaceholder')} + className="w-full" + /> +
+ )} + +
@@ -1030,6 +1430,36 @@ const EditStationDialog: React.FC<{ const { t } = useTranslation(); + // 获取API Key获取地址 + const getApiKeyUrl = (adapter: string, service?: string): string | null => { + switch (adapter) { + case 'deepseek': + return 'https://platform.deepseek.com/api_keys'; + case 'glm': + return 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys'; + case 'qwen': + return 'https://bailian.console.aliyun.com/?tab=model#/api-key'; + case 'kimi': + return 'https://platform.moonshot.cn/console/api-keys'; + case 'packycode': + if (service === 'taxi') { + return 'https://share.packycode.com/api-management'; + } + return 'https://www.packycode.com/api-management'; + default: + return null; + } + }; + + // 打开外部链接 + const openExternalLink = async (url: string) => { + try { + await open(url); + } catch (error) { + console.error('Failed to open URL:', error); + } + }; + // 当适配器改变时更新认证方式和 URL useEffect(() => { if (formData.adapter === 'packycode') { @@ -1057,7 +1487,7 @@ const EditStationDialog: React.FC<{ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!formData.name.trim()) { + if (formData.adapter === 'custom' && !formData.name.trim()) { setFormToast({ message: t('relayStation.nameRequired'), type: "error" }); return; } @@ -1089,50 +1519,230 @@ const EditStationDialog: React.FC<{ {t('relayStation.editTitle')} - +
+
+ +
+ {/* 第一行:主流适配器 */} + + + + + + + {/* 第二行:更多适配器 */} + + + + + + + {/* 第三行:其他适配器 */} + + + + + +
+
+
+ + {/* 仅在选择 Custom 时显示名称输入框 */} + {formData.adapter === 'custom' && (
- + setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder={t('relayStation.namePlaceholder')} className="w-full" />
- -
- - -
-
+ )} {formData.adapter === 'packycode' && ( -
+
- +
@@ -1155,7 +1765,7 @@ const EditStationDialog: React.FC<{ @@ -1262,7 +1872,7 @@ const EditStationDialog: React.FC<{ {formData.adapter !== 'packycode' && (
- + - +
+ + {getApiKeyUrl(formData.adapter, packycodeService) && ( + + )} +
- +
+ + {getApiKeyUrl(formData.adapter) && ( + + )} +
)} -
+
-
-
+ {/* 仅在选择 Custom 时显示名称输入框 */} + {formData.adapter === 'custom' && ( +
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder={t('relayStation.namePlaceholder')} + className="w-full" + /> +
+ )} + +
diff --git a/src/lib/api.ts b/src/lib/api.ts index c57b253..2203578 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -453,6 +453,10 @@ export interface ImportServerResult { /** 中转站适配器类型 */ export type RelayStationAdapter = | 'packycode' // PackyCode 平台(默认) + | 'deepseek' // DeepSeek v3.1 + | 'glm' // 智谱GLM + | 'qwen' // 千问Qwen + | 'kimi' // Kimi k2 | 'newapi' // NewAPI 兼容平台 | 'oneapi' // OneAPI 兼容平台 | 'yourapi' // YourAPI 特定平台 diff --git a/src/locales/en/common.json b/src/locales/en/common.json index cd7eb3d..c7b2bb4 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -863,6 +863,7 @@ "adapterType": "Adapter Type", "authMethod": "Authentication Method", "systemToken": "System Token", + "getApiKey": "Get API Key", "tokenPlaceholder": "Enter your API token", "tokenRequired": "System token is required", "userId": "User ID", @@ -883,6 +884,11 @@ "disabledSuccess": "Relay station disabled successfully", "toggleEnableFailed": "Failed to toggle relay station status", "syncConfig": "Sync Config", + "configPreview": "Config Preview", + "viewJson": "View JSON", + "configSaved": "Config saved", + "invalidJson": "Invalid JSON format", + "saveFailed": "Save failed", "syncFailed": "Failed to sync configuration", "currentConfig": "Current Configuration", "notConfigured": "Not configured", diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index 129e686..dd1d18b 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -790,6 +790,7 @@ "adapterType": "适配器类型", "authMethod": "认证方式", "systemToken": "系统令牌", + "getApiKey": "获取 API Key", "tokenPlaceholder": "输入您的 API 令牌", "tokenRequired": "系统令牌必填", "userId": "用户 ID", @@ -810,6 +811,11 @@ "disabledSuccess": "中转站禁用成功", "toggleEnableFailed": "切换中转站状态失败", "syncConfig": "同步配置", + "configPreview": "配置预览", + "viewJson": "查看 JSON", + "configSaved": "配置已保存", + "invalidJson": "JSON 格式无效", + "saveFailed": "保存失败", "syncFailed": "同步配置失败", "currentConfig": "当前配置", "notConfigured": "未配置",