增加配置转换

This commit is contained in:
2025-08-07 23:46:55 +08:00
parent 5910362683
commit ca56cc83f0
16 changed files with 2837 additions and 2 deletions

View File

@@ -28,6 +28,7 @@ import { AnalyticsConsentBanner } from "@/components/AnalyticsConsent";
import { useAppLifecycle, useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
import { WelcomePage } from "@/components/WelcomePage";
import RelayStationManager from "@/components/RelayStationManager";
type View =
| "welcome"
@@ -41,6 +42,7 @@ type View =
| "agent-execution"
| "agent-run-view"
| "mcp"
| "relay-stations"
| "usage-dashboard"
| "project-settings"
| "tabs"; // New view for tab-based interface
@@ -255,6 +257,11 @@ function AppContent() {
/>
);
case "relay-stations":
return (
<RelayStationManager onBack={() => handleViewChange("welcome")} />
);
case "cc-agents":
return (
<CCAgents

View File

@@ -0,0 +1,691 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useTranslation } from '@/hooks/useTranslation';
import {
RelayStation,
CreateRelayStationRequest,
UpdateRelayStationRequest,
RelayStationAdapter,
AuthMethod,
ConnectionTestResult,
api
} from '@/lib/api';
import {
Plus,
Edit,
Trash2,
Globe,
CheckCircle,
XCircle,
Wifi,
WifiOff,
Server,
ArrowLeft,
Settings,
RefreshCw
} from 'lucide-react';
interface RelayStationManagerProps {
onBack: () => void;
}
const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) => {
const [stations, setStations] = useState<RelayStation[]>([]);
const [loading, setLoading] = useState(false);
const [selectedStation, setSelectedStation] = useState<RelayStation | null>(null);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [connectionTests, setConnectionTests] = useState<Record<string, ConnectionTestResult>>({});
const [testingConnections, setTestingConnections] = useState<Record<string, boolean>>({});
const [togglingEnable, setTogglingEnable] = useState<Record<string, boolean>>({});
const [currentConfig, setCurrentConfig] = useState<Record<string, string | null>>({});
const [loadingConfig, setLoadingConfig] = useState(false);
const { t } = useTranslation();
// 加载中转站列表
const loadStations = async () => {
try {
setLoading(true);
const stationList = await api.relayStationsList();
setStations(stationList);
} catch (error) {
console.error('Failed to load stations:', error);
alert(t('relayStation.loadFailed'));
} finally {
setLoading(false);
}
};
// 加载当前配置状态
const loadCurrentConfig = async () => {
try {
setLoadingConfig(true);
const config = await api.relayStationGetCurrentConfig();
setCurrentConfig(config);
} catch (error) {
console.error('Failed to load current config:', error);
} finally {
setLoadingConfig(false);
}
};
// 手动同步配置
const syncConfig = async () => {
try {
const result = await api.relayStationSyncConfig();
alert(result);
loadCurrentConfig();
} catch (error) {
console.error('Failed to sync config:', error);
alert(t('relayStation.syncFailed'));
}
};
// 测试连接
const testConnection = async (stationId: string) => {
try {
setTestingConnections(prev => ({ ...prev, [stationId]: true }));
const result = await api.relayStationTestConnection(stationId);
setConnectionTests(prev => ({ ...prev, [stationId]: result }));
if (result.success) {
alert(t('relayStation.connectionSuccess'));
} else {
alert(result.message);
}
} catch (error) {
console.error('Connection test failed:', error);
alert(t('relayStation.connectionFailed'));
} finally {
setTestingConnections(prev => ({ ...prev, [stationId]: false }));
}
};
// 删除中转站
const deleteStation = async (stationId: string) => {
if (!confirm(t('relayStation.deleteConfirm'))) return;
try {
await api.relayStationDelete(stationId);
alert(t('relayStation.deleteSuccess'));
loadStations();
} catch (error) {
console.error('Failed to delete station:', error);
alert(t('relayStation.deleteFailed'));
}
};
// 获取适配器类型显示名称
const getAdapterDisplayName = (adapter: RelayStationAdapter): string => {
switch (adapter) {
case 'newapi': return 'NewAPI';
case 'oneapi': return 'OneAPI';
case 'yourapi': return 'YourAPI';
case 'custom': return t('relayStation.custom');
default: return adapter;
}
};
// 切换启用状态
const toggleEnableStatus = async (stationId: string, currentEnabled: boolean) => {
try {
setTogglingEnable(prev => ({ ...prev, [stationId]: true }));
const newEnabled = !currentEnabled;
await api.relayStationToggleEnable(stationId, newEnabled);
alert(newEnabled ? t('relayStation.enabledSuccess') : t('relayStation.disabledSuccess'));
loadStations();
loadCurrentConfig(); // 重新加载配置状态
} catch (error) {
console.error('Failed to toggle enable status:', error);
alert(t('relayStation.toggleEnableFailed'));
} finally {
setTogglingEnable(prev => ({ ...prev, [stationId]: false }));
}
};
// 获取状态样式
const getStatusBadge = (station: RelayStation) => {
const enabled = station.enabled;
const isToggling = togglingEnable[station.id];
return (
<div className="flex items-center gap-2">
<Switch
checked={enabled}
disabled={isToggling}
onCheckedChange={() => toggleEnableStatus(station.id, enabled)}
className="data-[state=checked]:bg-green-500"
/>
{isToggling ? (
<Badge variant="secondary" className="animate-pulse">{t('common.updating')}</Badge>
) : enabled ? (
<Badge variant="default" className="bg-green-500">{t('status.enabled')}</Badge>
) : (
<Badge variant="secondary">{t('status.disabled')}</Badge>
)}
</div>
);
};
useEffect(() => {
loadStations();
loadCurrentConfig();
}, []);
return (
<div className="container mx-auto p-6 space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
{t('app.back')}
</Button>
<div>
<h1 className="text-3xl font-bold">{t('navigation.relayStations')}</h1>
<p className="text-muted-foreground">{t('relayStation.description')}</p>
</div>
</div>
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
{t('relayStation.create')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<CreateStationDialog
onSuccess={() => {
setShowCreateDialog(false);
loadStations();
}}
/>
</DialogContent>
</Dialog>
</div>
{/* 当前配置状态 */}
<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>
</CardHeader>
<CardContent>
<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>
</CardContent>
</Card>
{/* 中转站列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{loading ? (
<div className="col-span-full text-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="mt-2 text-muted-foreground">{t('common.loading')}</p>
</div>
) : stations.length === 0 ? (
<div className="col-span-full text-center py-12">
<Server className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">{t('relayStation.noStations')}</h3>
<p className="text-muted-foreground mb-4">{t('relayStation.noStationsDesc')}</p>
<Button onClick={() => setShowCreateDialog(true)}>
<Plus className="mr-2 h-4 w-4" />
{t('relayStation.createFirst')}
</Button>
</div>
) : (
stations.map((station) => (
<Card key={station.id} className="relative">
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg">{station.name}</CardTitle>
<CardDescription className="mt-1">
{getAdapterDisplayName(station.adapter)}
</CardDescription>
</div>
{getStatusBadge(station)}
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center text-sm text-muted-foreground">
<Globe className="mr-2 h-4 w-4" />
{station.api_url}
</div>
{station.description && (
<p className="text-sm text-muted-foreground">
{station.description}
</p>
)}
{connectionTests[station.id] && (
<div className="flex items-center text-sm">
{connectionTests[station.id].success ? (
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
) : (
<XCircle className="mr-2 h-4 w-4 text-red-500" />
)}
<span>
{connectionTests[station.id].message}
{connectionTests[station.id].response_time && (
<span className="ml-2 text-muted-foreground">
({connectionTests[station.id].response_time}ms)
</span>
)}
</span>
</div>
)}
<div className="flex justify-between">
<Button
variant="outline"
size="sm"
onClick={() => testConnection(station.id)}
disabled={testingConnections[station.id]}
>
{testingConnections[station.id] ? (
<WifiOff className="mr-2 h-4 w-4 animate-spin" />
) : (
<Wifi className="mr-2 h-4 w-4" />
)}
{t('relayStation.testConnection')}
</Button>
<div className="flex space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedStation(station);
setShowEditDialog(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => deleteStation(station.id)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
{/* 编辑对话框 */}
{selectedStation && (
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
<DialogContent className="sm:max-w-[600px]">
<EditStationDialog
station={selectedStation}
onSuccess={() => {
setShowEditDialog(false);
setSelectedStation(null);
loadStations();
}}
onCancel={() => {
setShowEditDialog(false);
setSelectedStation(null);
}}
/>
</DialogContent>
</Dialog>
)}
</div>
);
};
// 创建中转站对话框组件
const CreateStationDialog: React.FC<{
onSuccess: () => void;
}> = ({ onSuccess }) => {
const [formData, setFormData] = useState<CreateRelayStationRequest>({
name: '',
description: '',
api_url: '',
adapter: 'newapi',
auth_method: 'bearer_token',
system_token: '',
user_id: '',
enabled: false, // 默认不启用,需要通过主界面切换
});
const [submitting, setSubmitting] = useState(false);
const { t } = useTranslation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
alert(t('relayStation.nameRequired'));
return;
}
if (!formData.api_url.trim()) {
alert(t('relayStation.apiUrlRequired'));
return;
}
if (!formData.system_token.trim()) {
alert(t('relayStation.tokenRequired'));
return;
}
try {
setSubmitting(true);
await api.relayStationCreate(formData);
alert(t('relayStation.createSuccess'));
onSuccess();
} catch (error) {
console.error('Failed to create station:', error);
alert(t('relayStation.createFailed'));
} finally {
setSubmitting(false);
}
};
return (
<>
<DialogHeader>
<DialogTitle>{t('relayStation.createTitle')}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">{t('relayStation.name')} *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder={t('relayStation.namePlaceholder')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="adapter">{t('relayStation.adapterType')}</Label>
<Select
value={formData.adapter}
onValueChange={(value: RelayStationAdapter) =>
setFormData(prev => ({ ...prev, adapter: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="newapi">NewAPI</SelectItem>
<SelectItem value="oneapi">OneAPI</SelectItem>
<SelectItem value="yourapi">YourAPI</SelectItem>
<SelectItem value="custom">{t('relayStation.custom')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">{t('relayStation.description')}</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder={t('relayStation.descriptionPlaceholder')}
rows={2}
/>
</div>
<div className="space-y-2">
<Label htmlFor="api_url">{t('relayStation.apiUrl')} *</Label>
<Input
id="api_url"
type="url"
value={formData.api_url}
onChange={(e) => setFormData(prev => ({ ...prev, api_url: e.target.value }))}
placeholder="https://api.example.com"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="auth_method">{t('relayStation.authMethod')}</Label>
<Select
value={formData.auth_method}
onValueChange={(value: AuthMethod) =>
setFormData(prev => ({ ...prev, auth_method: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bearer_token">Bearer Token</SelectItem>
<SelectItem value="api_key">API Key</SelectItem>
<SelectItem value="custom">{t('relayStation.custom')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="system_token">{t('relayStation.systemToken')} *</Label>
<Input
id="system_token"
type="password"
value={formData.system_token}
onChange={(e) => setFormData(prev => ({ ...prev, system_token: e.target.value }))}
placeholder={t('relayStation.tokenPlaceholder')}
/>
</div>
</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')}
/>
</div>
)}
<div className="flex items-center space-x-2">
<Switch
id="enabled"
checked={formData.enabled}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, enabled: checked }))
}
/>
<Label htmlFor="enabled">{t('relayStation.enabled')}</Label>
</div>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={() => {}}>
{t('common.cancel')}
</Button>
<Button type="submit" disabled={submitting}>
{submitting && <div className="mr-2 h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>}
{t('common.create')}
</Button>
</div>
</form>
</>
);
};
// 编辑中转站对话框组件
const EditStationDialog: React.FC<{
station: RelayStation;
onSuccess: () => void;
onCancel: () => void;
}> = ({ station, onSuccess, onCancel }) => {
const [formData, setFormData] = useState<UpdateRelayStationRequest>({
id: station.id,
name: station.name,
description: station.description || '',
api_url: station.api_url,
adapter: station.adapter,
auth_method: station.auth_method,
system_token: station.system_token,
user_id: station.user_id || '',
enabled: station.enabled,
});
const [submitting, setSubmitting] = useState(false);
const { t } = useTranslation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
alert(t('relayStation.nameRequired'));
return;
}
try {
setSubmitting(true);
await api.relayStationUpdate(formData);
alert(t('relayStation.updateSuccess'));
onSuccess();
} catch (error) {
console.error('Failed to update station:', error);
alert(t('relayStation.updateFailed'));
} finally {
setSubmitting(false);
}
};
return (
<>
<DialogHeader>
<DialogTitle>{t('relayStation.editTitle')}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* 表单内容与创建对话框相同,但使用 formData 和 setFormData */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">{t('relayStation.name')} *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder={t('relayStation.namePlaceholder')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="adapter">{t('relayStation.adapterType')}</Label>
<Select
value={formData.adapter}
onValueChange={(value: RelayStationAdapter) =>
setFormData(prev => ({ ...prev, adapter: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="newapi">NewAPI</SelectItem>
<SelectItem value="oneapi">OneAPI</SelectItem>
<SelectItem value="yourapi">YourAPI</SelectItem>
<SelectItem value="custom">{t('relayStation.custom')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="api_url">{t('relayStation.apiUrl')} *</Label>
<Input
id="api_url"
type="url"
value={formData.api_url}
onChange={(e) => setFormData(prev => ({ ...prev, api_url: e.target.value }))}
placeholder="https://api.example.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="system_token">{t('relayStation.systemToken')} *</Label>
<Input
id="system_token"
type="password"
value={formData.system_token}
onChange={(e) => setFormData(prev => ({ ...prev, system_token: e.target.value }))}
placeholder={t('relayStation.tokenPlaceholder')}
/>
</div>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={onCancel}>
{t('common.cancel')}
</Button>
<Button type="submit" disabled={submitting}>
{submitting && <div className="mr-2 h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>}
{t('common.save')}
</Button>
</div>
</form>
</>
);
};
export default RelayStationManager;

View File

@@ -1,5 +1,5 @@
import { motion } from "framer-motion";
import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings } from "lucide-react";
import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings, Network } from "lucide-react";
import { useTranslation } from "@/hooks/useTranslation";
import { Button } from "@/components/ui/button";
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
@@ -14,6 +14,15 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
const { t } = useTranslation();
const mainFeatures = [
{
id: "relay-stations",
icon: Network,
title: t("welcome.relayStationManagement"),
subtitle: t("welcome.relayStationManagementDesc"),
color: "text-indigo-500",
bgColor: "bg-indigo-500/10",
view: "relay-stations"
},
{
id: "agents",
icon: Bot,
@@ -101,7 +110,7 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
</motion.div>
{/* Main Feature Cards */}
<div className="grid grid-cols-2 gap-8 mb-12">
<div className="grid grid-cols-3 gap-8 mb-12">
{mainFeatures.map((feature, index) => (
<motion.div
key={feature.id}

View File

@@ -440,6 +440,117 @@ export interface ImportServerResult {
error?: string;
}
// ================================
// Relay Station Types
// ================================
/** 中转站适配器类型 */
export type RelayStationAdapter =
| 'newapi' // NewAPI 兼容平台
| 'oneapi' // OneAPI 兼容平台
| 'yourapi' // YourAPI 特定平台
| 'custom'; // 自定义简单配置
/** 认证方式 */
export type AuthMethod =
| 'bearer_token' // Bearer Token 认证(推荐)
| 'api_key' // API Key 认证
| 'custom'; // 自定义认证方式
/** 中转站配置 */
export interface RelayStation {
id: string; // 唯一标识符
name: string; // 显示名称
description?: string; // 描述信息
api_url: string; // API 基础 URL
adapter: RelayStationAdapter; // 适配器类型
auth_method: AuthMethod; // 认证方式
system_token: string; // 系统令牌
user_id?: string; // 用户 IDNewAPI 必需)
adapter_config?: Record<string, any>; // 适配器特定配置
enabled: boolean; // 启用状态
created_at: number; // 创建时间
updated_at: number; // 更新时间
}
/** 创建中转站请求 */
export interface CreateRelayStationRequest {
name: string;
description?: string;
api_url: string;
adapter: RelayStationAdapter;
auth_method: AuthMethod;
system_token: string;
user_id?: string;
adapter_config?: Record<string, any>;
enabled: boolean;
}
/** 更新中转站请求 */
export interface UpdateRelayStationRequest {
id: string;
name: string;
description?: string;
api_url: string;
adapter: RelayStationAdapter;
auth_method: AuthMethod;
system_token: string;
user_id?: string;
adapter_config?: Record<string, any>;
enabled: boolean;
}
/** 站点信息 */
export interface StationInfo {
name: string; // 站点名称
announcement?: string; // 公告信息
api_url: string; // API 地址
version?: string; // 版本信息
metadata?: Record<string, any>; // 扩展元数据
quota_per_unit?: number; // 单位配额(用于价格转换)
}
/** 用户信息 */
export interface UserInfo {
user_id: string; // 用户 ID
username?: string; // 用户名
email?: string; // 邮箱
balance_remaining?: number; // 剩余余额(美元)
amount_used?: number; // 已用金额(美元)
request_count?: number; // 请求次数
status?: string; // 账户状态
metadata?: Record<string, any>; // 原始数据
}
/** 连接测试结果 */
export interface ConnectionTestResult {
success: boolean; // 连接是否成功
response_time?: number; // 响应时间(毫秒)
message: string; // 结果消息
error?: string; // 错误信息
}
/** Token 信息 */
export interface TokenInfo {
id: string;
name: string;
token: string;
quota?: number;
used_quota?: number;
status: string;
created_at: number;
updated_at: number;
}
/** Token 分页响应 */
export interface TokenPaginationResponse {
tokens: TokenInfo[];
total: number;
page: number;
size: number;
has_more: boolean;
}
/**
* API client for interacting with the Rust backend
*/
@@ -1923,5 +2034,274 @@ export const api = {
console.error("Failed to get supported languages:", error);
throw error;
}
},
// ================================
// Relay Stations
// ================================
/**
* Lists all relay stations
* @returns Promise resolving to array of relay stations
*/
async relayStationsList(): Promise<RelayStation[]> {
try {
return await invoke<RelayStation[]>("relay_stations_list");
} catch (error) {
console.error("Failed to list relay stations:", error);
throw error;
}
},
/**
* Gets a single relay station by ID
* @param id - The relay station ID
* @returns Promise resolving to the relay station
*/
async relayStationGet(id: string): Promise<RelayStation> {
try {
return await invoke<RelayStation>("relay_station_get", { id });
} catch (error) {
console.error("Failed to get relay station:", error);
throw error;
}
},
/**
* Creates a new relay station
* @param request - The relay station creation request
* @returns Promise resolving to the created relay station
*/
async relayStationCreate(request: CreateRelayStationRequest): Promise<RelayStation> {
try {
return await invoke<RelayStation>("relay_station_create", { request });
} catch (error) {
console.error("Failed to create relay station:", error);
throw error;
}
},
/**
* Updates an existing relay station
* @param request - The relay station update request
* @returns Promise resolving to the updated relay station
*/
async relayStationUpdate(request: UpdateRelayStationRequest): Promise<RelayStation> {
try {
return await invoke<RelayStation>("relay_station_update", { request });
} catch (error) {
console.error("Failed to update relay station:", error);
throw error;
}
},
/**
* Deletes a relay station
* @param id - The relay station ID
* @returns Promise resolving to success message
*/
async relayStationDelete(id: string): Promise<string> {
try {
return await invoke<string>("relay_station_delete", { id });
} catch (error) {
console.error("Failed to delete relay station:", error);
throw error;
}
},
/**
* Toggles relay station enable status (ensures only one station is enabled)
* @param id - The relay station ID
* @param enabled - Whether to enable or disable the station
* @returns Promise resolving to success message
*/
async relayStationToggleEnable(id: string, enabled: boolean): Promise<string> {
try {
return await invoke<string>("relay_station_toggle_enable", { id, enabled });
} catch (error) {
console.error("Failed to toggle relay station enable status:", error);
throw error;
}
},
/**
* Syncs relay station config to Claude settings.json
* @returns Promise resolving to sync result message
*/
async relayStationSyncConfig(): Promise<string> {
try {
return await invoke<string>("relay_station_sync_config");
} catch (error) {
console.error("Failed to sync relay station config:", error);
throw error;
}
},
/**
* Restores Claude config from backup
* @returns Promise resolving to restore result message
*/
async relayStationRestoreConfig(): Promise<string> {
try {
return await invoke<string>("relay_station_restore_config");
} catch (error) {
console.error("Failed to restore config:", error);
throw error;
}
},
/**
* Gets current API config from Claude settings
* @returns Promise resolving to current config info
*/
async relayStationGetCurrentConfig(): Promise<Record<string, string | null>> {
try {
return await invoke<Record<string, string | null>>("relay_station_get_current_config");
} catch (error) {
console.error("Failed to get current config:", error);
throw error;
}
},
/**
* Gets relay station information
* @param stationId - The relay station ID
* @returns Promise resolving to station information
*/
async relayStationGetInfo(stationId: string): Promise<StationInfo> {
try {
return await invoke<StationInfo>("relay_station_get_info", { stationId });
} catch (error) {
console.error("Failed to get station info:", error);
throw error;
}
},
/**
* Gets user information from relay station
* @param stationId - The relay station ID
* @param userId - The user ID
* @returns Promise resolving to user information
*/
async relayStationGetUserInfo(stationId: string, userId: string): Promise<UserInfo> {
try {
return await invoke<UserInfo>("relay_station_get_user_info", { stationId, userId });
} catch (error) {
console.error("Failed to get user info:", error);
throw error;
}
},
/**
* Tests relay station connection
* @param stationId - The relay station ID
* @returns Promise resolving to connection test result
*/
async relayStationTestConnection(stationId: string): Promise<ConnectionTestResult> {
try {
return await invoke<ConnectionTestResult>("relay_station_test_connection", { stationId });
} catch (error) {
console.error("Failed to test connection:", error);
throw error;
}
},
/**
* Gets usage logs from relay station
* @param stationId - The relay station ID
* @param userId - The user ID
* @param page - Page number (optional)
* @param size - Page size (optional)
* @returns Promise resolving to usage logs
*/
async relayStationGetUsageLogs(
stationId: string,
userId: string,
page?: number,
size?: number
): Promise<any> {
try {
return await invoke<any>("relay_station_get_usage_logs", { stationId, userId, page, size });
} catch (error) {
console.error("Failed to get usage logs:", error);
throw error;
}
},
/**
* Lists tokens from relay station
* @param stationId - The relay station ID
* @param page - Page number (optional)
* @param size - Page size (optional)
* @returns Promise resolving to token pagination response
*/
async relayStationListTokens(
stationId: string,
page?: number,
size?: number
): Promise<TokenPaginationResponse> {
try {
return await invoke<TokenPaginationResponse>("relay_station_list_tokens", { stationId, page, size });
} catch (error) {
console.error("Failed to list tokens:", error);
throw error;
}
},
/**
* Creates a new token on relay station
* @param stationId - The relay station ID
* @param name - Token name
* @param quota - Token quota (optional)
* @returns Promise resolving to created token info
*/
async relayStationCreateToken(
stationId: string,
name: string,
quota?: number
): Promise<TokenInfo> {
try {
return await invoke<TokenInfo>("relay_station_create_token", { stationId, name, quota });
} catch (error) {
console.error("Failed to create token:", error);
throw error;
}
},
/**
* Updates a token on relay station
* @param stationId - The relay station ID
* @param tokenId - The token ID
* @param name - New token name (optional)
* @param quota - New token quota (optional)
* @returns Promise resolving to updated token info
*/
async relayStationUpdateToken(
stationId: string,
tokenId: string,
name?: string,
quota?: number
): Promise<TokenInfo> {
try {
return await invoke<TokenInfo>("relay_station_update_token", { stationId, tokenId, name, quota });
} catch (error) {
console.error("Failed to update token:", error);
throw error;
}
},
/**
* Deletes a token from relay station
* @param stationId - The relay station ID
* @param tokenId - The token ID
* @returns Promise resolving to success message
*/
async relayStationDeleteToken(stationId: string, tokenId: string): Promise<string> {
try {
return await invoke<string>("relay_station_delete_token", { stationId, tokenId });
} catch (error) {
console.error("Failed to delete token:", error);
throw error;
}
}
};

View File

@@ -42,9 +42,12 @@
"settings": "Settings",
"usage": "Usage Dashboard",
"mcp": "MCP Manager",
"relayStations": "Relay Stations",
"about": "About"
},
"welcome": {
"relayStationManagement": "Relay Stations",
"relayStationManagementDesc": "Manage Claude API relay stations",
"agentManagement": "Agent Management",
"agentManagementDesc": "Create and manage intelligent agents",
"projectManagement": "Project Management",
@@ -705,5 +708,77 @@
"input": {
"pressEnterToSend": "Press Enter to send, Shift+Enter for new line",
"withFileAndCommandSupport": ", @ to mention files, / for commands, drag & drop or paste images"
},
"relayStation": {
"title": "Relay Stations",
"description": "Manage Claude API relay stations for network access",
"create": "Add Station",
"createTitle": "Create Relay Station",
"createFirst": "Create First Station",
"edit": "Edit Station",
"editTitle": "Edit Relay Station",
"delete": "Delete Station",
"deleteConfirm": "Are you sure you want to delete this relay station?",
"noStations": "No relay stations configured",
"noStationsDesc": "Add a relay station to access Claude API through proxy services",
"name": "Station Name",
"namePlaceholder": "My Relay Station",
"nameRequired": "Station name is required",
"description": "Description",
"descriptionPlaceholder": "Optional description for this station",
"apiUrl": "API URL",
"apiUrlRequired": "API URL is required",
"adapterType": "Adapter Type",
"authMethod": "Authentication Method",
"systemToken": "System Token",
"tokenPlaceholder": "Enter your API token",
"tokenRequired": "System token is required",
"userId": "User ID",
"userIdPlaceholder": "Required for NewAPI/OneAPI",
"enabled": "Enabled",
"testConnection": "Test Connection",
"connectionSuccess": "Connection successful",
"connectionFailed": "Connection test failed",
"createSuccess": "Relay station created successfully",
"createFailed": "Failed to create relay station",
"updateSuccess": "Relay station updated successfully",
"updateFailed": "Failed to update relay station",
"deleteSuccess": "Relay station deleted successfully",
"deleteFailed": "Failed to delete relay station",
"loadFailed": "Failed to load relay stations",
"custom": "Custom",
"enabledSuccess": "Relay station enabled successfully",
"disabledSuccess": "Relay station disabled successfully",
"toggleEnableFailed": "Failed to toggle relay station status",
"syncConfig": "Sync Config",
"syncFailed": "Failed to sync configuration",
"currentConfig": "Current Configuration",
"notConfigured": "Not configured",
"configLocation": "Config file location"
},
"status": {
"connected": "Connected",
"disconnected": "Disconnected",
"unknown": "Unknown",
"disabled": "Disabled",
"enabled": "Enabled"
},
"common": {
"loading": "Loading...",
"cancel": "Cancel",
"save": "Save",
"create": "Create",
"edit": "Edit",
"delete": "Delete",
"updating": "Updating..."
},
"error": {
"title": "Error"
},
"success": {
"title": "Success"
},
"warning": {
"title": "Warning"
}
}

View File

@@ -39,9 +39,12 @@
"settings": "设置",
"usage": "用量仪表板",
"mcp": "MCP 管理器",
"relayStations": "中转站",
"about": "关于"
},
"welcome": {
"relayStationManagement": "中转站管理",
"relayStationManagementDesc": "管理 Claude API 中转站",
"agentManagement": "Agent 管理",
"agentManagementDesc": "创建和管理智能 Agent",
"projectManagement": "项目管理",
@@ -632,6 +635,78 @@
"input": {
"pressEnterToSend": "按 Enter 发送Shift+Enter 换行",
"withFileAndCommandSupport": "@ 提及文件,/ 调用命令,拖拽或粘贴图片"
},
"relayStation": {
"title": "中转站",
"description": "管理 Claude API 中转站以实现网络访问",
"create": "添加中转站",
"createTitle": "创建中转站",
"createFirst": "创建第一个中转站",
"edit": "编辑中转站",
"editTitle": "编辑中转站",
"delete": "删除中转站",
"deleteConfirm": "确定要删除这个中转站吗?",
"noStations": "未配置中转站",
"noStationsDesc": "添加中转站以通过代理服务访问 Claude API",
"name": "中转站名称",
"namePlaceholder": "我的中转站",
"nameRequired": "中转站名称必填",
"description": "描述",
"descriptionPlaceholder": "此中转站的可选描述",
"apiUrl": "API 地址",
"apiUrlRequired": "API 地址必填",
"adapterType": "适配器类型",
"authMethod": "认证方式",
"systemToken": "系统令牌",
"tokenPlaceholder": "输入您的 API 令牌",
"tokenRequired": "系统令牌必填",
"userId": "用户 ID",
"userIdPlaceholder": "NewAPI/OneAPI 必需",
"enabled": "启用",
"testConnection": "测试连接",
"connectionSuccess": "连接成功",
"connectionFailed": "连接测试失败",
"createSuccess": "中转站创建成功",
"createFailed": "创建中转站失败",
"updateSuccess": "中转站更新成功",
"updateFailed": "更新中转站失败",
"deleteSuccess": "中转站删除成功",
"deleteFailed": "删除中转站失败",
"loadFailed": "加载中转站失败",
"custom": "自定义",
"enabledSuccess": "中转站启用成功",
"disabledSuccess": "中转站禁用成功",
"toggleEnableFailed": "切换中转站状态失败",
"syncConfig": "同步配置",
"syncFailed": "同步配置失败",
"currentConfig": "当前配置",
"notConfigured": "未配置",
"configLocation": "配置文件位置"
},
"status": {
"connected": "已连接",
"disconnected": "已断开",
"unknown": "未知",
"disabled": "已禁用",
"enabled": "已启用"
},
"common": {
"loading": "加载中...",
"cancel": "取消",
"save": "保存",
"create": "创建",
"edit": "编辑",
"delete": "删除",
"updating": "更新中..."
},
"error": {
"title": "错误"
},
"success": {
"title": "成功"
},
"warning": {
"title": "警告"
}
}