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 { useAuth } from '../contexts/AuthContext' import { getExchangeIcon } from './ExchangeIcons' import { getModelIcon } from './ModelIcons' import { TraderConfigModal } from './TraderConfigModal' import { TwoStageKeyModal, type TwoStageKeyModalResult, } from './TwoStageKeyModal' import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users, AlertTriangle, BookOpen, HelpCircle, Radio, } from 'lucide-react' // 获取友好的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 } export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage() const { user, token } = useAuth() 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( 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) // 加载用户信号源配置 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() }, [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) { 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.id === 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, 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, 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 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(', ') alert( t(config.cannotDeleteKey, language) + '\n\n' + t('tradersUsing', language) + ': ' + traderNames + '\n\n' + t('pleaseDeleteTradersFirst', language) ) return } if (!confirm(t(config.confirmDeleteKey, language))) return try { const updatedItems = config.allItems?.map((item) => item.id === config.id ? config.clearFields(item) : item ) || [] const request = config.buildRequest(updatedItems) await config.updateApi(request) // 重新获取用户配置以确保数据同步 const refreshedItems = await config.refreshApi() config.setItems(refreshedItems) config.closeModal() } catch (error) { console.error(`Failed to delete ${config.type} config:`, error) alert(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) { alert(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 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) => { await handleDeleteConfig({ id: exchangeId, type: 'exchange', checkInUse: isExchangeUsedByAnyTrader, getUsingTraders: getTradersUsingExchange, cannotDeleteKey: 'cannotDeleteExchangeInUse', confirmDeleteKey: 'confirmDeleteExchange', allItems: allExchanges, clearFields: (e) => ({ ...e, apiKey: '', secretKey: '', hyperliquidWalletAddr: '', asterUser: '', asterSigner: '', asterPrivateKey: '', enabled: false, }), buildRequest: (exchanges) => ({ exchanges: Object.fromEntries( exchanges.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 || '', }, ]) ), }), updateApi: api.updateExchangeConfigsEncrypted, refreshApi: api.getExchangeConfigs, setItems: (items) => { // 使用函数式更新确保状态正确更新 setAllExchanges([...items]) }, closeModal: () => { setShowExchangeModal(false) setEditingExchange(null) }, errorKey: 'deleteExchangeConfigFailed', }) } 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.updateExchangeConfigsEncrypted(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)}

{/* 信号源配置警告 */} {traders && traders.some((t) => t.use_coin_pool || t.use_oi_top) && !userSignalSource.coinPoolUrl && !userSignalSource.oiTopUrl && (
⚠️ {t('signalSourceNotConfigured', language)}

{t('signalSourceWarningMessage', language)}

{t('solutions', language)}

  • 点击"{t('signalSource', language)}"按钮配置API地址
  • 或在交易员配置中禁用"使用币种池"和"使用OI Top"
  • 或在交易员配置中设置自定义币种列表
)} {/* 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.id, { width: 28, height: 28 })}
{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} /> )}
) } // Tooltip Helper Component function Tooltip({ content, children, }: { content: string children: React.ReactNode }) { const [show, setShow] = useState(false) return (
setShow(true)} onMouseLeave={() => setShow(false)} onClick={() => setShow(!show)} > {children}
{show && (
{content}
)}
) } // 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, 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)}
)}
) } // 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 [showGuide, setShowGuide] = useState(false) const [serverIP, setServerIP] = useState<{ public_ip: string message: string } | null>(null) const [loadingIP, setLoadingIP] = useState(false) const [copiedIP, setCopiedIP] = useState(false) // 币安配置指南展开状态 const [showBinanceGuide, setShowBinanceGuide] = useState(false) // Aster 特定字段 const [asterUser, setAsterUser] = useState('') const [asterSigner, setAsterSigner] = useState('') const [asterPrivateKey, setAsterPrivateKey] = useState('') // Hyperliquid 特定字段 const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('') // 安全输入状态 const [secureInputTarget, setSecureInputTarget] = useState< null | 'hyperliquid' | 'aster' >(null) // 获取当前编辑的交易所信息 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) // Aster 字段 setAsterUser(selectedExchange.asterUser || '') setAsterSigner(selectedExchange.asterSigner || '') setAsterPrivateKey('') // Don't load existing private key for security // Hyperliquid 字段 setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '') } }, [editingExchangeId, selectedExchange]) // 加载服务器IP(当选择binance时) useEffect(() => { if (selectedExchangeId === 'binance' && !serverIP) { setLoadingIP(true) api .getServerIP() .then((data) => { setServerIP(data) }) .catch((err) => { console.error('Failed to load server IP:', err) }) .finally(() => { setLoadingIP(false) }) } }, [selectedExchangeId]) const handleCopyIP = (ip: string) => { navigator.clipboard.writeText(ip).then(() => { setCopiedIP(true) setTimeout(() => setCopiedIP(false), 2000) }) } // 安全输入处理函数 const secureInputContextLabel = secureInputTarget === 'aster' ? t('asterExchangeName', language) : secureInputTarget === 'hyperliquid' ? t('hyperliquidExchangeName', language) : undefined const handleSecureInputCancel = () => { setSecureInputTarget(null) } const handleSecureInputComplete = ({ value, obfuscationLog, }: TwoStageKeyModalResult) => { const trimmed = value.trim() if (secureInputTarget === 'hyperliquid') { setApiKey(trimmed) } if (secureInputTarget === 'aster') { setAsterPrivateKey(trimmed) } console.log('Secure input obfuscation log:', obfuscationLog) setSecureInputTarget(null) } // 掩盖敏感数据显示 const maskSecret = (secret: string) => { if (!secret || secret.length === 0) return '' if (secret.length <= 8) return '*'.repeat(secret.length) return ( secret.slice(0, 4) + '*'.repeat(Math.max(secret.length - 8, 4)) + secret.slice(-4) ) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!selectedExchangeId) return // 根据交易所类型验证不同字段 if (selectedExchange?.id === 'binance') { if (!apiKey.trim() || !secretKey.trim()) return await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) } else if (selectedExchange?.id === 'hyperliquid') { if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址 await onSave( selectedExchangeId, apiKey.trim(), '', testnet, hyperliquidWalletAddr.trim() ) } else if (selectedExchange?.id === 'aster') { if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return await onSave( selectedExchangeId, '', '', testnet, undefined, asterUser.trim(), asterSigner.trim(), asterPrivateKey.trim() ) } else if (selectedExchange?.id === 'okx') { if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) } else { // 默认情况(其他CEX交易所) if (!apiKey.trim() || !secretKey.trim()) return await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) } } // 可选择的交易所列表(所有支持的交易所) const availableExchanges = allExchanges || [] return (

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

{selectedExchange?.id === 'binance' && ( )} {editingExchangeId && ( )}
{!editingExchangeId && (
)} {selectedExchange && (
{getExchangeIcon(selectedExchange.id, { width: 32, height: 32, })}
{getShortName(selectedExchange.name)}
{selectedExchange.type.toUpperCase()} •{' '} {selectedExchange.id}
)} {selectedExchange && ( <> {/* Binance 和其他 CEX 交易所的字段 */} {(selectedExchange.id === 'binance' || selectedExchange.type === 'cex') && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && ( <> {/* 币安用户配置提示 (D1 方案) */} {selectedExchange.id === 'binance' && (
setShowBinanceGuide(!showBinanceGuide)} >
ℹ️ 币安用户必读: 使用「现货与合约交易」API,不要用「统一账户 API」
{showBinanceGuide ? '▲' : '▼'}
{/* 展开的详细说明 */} {showBinanceGuide && (
e.stopPropagation()} >

原因:统一账户 API 权限结构不同,会导致订单提交失败

正确配置步骤:

  1. 登录币安 → 个人中心 → API 管理
  2. 创建 API → 选择「 系统生成的 API 密钥
  3. 勾选「现货与合约交易」( 不选统一账户
  4. IP 限制选「无限制」或添加服务器 IP

💡 多资产模式用户注意: 如果您开启了多资产模式,将强制使用全仓模式。建议关闭多资产模式以支持逐仓交易。

📖 查看币安官方教程 ↗
)}
)}
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 />
)} {/* Binance 白名单IP提示 */} {selectedExchange.id === 'binance' && (
{t('whitelistIP', language)}
{t('whitelistIPDesc', language)}
{loadingIP ? (
{t('loadingServerIP', language)}
) : serverIP && serverIP.public_ip ? (
{serverIP.public_ip}
) : null}
)} )} {/* Hyperliquid 交易所的字段 */} {selectedExchange.id === 'hyperliquid' && ( <> {/* 安全提示 banner */}
🔐
{t('hyperliquidAgentWalletTitle', language)}
{t('hyperliquidAgentWalletDesc', language)}
{/* Agent Private Key 字段 */}
{apiKey && ( )}
{apiKey && (
{t('secureInputHint', language)}
)}
{t('hyperliquidAgentPrivateKeyDesc', language)}
{/* Main Wallet Address 字段 */}
setHyperliquidWalletAddr(e.target.value)} placeholder={t( 'enterHyperliquidMainWalletAddress', language )} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{t('hyperliquidMainWalletAddressDesc', language)}
)} {/* Aster 交易所的字段 */} {selectedExchange.id === 'aster' && ( <>
setAsterUser(e.target.value)} placeholder={t('enterUser', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
setAsterSigner(e.target.value)} placeholder={t('enterSigner', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{asterPrivateKey && ( )}
{asterPrivateKey && (
{t('secureInputHint', language)}
)}
)}
{t('testnetDescription', language)}
{' '} {t('securityWarning', language)}
{selectedExchange.id === 'aster' && (
{t('asterUsdtWarning', language)}
)}
{t('exchangeConfigWarning1', language)}
{t('exchangeConfigWarning2', language)}
{t('exchangeConfigWarning3', language)}
)}
{/* Binance Setup Guide Modal */} {showGuide && (
setShowGuide(false)} >
e.stopPropagation()} >

{t('binanceSetupGuide', language)}

{t('binanceSetupGuide',
)} {/* Two Stage Key Modal */}
) }