import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import useSWR from 'swr' import { api } from '../lib/api' import type { TraderInfo, CreateTraderRequest, AIModel, Exchange, } from '../types' import { useLanguage } from '../contexts/LanguageContext' import { t, type Language } from '../i18n/translations' import { useAuth } from '../contexts/AuthContext' import { getExchangeIcon } from './ExchangeIcons' import { getModelIcon } from './ModelIcons' import { TraderConfigModal } from './TraderConfigModal' import { ExchangeConfigModal } from './traders/ExchangeConfigModal' import { PunkAvatar, getTraderAvatar } from './PunkAvatar' import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users, Pencil, Eye, EyeOff, } from 'lucide-react' import { confirmToast } from '../lib/notify' import { toast } from 'sonner' // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { switch (modelId.toLowerCase()) { case 'deepseek': return 'DeepSeek' case 'qwen': return 'Qwen' case 'claude': return 'Claude' default: return modelId.toUpperCase() } } // 提取下划线后面的名称部分 function getShortName(fullName: string): string { const parts = fullName.split('_') return parts.length > 1 ? parts[parts.length - 1] : fullName } interface AITradersPageProps { onTraderSelect?: (traderId: string) => void } // Helper function to get exchange display name from exchange ID (UUID) function getExchangeDisplayName(exchangeId: string | undefined, exchanges: Exchange[]): string { if (!exchangeId) return 'Unknown' const exchange = exchanges.find(e => e.id === exchangeId) if (!exchange) return exchangeId.substring(0, 8).toUpperCase() + '...' // Show truncated UUID if not found const typeName = exchange.exchange_type?.toUpperCase() || exchange.name return exchange.account_name ? `${typeName} - ${exchange.account_name}` : typeName } export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage() const { user, token } = useAuth() const navigate = useNavigate() const [showCreateModal, setShowCreateModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [showModelModal, setShowModelModal] = useState(false) const [showExchangeModal, setShowExchangeModal] = useState(false) const [editingModel, setEditingModel] = useState(null) const [editingExchange, setEditingExchange] = useState(null) const [editingTrader, setEditingTrader] = useState(null) const [allModels, setAllModels] = useState([]) const [allExchanges, setAllExchanges] = useState([]) const [supportedModels, setSupportedModels] = useState([]) const [supportedExchanges, setSupportedExchanges] = useState([]) const { data: traders, mutate: mutateTraders, isLoading: isTradersLoading } = useSWR( user && token ? 'traders' : null, api.getTraders, { refreshInterval: 5000 } ) // 加载AI模型和交易所配置 useEffect(() => { const loadConfigs = async () => { if (!user || !token) { // 未登录时只加载公开的支持模型和交易所 try { const [supportedModels, supportedExchanges] = await Promise.all([ api.getSupportedModels(), api.getSupportedExchanges(), ]) setSupportedModels(supportedModels) setSupportedExchanges(supportedExchanges) } catch (err) { console.error('Failed to load supported configs:', err) } return } try { const [ modelConfigs, exchangeConfigs, supportedModels, supportedExchanges, ] = await Promise.all([ api.getModelConfigs(), api.getExchangeConfigs(), api.getSupportedModels(), api.getSupportedExchanges(), ]) setAllModels(modelConfigs) setAllExchanges(exchangeConfigs) setSupportedModels(supportedModels) setSupportedExchanges(supportedExchanges) } catch (error) { console.error('Failed to load configs:', error) } } loadConfigs() }, [user, token]) // 只显示已配置的模型和交易所 // 注意:后端返回的数据不包含敏感信息(apiKey等),所以通过其他字段判断是否已配置 const configuredModels = allModels?.filter((m) => { // 如果模型已启用,说明已配置 // 或者有自定义API URL,也说明已配置 return m.enabled || (m.customApiUrl && m.customApiUrl.trim() !== '') }) || [] const configuredExchanges = allExchanges?.filter((e) => { // Aster 交易所检查特殊字段 if (e.id === 'aster') { return e.asterUser && e.asterUser.trim() !== '' } // Hyperliquid 需要检查钱包地址(后端会返回这个字段) if (e.id === 'hyperliquid') { return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '' } // 其他交易所:如果已启用,说明已配置(后端返回的已配置交易所会有 enabled: true) return e.enabled }) || [] // 只在创建交易员时使用已启用且配置完整的 // 注意:后端返回的数据不包含敏感信息,所以只检查 enabled 状态和必要的非敏感字段 const enabledModels = allModels?.filter((m) => m.enabled) || [] const enabledExchanges = allExchanges?.filter((e) => { if (!e.enabled) return false // Aster 交易所需要特殊字段(后端会返回这些非敏感字段) if (e.id === 'aster') { return ( e.asterUser && e.asterUser.trim() !== '' && e.asterSigner && e.asterSigner.trim() !== '' ) } // Hyperliquid 需要钱包地址(后端会返回这个字段) if (e.id === 'hyperliquid') { return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '' } // 其他交易所:如果已启用,说明已配置完整(后端只返回已配置的交易所) return true }) || [] // 检查模型是否正在被运行中的交易员使用(用于UI禁用) const isModelInUse = (modelId: string) => { return traders?.some((t) => t.ai_model === modelId && t.is_running) } // 检查交易所是否正在被运行中的交易员使用(用于UI禁用) const isExchangeInUse = (exchangeId: string) => { return traders?.some((t) => t.exchange_id === exchangeId && t.is_running) } // 检查模型是否被任何交易员使用(包括停止状态的) const isModelUsedByAnyTrader = (modelId: string) => { return traders?.some((t) => t.ai_model === modelId) || false } // 检查交易所是否被任何交易员使用(包括停止状态的) const isExchangeUsedByAnyTrader = (exchangeId: string) => { return traders?.some((t) => t.exchange_id === exchangeId) || false } // 获取使用特定模型的交易员列表 const getTradersUsingModel = (modelId: string) => { return traders?.filter((t) => t.ai_model === modelId) || [] } // 获取使用特定交易所的交易员列表 const getTradersUsingExchange = (exchangeId: string) => { return traders?.filter((t) => t.exchange_id === exchangeId) || [] } const handleCreateTrader = async (data: CreateTraderRequest) => { try { const model = allModels?.find((m) => m.id === data.ai_model_id) const exchange = allExchanges?.find((e) => e.id === data.exchange_id) if (!model?.enabled) { toast.error(t('modelNotConfigured', language)) return } if (!exchange?.enabled) { toast.error(t('exchangeNotConfigured', language)) return } await toast.promise(api.createTrader(data), { loading: '正在创建…', success: '创建成功', error: '创建失败', }) setShowCreateModal(false) // Immediately refresh traders list for better UX await mutateTraders() } catch (error) { console.error('Failed to create trader:', error) toast.error(t('createTraderFailed', language)) } } const handleEditTrader = async (traderId: string) => { try { const traderConfig = await api.getTraderConfig(traderId) setEditingTrader(traderConfig) setShowEditModal(true) } catch (error) { console.error('Failed to fetch trader config:', error) toast.error(t('getTraderConfigFailed', language)) } } const handleSaveEditTrader = async (data: CreateTraderRequest) => { if (!editingTrader) return try { const model = enabledModels?.find((m) => m.id === data.ai_model_id) const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id) if (!model) { toast.error(t('modelConfigNotExist', language)) return } if (!exchange) { toast.error(t('exchangeConfigNotExist', language)) return } const request = { name: data.name, ai_model_id: data.ai_model_id, exchange_id: data.exchange_id, initial_balance: data.initial_balance, scan_interval_minutes: data.scan_interval_minutes, btc_eth_leverage: data.btc_eth_leverage, altcoin_leverage: data.altcoin_leverage, trading_symbols: data.trading_symbols, custom_prompt: data.custom_prompt, override_base_prompt: data.override_base_prompt, system_prompt_template: data.system_prompt_template, is_cross_margin: data.is_cross_margin, use_coin_pool: data.use_coin_pool, use_oi_top: data.use_oi_top, } await toast.promise(api.updateTrader(editingTrader.trader_id, request), { loading: '正在保存…', success: '保存成功', error: '保存失败', }) setShowEditModal(false) setEditingTrader(null) // Immediately refresh traders list for better UX await mutateTraders() } catch (error) { console.error('Failed to update trader:', error) toast.error(t('updateTraderFailed', language)) } } const handleDeleteTrader = async (traderId: string) => { { const ok = await confirmToast(t('confirmDeleteTrader', language)) if (!ok) return } try { await toast.promise(api.deleteTrader(traderId), { loading: '正在删除…', success: '删除成功', error: '删除失败', }) // Immediately refresh traders list for better UX await mutateTraders() } catch (error) { console.error('Failed to delete trader:', error) toast.error(t('deleteTraderFailed', language)) } } const handleToggleTrader = async (traderId: string, running: boolean) => { try { if (running) { await toast.promise(api.stopTrader(traderId), { loading: '正在停止…', success: '已停止', error: '停止失败', }) } else { await toast.promise(api.startTrader(traderId), { loading: '正在启动…', success: '已启动', error: '启动失败', }) } // Immediately refresh traders list to update running status await mutateTraders() } catch (error) { console.error('Failed to toggle trader:', error) toast.error(t('operationFailed', language)) } } const handleToggleCompetition = async (traderId: string, currentShowInCompetition: boolean) => { try { const newValue = !currentShowInCompetition await toast.promise(api.toggleCompetition(traderId, newValue), { loading: '正在更新…', success: newValue ? '已在竞技场显示' : '已在竞技场隐藏', error: '更新失败', }) // Immediately refresh traders list to update status await mutateTraders() } catch (error) { console.error('Failed to toggle competition visibility:', error) toast.error(t('operationFailed', language)) } } const handleModelClick = (modelId: string) => { if (!isModelInUse(modelId)) { setEditingModel(modelId) setShowModelModal(true) } } const handleExchangeClick = (exchangeId: string) => { if (!isExchangeInUse(exchangeId)) { setEditingExchange(exchangeId) setShowExchangeModal(true) } } // 通用删除配置处理函数 const handleDeleteConfig = async (config: { id: string type: 'model' | 'exchange' checkInUse: (id: string) => boolean getUsingTraders: (id: string) => any[] cannotDeleteKey: string confirmDeleteKey: string allItems: T[] | undefined clearFields: (item: T) => T buildRequest: (items: T[]) => any updateApi: (request: any) => Promise refreshApi: () => Promise setItems: (items: T[]) => void closeModal: () => void errorKey: string }) => { // 检查是否有交易员正在使用 if (config.checkInUse(config.id)) { const usingTraders = config.getUsingTraders(config.id) const traderNames = usingTraders.map((t) => t.trader_name).join(', ') toast.error( `${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}` ) return } { const ok = await confirmToast(t(config.confirmDeleteKey, language)) if (!ok) return } try { const updatedItems = config.allItems?.map((item) => item.id === config.id ? config.clearFields(item) : item ) || [] const request = config.buildRequest(updatedItems) await toast.promise(config.updateApi(request), { loading: '正在更新配置…', success: '配置已更新', error: '更新配置失败', }) // 重新获取用户配置以确保数据同步 const refreshedItems = await config.refreshApi() config.setItems(refreshedItems) config.closeModal() } catch (error) { console.error(`Failed to delete ${config.type} config:`, error) toast.error(t(config.errorKey, language)) } } const handleDeleteModelConfig = async (modelId: string) => { await handleDeleteConfig({ id: modelId, type: 'model', checkInUse: isModelUsedByAnyTrader, getUsingTraders: getTradersUsingModel, cannotDeleteKey: 'cannotDeleteModelInUse', confirmDeleteKey: 'confirmDeleteModel', allItems: allModels, clearFields: (m) => ({ ...m, apiKey: '', customApiUrl: '', customModelName: '', enabled: false, }), buildRequest: (models) => ({ models: Object.fromEntries( models.map((model) => [ model.provider, { enabled: model.enabled, api_key: model.apiKey || '', custom_api_url: model.customApiUrl || '', custom_model_name: model.customModelName || '', }, ]) ), }), updateApi: api.updateModelConfigs, refreshApi: api.getModelConfigs, setItems: (items) => { // 使用函数式更新确保状态正确更新 setAllModels([...items]) }, closeModal: () => { setShowModelModal(false) setEditingModel(null) }, errorKey: 'deleteConfigFailed', }) } const handleSaveModelConfig = async ( modelId: string, apiKey: string, customApiUrl?: string, customModelName?: string ) => { try { // 创建或更新用户的模型配置 const existingModel = allModels?.find((m) => m.id === modelId) let updatedModels // 找到要配置的模型(优先从已配置列表,其次从支持列表) const modelToUpdate = existingModel || supportedModels?.find((m) => m.id === modelId) if (!modelToUpdate) { toast.error(t('modelNotExist', language)) return } if (existingModel) { // 更新现有配置 updatedModels = allModels?.map((m) => m.id === modelId ? { ...m, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true, } : m ) || [] } else { // 添加新配置 const newModel = { ...modelToUpdate, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true, } updatedModels = [...(allModels || []), newModel] } const request = { models: Object.fromEntries( updatedModels.map((model) => [ model.provider, // 使用 provider 而不是 id { enabled: model.enabled, api_key: model.apiKey || '', custom_api_url: model.customApiUrl || '', custom_model_name: model.customModelName || '', }, ]) ), } await toast.promise(api.updateModelConfigs(request), { loading: '正在更新模型配置…', success: '模型配置已更新', error: '更新模型配置失败', }) // 重新获取用户配置以确保数据同步 const refreshedModels = await api.getModelConfigs() setAllModels(refreshedModels) setShowModelModal(false) setEditingModel(null) } catch (error) { console.error('Failed to save model config:', error) toast.error(t('saveConfigFailed', language)) } } const handleDeleteExchangeConfig = async (exchangeId: string) => { // 检查是否有trader在使用此交易所账户 if (isExchangeUsedByAnyTrader(exchangeId)) { const tradersUsing = getTradersUsingExchange(exchangeId) toast.error( `${t('cannotDeleteExchangeInUse', language)}: ${tradersUsing.join(', ')}` ) return } // 确认删除 const ok = await confirmToast(t('confirmDeleteExchange', language)) if (!ok) return try { await toast.promise(api.deleteExchange(exchangeId), { loading: language === 'zh' ? '正在删除交易所账户…' : 'Deleting exchange account...', success: language === 'zh' ? '交易所账户已删除' : 'Exchange account deleted', error: language === 'zh' ? '删除交易所账户失败' : 'Failed to delete exchange account', }) // 重新获取用户配置以确保数据同步 const refreshedExchanges = await api.getExchangeConfigs() setAllExchanges(refreshedExchanges) setShowExchangeModal(false) setEditingExchange(null) } catch (error) { console.error('Failed to delete exchange config:', error) toast.error(t('deleteExchangeConfigFailed', language)) } } const handleSaveExchangeConfig = async ( exchangeId: string | null, // null for creating new account exchangeType: string, accountName: string, apiKey: string, secretKey?: string, passphrase?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string, lighterWalletAddr?: string, lighterPrivateKey?: string, lighterApiKeyPrivateKey?: string ) => { try { if (exchangeId) { // 更新现有账户配置 const existingExchange = allExchanges?.find((e) => e.id === exchangeId) if (!existingExchange) { toast.error(t('exchangeNotExist', language)) return } const request = { exchanges: { [exchangeId]: { enabled: true, api_key: apiKey || '', secret_key: secretKey || '', passphrase: passphrase || '', testnet: testnet || false, hyperliquid_wallet_addr: hyperliquidWalletAddr || '', aster_user: asterUser || '', aster_signer: asterSigner || '', aster_private_key: asterPrivateKey || '', lighter_wallet_addr: lighterWalletAddr || '', lighter_private_key: lighterPrivateKey || '', lighter_api_key_private_key: lighterApiKeyPrivateKey || '', }, }, } await toast.promise(api.updateExchangeConfigsEncrypted(request), { loading: language === 'zh' ? '正在更新交易所配置…' : 'Updating exchange config...', success: language === 'zh' ? '交易所配置已更新' : 'Exchange config updated', error: language === 'zh' ? '更新交易所配置失败' : 'Failed to update exchange config', }) } else { // 创建新账户 const createRequest = { exchange_type: exchangeType, account_name: accountName, enabled: true, api_key: apiKey || '', secret_key: secretKey || '', passphrase: passphrase || '', testnet: testnet || false, hyperliquid_wallet_addr: hyperliquidWalletAddr || '', aster_user: asterUser || '', aster_signer: asterSigner || '', aster_private_key: asterPrivateKey || '', lighter_wallet_addr: lighterWalletAddr || '', lighter_private_key: lighterPrivateKey || '', lighter_api_key_private_key: lighterApiKeyPrivateKey || '', } await toast.promise(api.createExchangeEncrypted(createRequest), { loading: language === 'zh' ? '正在创建交易所账户…' : 'Creating exchange account...', success: language === 'zh' ? '交易所账户已创建' : 'Exchange account created', error: language === 'zh' ? '创建交易所账户失败' : 'Failed to create exchange account', }) } // 重新获取用户配置以确保数据同步 const refreshedExchanges = await api.getExchangeConfigs() setAllExchanges(refreshedExchanges) setShowExchangeModal(false) setEditingExchange(null) } catch (error) { console.error('Failed to save exchange config:', error) toast.error(t('saveConfigFailed', language)) } } const handleAddModel = () => { setEditingModel(null) setShowModelModal(true) } const handleAddExchange = () => { setEditingExchange(null) setShowExchangeModal(true) } return (
{/* Header */}

{t('aiTraders', language)} {traders?.length || 0} {t('active', language)}

{t('manageAITraders', language)}

{/* Configuration Status */}
{/* AI Models */}

{t('aiModels', language)}

{configuredModels.map((model) => { const inUse = isModelInUse(model.id) return (
handleModelClick(model.id)} >
{getModelIcon(model.provider || model.id, { width: 28, height: 28, }) || (
{getShortName(model.name)[0]}
)}
{getShortName(model.name)}
{inUse ? t('inUse', language) : model.enabled ? t('enabled', language) : t('configured', language)}
) })} {configuredModels.length === 0 && (
{t('noModelsConfigured', language)}
)}
{/* Exchanges */}

{t('exchanges', language)}

{configuredExchanges.map((exchange) => { const inUse = isExchangeInUse(exchange.id) return (
handleExchangeClick(exchange.id)} >
{getExchangeIcon(exchange.exchange_type || exchange.id, { width: 28, height: 28 })}
{exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)} - {exchange.account_name || 'Default'}
{exchange.type?.toUpperCase() || 'CEX'} •{' '} {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)}
) })} {configuredExchanges.length === 0 && (
{t('noExchangesConfigured', language)}
)}
{/* Traders List */}

{t('currentTraders', language)}

{isTradersLoading ? ( /* Loading Skeleton */
{[1, 2, 3].map((i) => (
))}
) : traders && traders.length > 0 ? (
{traders.map((trader) => (
{trader.trader_name}
{getModelDisplayName( trader.ai_model.split('_').pop() || trader.ai_model )}{' '} Model • {getExchangeDisplayName(trader.exchange_id, allExchanges)}
{/* Status */}
{/*
{t('status', language)}
*/}
{trader.is_running ? t('running', language) : t('stopped', language)}
{/* Actions: 禁止换行,超出横向滚动 */}
))}
) : (
{t('noTraders', language)}
{t('createFirstTrader', language)}
{(configuredModels.length === 0 || configuredExchanges.length === 0) && (
{configuredModels.length === 0 && configuredExchanges.length === 0 ? t('configureModelsAndExchangesFirst', language) : configuredModels.length === 0 ? t('configureModelsFirst', language) : t('configureExchangesFirst', language)}
)}
)}
{/* Create Trader Modal */} {showCreateModal && ( setShowCreateModal(false)} /> )} {/* Edit Trader Modal */} {showEditModal && editingTrader && ( { setShowEditModal(false) setEditingTrader(null) }} /> )} {/* Model Configuration Modal */} {showModelModal && ( { setShowModelModal(false) setEditingModel(null) }} language={language} /> )} {/* Exchange Configuration Modal */} {showExchangeModal && ( { setShowExchangeModal(false) setEditingExchange(null) }} language={language} /> )}
) } // Model Configuration Modal Component function ModelConfigModal({ allModels, configuredModels, editingModelId, onSave, onDelete, onClose, language, }: { allModels: AIModel[] configuredModels: AIModel[] editingModelId: string | null onSave: ( modelId: string, apiKey: string, baseUrl?: string, modelName?: string ) => void onDelete: (modelId: string) => void onClose: () => void language: Language }) { const [selectedModelId, setSelectedModelId] = useState(editingModelId || '') const [apiKey, setApiKey] = useState('') const [baseUrl, setBaseUrl] = useState('') const [modelName, setModelName] = useState('') // 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找 const selectedModel = editingModelId ? configuredModels?.find((m) => m.id === selectedModelId) : allModels?.find((m) => m.id === selectedModelId) // 如果是编辑现有模型,初始化API Key、Base URL和Model Name useEffect(() => { if (editingModelId && selectedModel) { setApiKey(selectedModel.apiKey || '') setBaseUrl(selectedModel.customApiUrl || '') setModelName(selectedModel.customModelName || '') } }, [editingModelId, selectedModel]) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!selectedModelId || !apiKey.trim()) return onSave( selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined ) } // 可选择的模型列表(所有支持的模型) const availableModels = allModels || [] return (

{editingModelId ? t('editAIModel', language) : t('addAIModel', language)}

{editingModelId && ( )}
{!editingModelId && (
)} {selectedModel && (
{getModelIcon(selectedModel.provider || selectedModel.id, { width: 32, height: 32, }) || (
{selectedModel.name[0]}
)}
{getShortName(selectedModel.name)}
{selectedModel.provider} • {selectedModel.id}
)} {selectedModel && ( <>
setApiKey(e.target.value)} placeholder={t('enterAPIKey', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
setBaseUrl(e.target.value)} placeholder={t('customBaseURLPlaceholder', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} />
{t('leaveBlankForDefault', language)}
setModelName(e.target.value)} placeholder="例如: deepseek-chat, qwen3-max, gpt-5" className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} />
留空使用默认模型名称
ℹ️ {t('information', language)}
{t('modelConfigInfo1', language)}
{t('modelConfigInfo2', language)}
{t('modelConfigInfo3', language)}
)}
) }