import React, { useState, useEffect } from 'react'; 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 { getExchangeIcon } from './ExchangeIcons'; import { getModelIcon } from './ModelIcons'; import { TraderConfigModal } from './TraderConfigModal'; import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users } from 'lucide-react'; // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { switch (modelId.toLowerCase()) { case 'deepseek': return 'DeepSeek'; case 'qwen': return 'Qwen'; case 'claude': return 'Claude'; case 'gpt4': case 'gpt-4': return 'GPT-4'; case 'gpt3.5': case 'gpt-3.5': return 'GPT-3.5'; 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; } export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage(); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showModelModal, setShowModelModal] = useState(false); const [showExchangeModal, setShowExchangeModal] = useState(false); const [showSignalSourceModal, setShowSignalSourceModal] = 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 [userSignalSource, setUserSignalSource] = useState<{coinPoolUrl: string, oiTopUrl: string}>({ coinPoolUrl: '', oiTopUrl: '' }); const { data: traders, mutate: mutateTraders } = useSWR( 'traders', api.getTraders, { refreshInterval: 5000 } ); // 加载AI模型和交易所配置 useEffect(() => { const loadConfigs = async () => { 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); // 加载用户信号源配置 try { const signalSource = await api.getUserSignalSource(); setUserSignalSource({ coinPoolUrl: signalSource.coin_pool_url || '', oiTopUrl: signalSource.oi_top_url || '' }); } catch (error) { console.log('📡 用户信号源配置暂未设置'); } } catch (error) { console.error('Failed to load configs:', error); } }; loadConfigs(); }, []); // 显示所有用户的模型和交易所配置(用于调试) const configuredModels = allModels || []; const configuredExchanges = allExchanges || []; // 只在创建交易员时使用已启用且配置完整的 const enabledModels = allModels?.filter(m => m.enabled && m.apiKey) || []; const enabledExchanges = allExchanges?.filter(e => { if (!e.enabled) return false; // Aster 交易所需要特殊字段 if (e.id === 'aster') { return e.asterUser && e.asterSigner && e.asterPrivateKey; } // Hyperliquid 只需要私钥(作为apiKey),不需要secretKey if (e.id === 'hyperliquid') { return e.apiKey && e.hyperliquidWalletAddr; } // Binance 等其他交易所需要 apiKey 和 secretKey return e.apiKey && e.apiKey.trim() !== '' && e.secretKey && e.secretKey.trim() !== ''; }) || []; // 检查模型是否正在被运行中的交易员使用 const isModelInUse = (modelId: string) => { return traders?.some(t => t.ai_model === modelId && t.is_running) || false; }; // 检查交易所是否正在被运行中的交易员使用 const isExchangeInUse = (exchangeId: string) => { return traders?.some(t => t.exchange_id === exchangeId && t.is_running) || false; }; const handleCreateTrader = async (data: CreateTraderRequest) => { try { const model = allModels?.find(m => m.provider === data.ai_model_id); const exchange = allExchanges?.find(e => e.id === data.exchange_id); if (!model?.enabled) { alert(t('modelNotConfigured', language)); return; } if (!exchange?.enabled) { alert(t('exchangeNotConfigured', language)); return; } await api.createTrader(data); setShowCreateModal(false); mutateTraders(); } catch (error) { console.error('Failed to create trader:', error); alert(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); alert(t('getTraderConfigFailed', language)); } }; const handleSaveEditTrader = async (data: CreateTraderRequest) => { if (!editingTrader) return; try { const model = enabledModels?.find(m => m.provider === data.ai_model_id); const exchange = enabledExchanges?.find(e => e.id === data.exchange_id); if (!model) { alert(t('modelConfigNotExist', language)); return; } if (!exchange) { alert(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, 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, is_cross_margin: data.is_cross_margin, use_coin_pool: data.use_coin_pool, use_oi_top: data.use_oi_top }; await api.updateTrader(editingTrader.trader_id, request); setShowEditModal(false); setEditingTrader(null); mutateTraders(); } catch (error) { console.error('Failed to update trader:', error); alert(t('updateTraderFailed', language)); } }; const handleDeleteTrader = async (traderId: string) => { if (!confirm(t('confirmDeleteTrader', language))) return; try { await api.deleteTrader(traderId); mutateTraders(); } catch (error) { console.error('Failed to delete trader:', error); alert(t('deleteTraderFailed', language)); } }; const handleToggleTrader = async (traderId: string, running: boolean) => { try { if (running) { await api.stopTrader(traderId); } else { await api.startTrader(traderId); } mutateTraders(); } catch (error) { console.error('Failed to toggle trader:', error); alert(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 handleDeleteModelConfig = async (modelId: string) => { if (!confirm(t('confirmDeleteModel', language))) return; try { const updatedModels = allModels?.map(m => m.id === modelId ? { ...m, apiKey: '', customApiUrl: '', customModelName: '', enabled: false } : m ) || []; 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 api.updateModelConfigs(request); setAllModels(updatedModels); setShowModelModal(false); setEditingModel(null); } catch (error) { console.error('Failed to delete model config:', error); alert(t('deleteConfigFailed', language)); } }; const handleSaveModelConfig = async (modelId: string, apiKey: string, customApiUrl?: string, customModelName?: string) => { try { // 找到要配置的模型(从supportedModels中) const modelToUpdate = supportedModels?.find(m => m.id === modelId); if (!modelToUpdate) { alert(t('modelNotExist', language)); return; } // 创建或更新用户的模型配置 const existingModel = allModels?.find(m => m.id === modelId); let updatedModels; 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 api.updateModelConfigs(request); // 重新获取用户配置以确保数据同步 const refreshedModels = await api.getModelConfigs(); setAllModels(refreshedModels); setShowModelModal(false); setEditingModel(null); } catch (error) { console.error('Failed to save model config:', error); alert(t('saveConfigFailed', language)); } }; const handleDeleteExchangeConfig = async (exchangeId: string) => { if (!confirm(t('confirmDeleteExchange', language))) return; try { const updatedExchanges = allExchanges?.map(e => e.id === exchangeId ? { ...e, apiKey: '', secretKey: '', enabled: false } : e ) || []; const request = { exchanges: Object.fromEntries( updatedExchanges.map(exchange => [ exchange.id, { enabled: exchange.enabled, api_key: exchange.apiKey || '', secret_key: exchange.secretKey || '', testnet: exchange.testnet || false } ]) ) }; await api.updateExchangeConfigs(request); setAllExchanges(updatedExchanges); setShowExchangeModal(false); setEditingExchange(null); } catch (error) { console.error('Failed to delete exchange config:', error); alert(t('deleteExchangeConfigFailed', language)); } }; const handleSaveExchangeConfig = async (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => { try { // 找到要配置的交易所(从supportedExchanges中) const exchangeToUpdate = supportedExchanges?.find(e => e.id === exchangeId); if (!exchangeToUpdate) { alert(t('exchangeNotExist', language)); return; } // 创建或更新用户的交易所配置 const existingExchange = allExchanges?.find(e => e.id === exchangeId); let updatedExchanges; if (existingExchange) { // 更新现有配置 updatedExchanges = allExchanges?.map(e => e.id === exchangeId ? { ...e, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true } : e ) || []; } else { // 添加新配置 const newExchange = { ...exchangeToUpdate, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true }; updatedExchanges = [...(allExchanges || []), newExchange]; } const request = { exchanges: Object.fromEntries( updatedExchanges.map(exchange => [ exchange.id, { enabled: exchange.enabled, api_key: exchange.apiKey || '', secret_key: exchange.secretKey || '', testnet: exchange.testnet || false, hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '', aster_user: exchange.asterUser || '', aster_signer: exchange.asterSigner || '', aster_private_key: exchange.asterPrivateKey || '' } ]) ) }; await api.updateExchangeConfigs(request); // 重新获取用户配置以确保数据同步 const refreshedExchanges = await api.getExchangeConfigs(); setAllExchanges(refreshedExchanges); setShowExchangeModal(false); setEditingExchange(null); } catch (error) { console.error('Failed to save exchange config:', error); alert(t('saveConfigFailed', language)); } }; const handleAddModel = () => { setEditingModel(null); setShowModelModal(true); }; const handleAddExchange = () => { setEditingExchange(null); setShowExchangeModal(true); }; const handleSaveSignalSource = async (coinPoolUrl: string, oiTopUrl: string) => { try { await api.saveUserSignalSource(coinPoolUrl, oiTopUrl); setUserSignalSource({ coinPoolUrl, oiTopUrl }); setShowSignalSourceModal(false); } catch (error) { console.error('Failed to save signal source:', error); alert(t('saveSignalSourceFailed', language)); } }; 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: 32, height: 32 }) || (
{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.id, { width: 32, height: 32 })}
{getShortName(exchange.name)}
{exchange.type.toUpperCase()} • {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)}
); })} {configuredExchanges.length === 0 && (
{t('noExchangesConfigured', language)}
)}
{/* Traders List */}

{t('currentTraders', language)}

{traders && traders.length > 0 ? (
{traders.map(trader => (
{trader.trader_name}
{getModelDisplayName(trader.ai_model.split('_').pop() || trader.ai_model)} Model • {trader.exchange_id?.toUpperCase()}
{/* 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} /> )} {/* Signal Source Configuration Modal */} {showSignalSourceModal && ( setShowSignalSourceModal(false)} language={language} /> )}
); } // Signal Source Configuration Modal Component function SignalSourceModal({ coinPoolUrl, oiTopUrl, onSave, onClose, language }: { coinPoolUrl: string; oiTopUrl: string; onSave: (coinPoolUrl: string, oiTopUrl: string) => void; onClose: () => void; language: Language; }) { const [coinPool, setCoinPool] = useState(coinPoolUrl || ''); const [oiTop, setOiTop] = useState(oiTopUrl || ''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSave(coinPool.trim(), oiTop.trim()); }; return (

📡 {t('signalSourceConfig', language)}

setCoinPool(e.target.value)} placeholder="https://api.example.com/coinpool" className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
{t('coinPoolDescription', language)}
setOiTop(e.target.value)} placeholder="https://api.example.com/oitop" className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
{t('oiTopDescription', language)}
ℹ️ {t('information', language)}
{t('signalSourceInfo1', language)}
{t('signalSourceInfo2', language)}
{t('signalSourceInfo3', 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, qwen-plus, gpt-4" 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)}
)}
); } // Exchange Configuration Modal Component function ExchangeConfigModal({ allExchanges, editingExchangeId, onSave, onDelete, onClose, language }: { allExchanges: Exchange[]; editingExchangeId: string | null; onSave: (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => Promise; onDelete: (exchangeId: string) => void; onClose: () => void; language: Language; }) { const [selectedExchangeId, setSelectedExchangeId] = useState(editingExchangeId || ''); const [apiKey, setApiKey] = useState(''); const [secretKey, setSecretKey] = useState(''); const [passphrase, setPassphrase] = useState(''); const [testnet, setTestnet] = useState(false); // 获取当前编辑的交易所信息 const selectedExchange = allExchanges?.find(e => e.id === selectedExchangeId); // 如果是编辑现有交易所,初始化表单数据 useEffect(() => { if (editingExchangeId && selectedExchange) { setApiKey(selectedExchange.apiKey || ''); setSecretKey(selectedExchange.secretKey || ''); setPassphrase(''); // Don't load existing passphrase for security setTestnet(selectedExchange.testnet || false); } }, [editingExchangeId, selectedExchange]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedExchangeId) return; // 根据交易所类型验证不同字段 if (selectedExchange?.id === 'binance') { if (!apiKey.trim() || !secretKey.trim()) return; } else if (selectedExchange?.id === 'okx') { if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return; } await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet, undefined, undefined, undefined, undefined); }; // 可选择的交易所列表(所有支持的交易所) const availableExchanges = allExchanges || []; return (

{editingExchangeId ? t('editExchange', language) : t('addExchange', language)}

{editingExchangeId && ( )}
{!editingExchangeId && (
)} {selectedExchange && (
{getExchangeIcon(selectedExchange.id, { width: 32, height: 32 })}
{getShortName(selectedExchange.name)}
{selectedExchange.type.toUpperCase()} • {selectedExchange.id}
)} {selectedExchange && ( <>
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 />
setSecretKey(e.target.value)} placeholder={t('enterSecretKey', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
{selectedExchange.id === 'okx' && (
setPassphrase(e.target.value)} placeholder={t('enterPassphrase', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
)}
{t('testnetDescription', language)}
⚠️ {t('securityWarning', language)}
{t('exchangeConfigWarning1', language)}
{t('exchangeConfigWarning2', language)}
{t('exchangeConfigWarning3', language)}
)}
); }