修改中转站

This commit is contained in:
2025-09-06 17:35:03 +08:00
parent a91b3ebbb2
commit 21c0a9e583
7 changed files with 421 additions and 1179 deletions

View File

@@ -204,15 +204,6 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin
"packycode" => {
// PackyCode 使用原始配置,不做特殊处理
}
"newapi" | "oneapi" => {
// NewAPI 和 OneAPI 兼容 OpenAI 格式,不需要特殊处理
}
"yourapi" => {
// YourAPI 可能需要特殊的路径格式
if !station.api_url.ends_with("/v1") {
config.env.anthropic_base_url = Some(format!("{}/v1", station.api_url));
}
}
"custom" => {
// 自定义适配器,使用原始配置
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,6 @@ pub enum RelayStationAdapter {
Glm, // 智谱GLM
Qwen, // 千问Qwen
Kimi, // Kimi k2
Newapi, // NewAPI 兼容平台
Oneapi, // OneAPI 兼容平台
Yourapi, // YourAPI 特定平台
Custom, // 自定义简单配置
}
@@ -33,9 +30,6 @@ impl RelayStationAdapter {
RelayStationAdapter::Glm => "glm",
RelayStationAdapter::Qwen => "qwen",
RelayStationAdapter::Kimi => "kimi",
RelayStationAdapter::Newapi => "newapi",
RelayStationAdapter::Oneapi => "oneapi",
RelayStationAdapter::Yourapi => "yourapi",
RelayStationAdapter::Custom => "custom",
}
}
@@ -60,7 +54,7 @@ pub struct RelayStation {
pub adapter: RelayStationAdapter, // 适配器类型
pub auth_method: AuthMethod, // 认证方式
pub system_token: String, // 系统令牌
pub user_id: Option<String>, // 用户 IDNewAPI 必需
pub user_id: Option<String>, // 用户 ID可选
pub adapter_config: Option<HashMap<String, serde_json::Value>>, // 适配器特定配置
pub enabled: bool, // 启用状态
pub created_at: i64, // 创建时间

View File

@@ -73,6 +73,17 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
const { t } = useTranslation();
// Token 脱敏函数
const maskToken = (token: string): string => {
if (!token || token.length <= 8) {
return '*'.repeat(token?.length || 0);
}
const start = token.substring(0, 4);
const end = token.substring(token.length - 4);
const middleLength = Math.max(token.length - 8, 8);
return `${start}${'*'.repeat(middleLength)}${end}`;
};
// 显示Toast
const showToast = (message: string, type: "success" | "error" = "success") => {
setToast({ message, type });
@@ -218,9 +229,6 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
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';
case 'custom': return t('relayStation.custom');
default: return adapter;
}
@@ -322,27 +330,9 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
{/* 当前配置状态 */}
<Card className="border-blue-200 dark:border-blue-900 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader className="pb-3">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<CardTitle className="text-lg">{t('relayStation.currentConfig')}</CardTitle>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
loadCurrentConfig();
syncConfig();
}}
disabled={loadingConfig}
>
{loadingConfig ? (
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-current" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span className="ml-2">{t('relayStation.syncConfig')}</span>
</Button>
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<CardTitle className="text-lg">{t('relayStation.currentConfig')}</CardTitle>
</div>
</CardHeader>
<CardContent>
@@ -422,49 +412,71 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
</div>
</div>
) : (
<div className="space-y-2">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium">{t('relayStation.configPreview')}</span>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setJsonConfigView(true)}
>
<Eye className="h-4 w-4 mr-1" />
{t('relayStation.viewJson')}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleFlushDns}
disabled={flushingDns}
>
{flushingDns ? (
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-current mr-1" />
) : (
<RefreshCw className="h-4 w-4 mr-1" />
)}
{t('relayStation.flushDns')}
</Button>
<div className="flex gap-4">
{/* 左侧数据展示 */}
<div className="flex-1 space-y-2">
<div className="text-sm font-medium mb-2">{t('relayStation.configPreview')}</div>
<div className="space-y-1.5 text-sm">
<div className="flex items-start gap-2">
<span className="text-muted-foreground min-w-[80px]">API URL:</span>
<span className="font-mono text-xs break-all">
{currentConfig.api_url || t('relayStation.notConfigured')}
</span>
</div>
<div className="flex items-start gap-2">
<span className="text-muted-foreground min-w-[80px]">API Token:</span>
<span className="font-mono text-xs">
{currentConfig.api_token ? maskToken(currentConfig.api_token) : t('relayStation.notConfigured')}
</span>
</div>
<div className="text-xs text-muted-foreground mt-2">
{t('relayStation.configLocation')}: ~/.claude/settings.json
</div>
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-start gap-2">
<span className="font-medium text-muted-foreground min-w-[100px]">API URL:</span>
<span className="font-mono text-xs break-all">
{currentConfig.api_url || t('relayStation.notConfigured')}
</span>
</div>
<div className="flex items-start gap-2">
<span className="font-medium text-muted-foreground min-w-[100px]">API Token:</span>
<span className="font-mono text-xs">
{currentConfig.api_token || t('relayStation.notConfigured')}
</span>
</div>
<div className="text-xs text-muted-foreground mt-3">
{t('relayStation.configLocation')}: ~/.claude/settings.json
</div>
{/* 右侧按钮区域 */}
<div className="flex flex-col gap-2 min-w-[100px]">
<Button
variant="outline"
size="sm"
onClick={() => {
loadCurrentConfig();
syncConfig();
}}
disabled={loadingConfig}
className="w-full h-9 justify-start"
>
{loadingConfig ? (
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-current mr-2" />
) : (
<RefreshCw className="h-4 w-4 mr-2" />
)}
<span className="text-xs">{t('relayStation.syncConfig')}</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={handleFlushDns}
disabled={flushingDns}
className="w-full h-9 justify-start"
>
{flushingDns ? (
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-current mr-2" />
) : (
<RefreshCw className="h-4 w-4 mr-2" />
)}
<span className="text-xs">{t('relayStation.flushDns')}</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setJsonConfigView(true)}
className="w-full h-9 justify-start"
>
<Eye className="h-4 w-4 mr-2" />
<span className="text-xs">{t('relayStation.viewJson')}</span>
</Button>
</div>
</div>
)}
@@ -542,7 +554,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
</div>
{quotaData[station.id].plan_expires_at && (
<span className="text-muted-foreground">
: {new Date(quotaData[station.id].plan_expires_at).toLocaleDateString()}
: {new Date(quotaData[station.id].plan_expires_at!).toLocaleDateString()}
</span>
)}
</div>
@@ -561,7 +573,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
<span className="text-muted-foreground">:</span>
<div className="flex items-center gap-2">
{(() => {
const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0);
const daily_spent = Number(quotaData[station.id].daily_spent_usd);
const daily_budget = Number(quotaData[station.id].daily_budget_usd);
return (
<>
@@ -579,14 +591,14 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
<div
className={`h-full transition-all ${
(() => {
const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0);
const daily_spent = Number(quotaData[station.id].daily_spent_usd);
const daily_budget = Number(quotaData[station.id].daily_budget_usd);
return daily_spent / daily_budget > 0.8;
})() ? 'bg-orange-500' : 'bg-green-500'
}`}
style={{ width: `${Math.min(
(() => {
const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0);
const daily_spent = Number(quotaData[station.id].daily_spent_usd);
const daily_budget = Number(quotaData[station.id].daily_budget_usd);
return (daily_spent / daily_budget) * 100;
})(), 100)}%` }}
@@ -600,7 +612,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
<span className="text-muted-foreground">:</span>
<div className="flex items-center gap-2">
{(() => {
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0);
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd);
const monthly_budget = Number(quotaData[station.id].monthly_budget_usd);
return (
<>
@@ -618,14 +630,14 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
<div
className={`h-full transition-all ${
(() => {
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0);
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd);
const monthly_budget = Number(quotaData[station.id].monthly_budget_usd);
return monthly_spent / monthly_budget > 0.8;
})() ? 'bg-orange-500' : 'bg-green-500'
}`}
style={{ width: `${Math.min(
(() => {
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0);
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd);
const monthly_budget = Number(quotaData[station.id].monthly_budget_usd);
return (monthly_spent / monthly_budget) * 100;
})(), 100)}%` }}
@@ -847,7 +859,6 @@ const CreateStationDialog: React.FC<{
const startTime = Date.now();
await fetch(node.url, {
method: 'HEAD',
timeout: 5000,
mode: 'no-cors'
});
const responseTime = Date.now() - startTime;
@@ -898,10 +909,7 @@ const CreateStationDialog: React.FC<{
if (formData.adapter === 'packycode') {
setFormData(prev => ({
...prev,
auth_method: 'api_key', // PackyCode 固定使用 API Key
api_url: packycodeService === 'taxi'
? packycodeTaxiNode
: packycodeNode
auth_method: 'api_key' // PackyCode 固定使用 API Key
}));
} else if (formData.adapter === 'custom') {
setFormData(prev => ({
@@ -914,7 +922,7 @@ const CreateStationDialog: React.FC<{
auth_method: 'bearer_token'
}));
}
}, [formData.adapter, packycodeService, packycodeNode, packycodeTaxiNode]);
}, [formData.adapter]);
// 自动填充中转站名称
const fillStationName = (serviceType: string) => {
@@ -1082,69 +1090,6 @@ const CreateStationDialog: React.FC<{
</div>
</Button>
<Button
type="button"
variant={formData.adapter === 'newapi' ? 'default' : 'outline'}
className={`p-3 h-auto flex flex-col items-center space-y-1 transition-all ${
formData.adapter === 'newapi'
? 'bg-green-600 hover:bg-green-700 text-white border-2 border-green-700'
: 'hover:bg-green-50 dark:hover:bg-green-950 border-2 border-transparent'
}`}
onClick={() => setFormData(prev => ({
...prev,
adapter: 'newapi',
name: 'NewAPI'
}))}
>
<div className="text-xl">🆕</div>
<div className="text-center">
<div className="font-semibold text-sm">NewAPI</div>
<div className="text-xs opacity-80 mt-1"></div>
</div>
</Button>
{/* 第三行:其他适配器 */}
<Button
type="button"
variant={formData.adapter === 'oneapi' ? 'default' : 'outline'}
className={`p-3 h-auto flex flex-col items-center space-y-1 transition-all ${
formData.adapter === 'oneapi'
? 'bg-purple-600 hover:bg-purple-700 text-white border-2 border-purple-700'
: 'hover:bg-purple-50 dark:hover:bg-purple-950 border-2 border-transparent'
}`}
onClick={() => setFormData(prev => ({
...prev,
adapter: 'oneapi',
name: 'OneAPI'
}))}
>
<div className="text-xl">1</div>
<div className="text-center">
<div className="font-semibold text-sm">OneAPI</div>
<div className="text-xs opacity-80 mt-1"></div>
</div>
</Button>
<Button
type="button"
variant={formData.adapter === 'yourapi' ? 'default' : 'outline'}
className={`p-3 h-auto flex flex-col items-center space-y-1 transition-all ${
formData.adapter === 'yourapi'
? 'bg-orange-600 hover:bg-orange-700 text-white border-2 border-orange-700'
: 'hover:bg-orange-50 dark:hover:bg-orange-950 border-2 border-transparent'
}`}
onClick={() => setFormData(prev => ({
...prev,
adapter: 'yourapi',
name: 'YourAPI'
}))}
>
<div className="text-xl">👤</div>
<div className="text-center">
<div className="font-semibold text-sm">YourAPI</div>
<div className="text-xs opacity-80 mt-1"></div>
</div>
</Button>
<Button
type="button"
@@ -1196,6 +1141,7 @@ const CreateStationDialog: React.FC<{
onClick={() => {
setPackycodeService('taxi');
fillStationName('taxi');
setFormData(prev => ({ ...prev, api_url: packycodeTaxiNode }));
}}
>
<div className="text-xl">🚗</div>
@@ -1216,6 +1162,7 @@ const CreateStationDialog: React.FC<{
onClick={() => {
setPackycodeService('bus');
fillStationName('bus');
setFormData(prev => ({ ...prev, api_url: packycodeNode }));
}}
>
<div className="text-xl">🚌</div>
@@ -1243,7 +1190,10 @@ const CreateStationDialog: React.FC<{
<div className="flex-1">
<Select
value={packycodeNode}
onValueChange={(value: string) => setPackycodeNode(value)}
onValueChange={(value: string) => {
setPackycodeNode(value);
setFormData(prev => ({ ...prev, api_url: value }));
}}
>
<SelectTrigger>
<SelectValue placeholder={t('relayStation.selectNode')} />
@@ -1316,7 +1266,10 @@ const CreateStationDialog: React.FC<{
<div className="flex-1">
<Select
value={packycodeTaxiNode}
onValueChange={(value: string) => setPackycodeTaxiNode(value)}
onValueChange={(value: string) => {
setPackycodeTaxiNode(value);
setFormData(prev => ({ ...prev, api_url: value }));
}}
>
<SelectTrigger>
<SelectValue placeholder={t('relayStation.selectNode')} />
@@ -1474,18 +1427,6 @@ const CreateStationDialog: React.FC<{
)}
</div>
{(formData.adapter === 'newapi' || formData.adapter === 'oneapi') && (
<div className="space-y-2">
<Label htmlFor="user_id">{t('relayStation.userId')}</Label>
<Input
id="user_id"
value={formData.user_id}
onChange={(e) => setFormData(prev => ({ ...prev, user_id: e.target.value }))}
placeholder={t('relayStation.userIdPlaceholder')}
className="w-full"
/>
</div>
)}
<div className="flex justify-end space-x-3 pt-3">
@@ -1601,15 +1542,9 @@ const EditStationDialog: React.FC<{
}
return 'https://api.packycode.com';
});
const [packycodeTaxiNode, setPackycodeTaxiNode] = useState<string>(() => {
// 如果是PackyCode滴滴车服务使用当前的API URL
if (station.adapter === 'packycode' && station.api_url.includes('share-api')) {
return station.api_url;
}
return 'https://share-api.packycode.com';
});
// 测速弹出框状态
// 滴滴车服务的节点
const [packycodeTaxiNode, setPackycodeTaxiNode] = useState<string>('https://share-api.packycode.com');
const [showSpeedTestModal, setShowSpeedTestModal] = useState(false);
const [speedTestResults, setSpeedTestResults] = useState<{ url: string; name: string; responseTime: number | null; status: 'testing' | 'success' | 'failed' }[]>([]);
const [speedTestInProgress, setSpeedTestInProgress] = useState(false);
@@ -1669,7 +1604,6 @@ const EditStationDialog: React.FC<{
const startTime = Date.now();
await fetch(node.url, {
method: 'HEAD',
timeout: 5000,
mode: 'no-cors'
});
const responseTime = Date.now() - startTime;
@@ -1720,10 +1654,7 @@ const EditStationDialog: React.FC<{
if (formData.adapter === 'packycode') {
setFormData(prev => ({
...prev,
auth_method: 'api_key', // PackyCode 固定使用 API Key
api_url: packycodeService === 'taxi'
? packycodeTaxiNode
: packycodeNode
auth_method: 'api_key' // PackyCode 固定使用 API Key
}));
} else if (formData.adapter === 'custom') {
setFormData(prev => ({
@@ -1736,7 +1667,7 @@ const EditStationDialog: React.FC<{
auth_method: 'bearer_token'
}));
}
}, [formData.adapter, packycodeService, packycodeNode, packycodeTaxiNode]);
}, [formData.adapter]);
const handleSubmit = async (e: React.FormEvent) => {
@@ -1891,69 +1822,6 @@ const EditStationDialog: React.FC<{
</div>
</Button>
<Button
type="button"
variant={formData.adapter === 'newapi' ? 'default' : 'outline'}
className={`p-3 h-auto flex flex-col items-center space-y-1 transition-all ${
formData.adapter === 'newapi'
? 'bg-green-600 hover:bg-green-700 text-white border-2 border-green-700'
: 'hover:bg-green-50 dark:hover:bg-green-950 border-2 border-transparent'
}`}
onClick={() => setFormData(prev => ({
...prev,
adapter: 'newapi',
name: 'NewAPI'
}))}
>
<div className="text-xl">🆕</div>
<div className="text-center">
<div className="font-semibold text-sm">NewAPI</div>
<div className="text-xs opacity-80 mt-1"></div>
</div>
</Button>
{/* 第三行:其他适配器 */}
<Button
type="button"
variant={formData.adapter === 'oneapi' ? 'default' : 'outline'}
className={`p-3 h-auto flex flex-col items-center space-y-1 transition-all ${
formData.adapter === 'oneapi'
? 'bg-purple-600 hover:bg-purple-700 text-white border-2 border-purple-700'
: 'hover:bg-purple-50 dark:hover:bg-purple-950 border-2 border-transparent'
}`}
onClick={() => setFormData(prev => ({
...prev,
adapter: 'oneapi',
name: 'OneAPI'
}))}
>
<div className="text-xl">1</div>
<div className="text-center">
<div className="font-semibold text-sm">OneAPI</div>
<div className="text-xs opacity-80 mt-1"></div>
</div>
</Button>
<Button
type="button"
variant={formData.adapter === 'yourapi' ? 'default' : 'outline'}
className={`p-3 h-auto flex flex-col items-center space-y-1 transition-all ${
formData.adapter === 'yourapi'
? 'bg-orange-600 hover:bg-orange-700 text-white border-2 border-orange-700'
: 'hover:bg-orange-50 dark:hover:bg-orange-950 border-2 border-transparent'
}`}
onClick={() => setFormData(prev => ({
...prev,
adapter: 'yourapi',
name: 'YourAPI'
}))}
>
<div className="text-xl">👤</div>
<div className="text-center">
<div className="font-semibold text-sm">YourAPI</div>
<div className="text-xs opacity-80 mt-1"></div>
</div>
</Button>
<Button
type="button"
@@ -2006,7 +1874,7 @@ const EditStationDialog: React.FC<{
setPackycodeService('taxi');
setFormData(prev => ({
...prev,
api_url: 'https://share-api.packycode.com'
api_url: packycodeTaxiNode
}));
}}
>
@@ -2027,6 +1895,7 @@ const EditStationDialog: React.FC<{
}`}
onClick={() => {
setPackycodeService('bus');
setFormData(prev => ({ ...prev, api_url: packycodeNode }));
}}
>
<div className="text-xl">🚌</div>
@@ -2227,18 +2096,6 @@ const EditStationDialog: React.FC<{
)}
</div>
{(formData.adapter === 'newapi' || formData.adapter === 'oneapi') && (
<div className="space-y-2">
<Label htmlFor="edit-user_id">{t('relayStation.userId')}</Label>
<Input
id="edit-user_id"
value={formData.user_id}
onChange={(e) => setFormData(prev => ({ ...prev, user_id: e.target.value }))}
placeholder={t('relayStation.userIdPlaceholder')}
className="w-full"
/>
</div>
)}
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
<div className="flex items-center space-x-3">

View File

@@ -457,9 +457,6 @@ export type RelayStationAdapter =
| 'glm' // 智谱GLM
| 'qwen' // 千问Qwen
| 'kimi' // Kimi k2
| 'newapi' // NewAPI 兼容平台
| 'oneapi' // OneAPI 兼容平台
| 'yourapi' // YourAPI 特定平台
| 'custom'; // 自定义简单配置
/** 认证方式 */
@@ -477,7 +474,7 @@ export interface RelayStation {
adapter: RelayStationAdapter; // 适配器类型
auth_method: AuthMethod; // 认证方式
system_token: string; // 系统令牌
user_id?: string; // 用户 IDNewAPI 必需
user_id?: string; // 用户 ID可选
adapter_config?: Record<string, any>; // 适配器特定配置
enabled: boolean; // 启用状态
created_at: number; // 创建时间
@@ -589,17 +586,17 @@ export interface NodeSpeedTestResult {
/** PackyCode 用户额度信息 */
export interface PackycodeUserQuota {
daily_budget_usd: string | number; // 日预算(美元)
daily_spent_usd: string | number | null; // 日已使用(美元)
monthly_budget_usd: string | number; // 月预算(美元)
monthly_spent_usd: string | number | null; // 月已使用(美元)
balance_usd: string | number; // 账户余额(美元)
total_spent_usd: string | number; // 总消费(美元)
plan_type: string; // 计划类型 (pro, basic, etc.)
plan_expires_at: string; // 计划过期时间
username?: string; // 用户名
email?: string; // 邮箱
opus_enabled?: boolean; // 是否启用Opus模型
daily_budget_usd: number; // 日预算(美元)
daily_spent_usd: number; // 日已使用(美元)
monthly_budget_usd: number; // 月预算(美元)
monthly_spent_usd: number; // 月已使用(美元)
balance_usd: number; // 账户余额(美元)
total_spent_usd: number; // 总消费(美元)
plan_type: string; // 计划类型 (pro, basic, etc.)
plan_expires_at?: string; // 计划过期时间
username?: string; // 用户名
email?: string; // 邮箱
opus_enabled?: boolean; // 是否启用Opus模型
}
/**

View File

@@ -867,7 +867,7 @@
"tokenPlaceholder": "Enter your API token",
"tokenRequired": "System token is required",
"userId": "User ID",
"userIdPlaceholder": "Required for NewAPI/OneAPI",
"userIdPlaceholder": "Optional",
"enabled": "Enabled",
"testConnection": "Test Connection",
"connectionSuccess": "Connection successful",

View File

@@ -794,7 +794,7 @@
"tokenPlaceholder": "输入您的 API 令牌",
"tokenRequired": "系统令牌必填",
"userId": "用户 ID",
"userIdPlaceholder": "NewAPI/OneAPI 必需",
"userIdPlaceholder": "可选",
"enabled": "启用",
"testConnection": "测试连接",
"connectionSuccess": "连接成功",