- {t('howAILearns', language)}
+ {stripLeadingIcons(t('howAILearns', language))}
diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx
index b691bb2f..5e066661 100644
--- a/web/src/components/AITradersPage.tsx
+++ b/web/src/components/AITradersPage.tsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
import useSWR from 'swr'
import { api } from '../lib/api'
import type {
@@ -13,6 +14,14 @@ import { useAuth } from '../contexts/AuthContext'
import { getExchangeIcon } from './ExchangeIcons'
import { getModelIcon } from './ModelIcons'
import { TraderConfigModal } from './TraderConfigModal'
+import {
+ TwoStageKeyModal,
+ type TwoStageKeyModalResult,
+} from './TwoStageKeyModal'
+import {
+ WebCryptoEnvironmentCheck,
+ type WebCryptoCheckStatus,
+} from './WebCryptoEnvironmentCheck'
import {
Bot,
Brain,
@@ -24,7 +33,11 @@ import {
AlertTriangle,
BookOpen,
HelpCircle,
+ Radio,
+ Pencil,
} from 'lucide-react'
+import { confirmToast } from '../lib/notify'
+import { toast } from 'sonner'
// 获取友好的AI模型名称
function getModelDisplayName(modelId: string): string {
@@ -53,6 +66,7 @@ interface AITradersPageProps {
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)
@@ -131,53 +145,82 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
loadConfigs()
}, [user, token])
- // 显示所有用户的模型和交易所配置(用于调试)
- const configuredModels = allModels || []
- const configuredExchanges = allExchanges || []
+ // 只显示已配置的模型和交易所
+ // 注意:后端返回的数据不包含敏感信息(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
+ }) || []
// 只在创建交易员时使用已启用且配置完整的
- const enabledModels = allModels?.filter((m) => m.enabled && m.apiKey) || []
+ // 注意:后端返回的数据不包含敏感信息,所以只检查 enabled 状态和必要的非敏感字段
+ const enabledModels = allModels?.filter((m) => m.enabled) || []
const enabledExchanges =
allExchanges?.filter((e) => {
if (!e.enabled) return false
- // Aster 交易所需要特殊字段
+ // Aster 交易所需要特殊字段(后端会返回这些非敏感字段)
if (e.id === 'aster') {
return (
e.asterUser &&
e.asterUser.trim() !== '' &&
e.asterSigner &&
- e.asterSigner.trim() !== '' &&
- e.asterPrivateKey &&
- e.asterPrivateKey.trim() !== ''
+ e.asterSigner.trim() !== ''
)
}
- // Hyperliquid 只需要私钥(作为apiKey),钱包地址会自动从私钥生成
+ // Hyperliquid 需要钱包地址(后端会返回这个字段)
if (e.id === 'hyperliquid') {
- return e.apiKey && e.apiKey.trim() !== ''
+ return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== ''
}
- // Binance 等其他交易所需要 apiKey 和 secretKey
- return (
- e.apiKey &&
- e.apiKey.trim() !== '' &&
- e.secretKey &&
- e.secretKey.trim() !== ''
- )
+ // 其他交易所:如果已启用,说明已配置完整(后端只返回已配置的交易所)
+ return true
}) || []
- // 检查模型是否正在被运行中的交易员使用
+ // 检查模型是否正在被运行中的交易员使用(用于UI禁用)
const isModelInUse = (modelId: string) => {
- return traders?.some((t) => t.ai_model === modelId && t.is_running) || false
+ 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) ||
- false
- )
+ 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) => {
@@ -186,21 +229,25 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const exchange = allExchanges?.find((e) => e.id === data.exchange_id)
if (!model?.enabled) {
- alert(t('modelNotConfigured', language))
+ toast.error(t('modelNotConfigured', language))
return
}
if (!exchange?.enabled) {
- alert(t('exchangeNotConfigured', language))
+ toast.error(t('exchangeNotConfigured', language))
return
}
- await api.createTrader(data)
+ await toast.promise(api.createTrader(data), {
+ loading: '正在创建…',
+ success: '创建成功',
+ error: '创建失败',
+ })
setShowCreateModal(false)
mutateTraders()
} catch (error) {
console.error('Failed to create trader:', error)
- alert(t('createTraderFailed', language))
+ toast.error(t('createTraderFailed', language))
}
}
@@ -211,7 +258,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowEditModal(true)
} catch (error) {
console.error('Failed to fetch trader config:', error)
- alert(t('getTraderConfigFailed', language))
+ toast.error(t('getTraderConfigFailed', language))
}
}
@@ -223,12 +270,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id)
if (!model) {
- alert(t('modelConfigNotExist', language))
+ toast.error(t('modelConfigNotExist', language))
return
}
if (!exchange) {
- alert(t('exchangeConfigNotExist', language))
+ toast.error(t('exchangeConfigNotExist', language))
return
}
@@ -243,44 +290,64 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
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 api.updateTrader(editingTrader.trader_id, request)
+ await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
+ loading: '正在保存…',
+ success: '保存成功',
+ error: '保存失败',
+ })
setShowEditModal(false)
setEditingTrader(null)
mutateTraders()
} catch (error) {
console.error('Failed to update trader:', error)
- alert(t('updateTraderFailed', language))
+ toast.error(t('updateTraderFailed', language))
}
}
const handleDeleteTrader = async (traderId: string) => {
- if (!confirm(t('confirmDeleteTrader', language))) return
+ {
+ const ok = await confirmToast(t('confirmDeleteTrader', language))
+ if (!ok) return
+ }
try {
- await api.deleteTrader(traderId)
+ await toast.promise(api.deleteTrader(traderId), {
+ loading: '正在删除…',
+ success: '删除成功',
+ error: '删除失败',
+ })
mutateTraders()
} catch (error) {
console.error('Failed to delete trader:', error)
- alert(t('deleteTraderFailed', language))
+ toast.error(t('deleteTraderFailed', language))
}
}
const handleToggleTrader = async (traderId: string, running: boolean) => {
try {
if (running) {
- await api.stopTrader(traderId)
+ await toast.promise(api.stopTrader(traderId), {
+ loading: '正在停止…',
+ success: '已停止',
+ error: '停止失败',
+ })
} else {
- await api.startTrader(traderId)
+ await toast.promise(api.startTrader(traderId), {
+ loading: '正在启动…',
+ success: '已启动',
+ error: '启动失败',
+ })
}
mutateTraders()
} catch (error) {
console.error('Failed to toggle trader:', error)
- alert(t('operationFailed', language))
+ toast.error(t('operationFailed', language))
}
}
@@ -298,27 +365,82 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
}
- const handleDeleteModelConfig = async (modelId: string) => {
- if (!confirm(t('confirmDeleteModel', language))) return
+ // 通用删除配置处理函数
+ 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 updatedModels =
- allModels?.map((m) =>
- m.id === modelId
- ? {
- ...m,
- apiKey: '',
- customApiUrl: '',
- customModelName: '',
- enabled: false,
- }
- : m
+ const updatedItems =
+ config.allItems?.map((item) =>
+ item.id === config.id ? config.clearFields(item) : item
) || []
- const request = {
+ 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(
- updatedModels.map((model) => [
- model.provider, // 使用 provider 而不是 id
+ models.map((model) => [
+ model.provider,
{
enabled: model.enabled,
api_key: model.apiKey || '',
@@ -327,16 +449,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
},
])
),
- }
-
- await api.updateModelConfigs(request)
- setAllModels(updatedModels)
- setShowModelModal(false)
- setEditingModel(null)
- } catch (error) {
- console.error('Failed to delete model config:', error)
- alert(t('deleteConfigFailed', language))
- }
+ }),
+ updateApi: api.updateModelConfigs,
+ refreshApi: api.getModelConfigs,
+ setItems: (items) => {
+ // 使用函数式更新确保状态正确更新
+ setAllModels([...items])
+ },
+ closeModal: () => {
+ setShowModelModal(false)
+ setEditingModel(null)
+ },
+ errorKey: 'deleteConfigFailed',
+ })
}
const handleSaveModelConfig = async (
@@ -354,7 +479,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const modelToUpdate =
existingModel || supportedModels?.find((m) => m.id === modelId)
if (!modelToUpdate) {
- alert(t('modelNotExist', language))
+ toast.error(t('modelNotExist', language))
return
}
@@ -398,7 +523,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
),
}
- await api.updateModelConfigs(request)
+ await toast.promise(api.updateModelConfigs(request), {
+ loading: '正在更新模型配置…',
+ success: '模型配置已更新',
+ error: '更新模型配置失败',
+ })
// 重新获取用户配置以确保数据同步
const refreshedModels = await api.getModelConfigs()
@@ -408,43 +537,58 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingModel(null)
} catch (error) {
console.error('Failed to save model config:', error)
- alert(t('saveConfigFailed', language))
+ toast.error(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 = {
+ 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(
- updatedExchanges.map((exchange) => [
+ 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 || '',
},
])
),
- }
-
- await api.updateExchangeConfigs(request)
- setAllExchanges(updatedExchanges)
- setShowExchangeModal(false)
- setEditingExchange(null)
- } catch (error) {
- console.error('Failed to delete exchange config:', error)
- alert(t('deleteExchangeConfigFailed', language))
- }
+ }),
+ updateApi: api.updateExchangeConfigsEncrypted,
+ refreshApi: api.getExchangeConfigs,
+ setItems: (items) => {
+ // 使用函数式更新确保状态正确更新
+ setAllExchanges([...items])
+ },
+ closeModal: () => {
+ setShowExchangeModal(false)
+ setEditingExchange(null)
+ },
+ errorKey: 'deleteExchangeConfigFailed',
+ })
}
const handleSaveExchangeConfig = async (
@@ -463,7 +607,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
(e) => e.id === exchangeId
)
if (!exchangeToUpdate) {
- alert(t('exchangeNotExist', language))
+ toast.error(t('exchangeNotExist', language))
return
}
@@ -523,7 +667,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
),
}
- await api.updateExchangeConfigs(request)
+ await toast.promise(api.updateExchangeConfigsEncrypted(request), {
+ loading: '正在更新交易所配置…',
+ success: '交易所配置已更新',
+ error: '更新交易所配置失败',
+ })
// 重新获取用户配置以确保数据同步
const refreshedExchanges = await api.getExchangeConfigs()
@@ -533,7 +681,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingExchange(null)
} catch (error) {
console.error('Failed to save exchange config:', error)
- alert(t('saveConfigFailed', language))
+ toast.error(t('saveConfigFailed', language))
}
}
@@ -552,12 +700,16 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
oiTopUrl: string
) => {
try {
- await api.saveUserSignalSource(coinPoolUrl, oiTopUrl)
+ await toast.promise(api.saveUserSignalSource(coinPoolUrl, oiTopUrl), {
+ loading: '正在保存…',
+ success: '保存成功',
+ error: '保存失败',
+ })
setUserSignalSource({ coinPoolUrl, oiTopUrl })
setShowSignalSourceModal(false)
} catch (error) {
console.error('Failed to save signal source:', error)
- alert(t('saveSignalSourceFailed', language))
+ toast.error(t('saveSignalSourceFailed', language))
}
}
@@ -597,7 +749,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {