Merge remote-tracking branch 'refs/remotes/origin/dev' into dev

This commit is contained in:
SkywalkerJi
2025-11-01 22:31:09 +08:00
18 changed files with 504 additions and 90 deletions
+2 -2
View File
@@ -97,7 +97,7 @@ docker compose up -d
Once deployed, open your browser and visit: Once deployed, open your browser and visit:
- **Web Interface**: http://localhost:3000 - **Web Interface**: http://localhost:3000
- **API Health Check**: http://localhost:8080/health - **API Health Check**: http://localhost:8080/api/health
## 📊 Service Management ## 📊 Service Management
@@ -280,7 +280,7 @@ docker inspect nofx-backend | jq '.[0].State.Health'
docker inspect nofx-frontend | jq '.[0].State.Health' docker inspect nofx-frontend | jq '.[0].State.Health'
# Manually test health endpoints # Manually test health endpoints
curl http://localhost:8080/health curl http://localhost:8080/api/health
curl http://localhost:3000/health curl http://localhost:3000/health
``` ```
+2 -2
View File
@@ -100,7 +100,7 @@ docker compose up -d
部署成功后,打开浏览器访问: 部署成功后,打开浏览器访问:
- **Web 界面**: http://localhost:3000 - **Web 界面**: http://localhost:3000
- **API 文档**: http://localhost:8080/health - **API 文档**: http://localhost:8080/api/health
## 📊 服务管理 ## 📊 服务管理
@@ -281,7 +281,7 @@ docker inspect nofx-backend | jq '.[0].State.Health'
docker inspect nofx-frontend | jq '.[0].State.Health' docker inspect nofx-frontend | jq '.[0].State.Health'
# 手动测试健康端点 # 手动测试健康端点
curl http://localhost:8080/health curl http://localhost:8080/api/health
curl http://localhost:3000/health curl http://localhost:3000/health
``` ```
+1 -1
View File
@@ -79,7 +79,7 @@ npm install -g pm2
- **前端 Web 界面**: http://localhost:3000 - **前端 Web 界面**: http://localhost:3000
- **后端 API**: http://localhost:8080 - **后端 API**: http://localhost:8080
- **健康检查**: http://localhost:8080/health - **健康检查**: http://localhost:8080/api/health
--- ---
+2 -2
View File
@@ -887,7 +887,7 @@ Open your web browser and visit:
```bash ```bash
# In a new terminal window # In a new terminal window
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
Should return: `{"status":"ok"}` Should return: `{"status":"ok"}`
@@ -1137,7 +1137,7 @@ GET /api/performance?trader_id=xxx # AI performance analysis
### System Endpoints ### System Endpoints
```bash ```bash
GET /health # Health check GET /api/health # Health check
``` ```
--- ---
+1 -1
View File
@@ -796,7 +796,7 @@ VITE v5.x.x ready in xxx ms
```bash ```bash
# В новом окне терминала # В новом окне терминала
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
Должно вернуть: `{"status":"ok"}` Должно вернуть: `{"status":"ok"}`
+1 -1
View File
@@ -796,7 +796,7 @@ VITE v5.x.x ready in xxx ms
```bash ```bash
# У новому вікні терміналу # У новому вікні терміналу
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
Повинно повернути: `{"status":"ok"}` Повинно повернути: `{"status":"ok"}`
+2 -2
View File
@@ -869,7 +869,7 @@ VITE v5.x.x ready in xxx ms
```bash ```bash
# 在新终端窗口中 # 在新终端窗口中
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
应返回:`{"status":"ok"}` 应返回:`{"status":"ok"}`
@@ -1076,7 +1076,7 @@ GET /api/statistics?trader_id=xxx # 统计信息
### 系统接口 ### 系统接口
```bash ```bash
GET /health # 健康检查 GET /api/health # 健康检查
GET /api/config # 系统配置 GET /api/config # 系统配置
``` ```
+8 -7
View File
@@ -66,12 +66,12 @@ func corsMiddleware() gin.HandlerFunc {
// setupRoutes 设置路由 // setupRoutes 设置路由
func (s *Server) setupRoutes() { func (s *Server) setupRoutes() {
// 健康检查
s.router.Any("/health", s.handleHealth)
// API路由组 // API路由组
api := s.router.Group("/api") api := s.router.Group("/api")
{ {
// 健康检查
api.Any("/health", s.handleHealth)
// 认证相关路由(无需认证) // 认证相关路由(无需认证)
api.POST("/register", s.handleRegister) api.POST("/register", s.handleRegister)
api.POST("/login", s.handleLogin) api.POST("/login", s.handleLogin)
@@ -85,6 +85,10 @@ func (s *Server) setupRoutes() {
// 系统配置(无需认证) // 系统配置(无需认证)
api.GET("/config", s.handleGetSystemConfig) api.GET("/config", s.handleGetSystemConfig)
// 系统提示词模板管理(无需认证)
api.GET("/prompt-templates", s.handleGetPromptTemplates)
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
// 需要认证的路由 // 需要认证的路由
protected := api.Group("/", s.authMiddleware()) protected := api.Group("/", s.authMiddleware())
{ {
@@ -110,9 +114,6 @@ func (s *Server) setupRoutes() {
protected.GET("/user/signal-sources", s.handleGetUserSignalSource) protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource) protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
// 系统提示词模板管理
protected.GET("/prompt-templates", s.handleGetPromptTemplates)
protected.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
// 竞赛总览 // 竞赛总览
protected.GET("/competition", s.handleCompetition) protected.GET("/competition", s.handleCompetition)
@@ -1398,7 +1399,7 @@ func (s *Server) Start() error {
addr := fmt.Sprintf(":%d", s.port) addr := fmt.Sprintf(":%d", s.port)
log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
log.Printf("📊 API文档:") log.Printf("📊 API文档:")
log.Printf(" • GET /health - 健康检查") log.Printf(" • GET /api/health - 健康检查")
log.Printf(" • GET /api/traders - AI交易员列表") log.Printf(" • GET /api/traders - AI交易员列表")
log.Printf(" • POST /api/traders - 创建新的AI交易员") log.Printf(" • POST /api/traders - 创建新的AI交易员")
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员") log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
+8 -1
View File
@@ -853,7 +853,12 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT SELECT
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running,
COALESCE(t.btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(t.altcoin_leverage, 5) as altcoin_leverage,
COALESCE(t.trading_symbols, '') as trading_symbols, COALESCE(t.use_coin_pool, 0) as use_coin_pool,
COALESCE(t.use_oi_top, 0) as use_oi_top, COALESCE(t.custom_prompt, '') as custom_prompt,
COALESCE(t.override_base_prompt, 0) as override_base_prompt, COALESCE(t.is_cross_margin, 1) as is_cross_margin,
t.created_at, t.updated_at,
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at,
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
@@ -868,6 +873,8 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
`, traderID, userID).Scan( `, traderID, userID).Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, &trader.UseCoinPool,
&trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt, &trader.CreatedAt, &trader.UpdatedAt,
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
&aiModel.CreatedAt, &aiModel.UpdatedAt, &aiModel.CreatedAt, &aiModel.UpdatedAt,
+1 -1
View File
@@ -19,7 +19,7 @@ services:
networks: networks:
- nofx-network - nofx-network
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"] test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
+1 -1
View File
@@ -63,6 +63,6 @@ COPY --from=backend-builder /app/nofx .
EXPOSE 8080 EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1
CMD ["./nofx"] CMD ["./nofx"]
+1 -1
View File
@@ -227,7 +227,7 @@ status() {
$COMPOSE_CMD ps $COMPOSE_CMD ps
echo "" echo ""
print_info "健康检查:" print_info "健康检查:"
curl -s "http://localhost:${NOFX_BACKEND_PORT}/health" | jq '.' || echo "后端未响应" curl -s "http://localhost:${NOFX_BACKEND_PORT}/api/health" | jq '.' || echo "后端未响应"
} }
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
+163 -11
View File
@@ -101,12 +101,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// Aster 交易所需要特殊字段 // Aster 交易所需要特殊字段
if (e.id === 'aster') { if (e.id === 'aster') {
return e.asterUser && e.asterSigner && e.asterPrivateKey; return e.asterUser && e.asterUser.trim() !== '' &&
e.asterSigner && e.asterSigner.trim() !== '' &&
e.asterPrivateKey && e.asterPrivateKey.trim() !== '';
} }
// Hyperliquid 只需要私钥(作为apiKey,不需要secretKey // Hyperliquid 只需要私钥(作为apiKey和钱包地址
if (e.id === 'hyperliquid') { if (e.id === 'hyperliquid') {
return e.apiKey && e.hyperliquidWalletAddr; return e.apiKey && e.apiKey.trim() !== '' &&
e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '';
} }
// Binance 等其他交易所需要 apiKey 和 secretKey // Binance 等其他交易所需要 apiKey 和 secretKey
@@ -372,11 +375,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
if (existingExchange) { if (existingExchange) {
// 更新现有配置 // 更新现有配置
updatedExchanges = allExchanges?.map(e => updatedExchanges = allExchanges?.map(e =>
e.id === exchangeId ? { ...e, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true } : e e.id === exchangeId ? {
...e,
apiKey,
secretKey,
testnet,
hyperliquidWalletAddr,
asterUser,
asterSigner,
asterPrivateKey,
enabled: true
} : e
) || []; ) || [];
} else { } else {
// 添加新配置 // 添加新配置
const newExchange = { ...exchangeToUpdate, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true }; const newExchange = {
...exchangeToUpdate,
apiKey,
secretKey,
testnet,
hyperliquidWalletAddr,
asterUser,
asterSigner,
asterPrivateKey,
enabled: true
};
updatedExchanges = [...(allExchanges || []), newExchange]; updatedExchanges = [...(allExchanges || []), newExchange];
} }
@@ -1122,6 +1145,14 @@ function ExchangeConfigModal({
const [passphrase, setPassphrase] = useState(''); const [passphrase, setPassphrase] = useState('');
const [testnet, setTestnet] = useState(false); const [testnet, setTestnet] = useState(false);
// Hyperliquid 特定字段
const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('');
// Aster 特定字段
const [asterUser, setAsterUser] = useState('');
const [asterSigner, setAsterSigner] = useState('');
const [asterPrivateKey, setAsterPrivateKey] = useState('');
// 获取当前编辑的交易所信息 // 获取当前编辑的交易所信息
const selectedExchange = allExchanges?.find(e => e.id === selectedExchangeId); const selectedExchange = allExchanges?.find(e => e.id === selectedExchangeId);
@@ -1132,6 +1163,14 @@ function ExchangeConfigModal({
setSecretKey(selectedExchange.secretKey || ''); setSecretKey(selectedExchange.secretKey || '');
setPassphrase(''); // Don't load existing passphrase for security setPassphrase(''); // Don't load existing passphrase for security
setTestnet(selectedExchange.testnet || false); setTestnet(selectedExchange.testnet || false);
// Hyperliquid 字段
setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '');
// Aster 字段
setAsterUser(selectedExchange.asterUser || '');
setAsterSigner(selectedExchange.asterSigner || '');
setAsterPrivateKey(''); // Don't load existing private key for security
} }
}, [editingExchangeId, selectedExchange]); }, [editingExchangeId, selectedExchange]);
@@ -1142,11 +1181,21 @@ function ExchangeConfigModal({
// 根据交易所类型验证不同字段 // 根据交易所类型验证不同字段
if (selectedExchange?.id === 'binance') { if (selectedExchange?.id === 'binance') {
if (!apiKey.trim() || !secretKey.trim()) return; 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') { } else if (selectedExchange?.id === 'okx') {
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return; 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);
} }
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet, undefined, undefined, undefined, undefined);
}; };
// 可选择的交易所列表(所有支持的交易所) // 可选择的交易所列表(所有支持的交易所)
@@ -1216,10 +1265,13 @@ function ExchangeConfigModal({
)} )}
{selectedExchange && ( {selectedExchange && (
<>
{/* Binance 和其他 CEX 交易所的字段 */}
{(selectedExchange.id === 'binance' || selectedExchange.type === 'cex') && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && (
<> <>
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
API Key {t('apiKey', language)}
</label> </label>
<input <input
type="password" type="password"
@@ -1234,7 +1286,7 @@ function ExchangeConfigModal({
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
Secret Key {t('secretKey', language)}
</label> </label>
<input <input
type="password" type="password"
@@ -1250,7 +1302,7 @@ function ExchangeConfigModal({
{selectedExchange.id === 'okx' && ( {selectedExchange.id === 'okx' && (
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
Passphrase {t('passphrase', language)}
</label> </label>
<input <input
type="password" type="password"
@@ -1263,6 +1315,99 @@ function ExchangeConfigModal({
/> />
</div> </div>
)} )}
</>
)}
{/* Hyperliquid 交易所的字段 */}
{selectedExchange.id === 'hyperliquid' && (
<>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('privateKey', language)}
</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={t('enterPrivateKey', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('hyperliquidPrivateKeyDesc', language)}
</div>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('walletAddress', language)}
</label>
<input
type="text"
value={hyperliquidWalletAddr}
onChange={(e) => setHyperliquidWalletAddr(e.target.value)}
placeholder={t('enterWalletAddress', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('hyperliquidWalletAddressDesc', language)}
</div>
</div>
</>
)}
{/* Aster 交易所的字段 */}
{selectedExchange.id === 'aster' && (
<>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('user', language)}
</label>
<input
type="text"
value={asterUser}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('signer', language)}
</label>
<input
type="text"
value={asterSigner}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('privateKey', language)}
</label>
<input
type="password"
value={asterPrivateKey}
onChange={(e) => setAsterPrivateKey(e.target.value)}
placeholder={t('enterPrivateKey', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
</div>
</>
)}
<div> <div>
<label className="flex items-center gap-2 text-sm"> <label className="flex items-center gap-2 text-sm">
@@ -1304,7 +1449,14 @@ function ExchangeConfigModal({
</button> </button>
<button <button
type="submit" type="submit"
disabled={!selectedExchange || !apiKey.trim() || !secretKey.trim() || (selectedExchange?.id === 'okx' && !passphrase.trim())} disabled={
!selectedExchange ||
(selectedExchange.id === 'binance' && (!apiKey.trim() || !secretKey.trim())) ||
(selectedExchange.id === 'okx' && (!apiKey.trim() || !secretKey.trim() || !passphrase.trim())) ||
(selectedExchange.id === 'hyperliquid' && (!apiKey.trim() || !hyperliquidWalletAddr.trim())) ||
(selectedExchange.id === 'aster' && (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())) ||
(selectedExchange.type === 'cex' && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && selectedExchange.id !== 'binance' && selectedExchange.id !== 'okx' && (!apiKey.trim() || !secretKey.trim()))
}
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50" className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
+3 -3
View File
@@ -3,7 +3,7 @@ import useSWR from 'swr';
import { api } from '../lib/api'; import { api } from '../lib/api';
import type { CompetitionData } from '../types'; import type { CompetitionData } from '../types';
import { ComparisonChart } from './ComparisonChart'; import { ComparisonChart } from './ComparisonChart';
import { TraderConfigModal } from './TraderConfigModal'; import { TraderConfigViewModal } from './TraderConfigViewModal';
import { getTraderColor } from '../utils/traderColors'; import { getTraderColor } from '../utils/traderColors';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations'; import { t } from '../i18n/translations';
@@ -273,8 +273,8 @@ export function CompetitionPage() {
</div> </div>
)} )}
{/* Trader Config Modal */} {/* Trader Config View Modal */}
<TraderConfigModal <TraderConfigViewModal
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={closeModal} onClose={closeModal}
traderData={selectedTrader} traderData={selectedTrader}
+1 -6
View File
@@ -120,12 +120,7 @@ export function TraderConfigModal({
useEffect(() => { useEffect(() => {
const fetchPromptTemplates = async () => { const fetchPromptTemplates = async () => {
try { try {
const token = localStorage.getItem('token'); const response = await fetch('/api/prompt-templates');
const response = await fetch('/api/prompt-templates', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json(); const data = await response.json();
if (data.templates) { if (data.templates) {
setPromptTemplates(data.templates); setPromptTemplates(data.templates);
@@ -0,0 +1,211 @@
import { useState } from 'react';
import type { TraderConfigData } from '../types';
// 提取下划线后面的名称部分
function getShortName(fullName: string): string {
const parts = fullName.split('_');
return parts.length > 1 ? parts[parts.length - 1] : fullName;
}
interface TraderConfigViewModalProps {
isOpen: boolean;
onClose: () => void;
traderData?: TraderConfigData | null;
}
export function TraderConfigViewModal({
isOpen,
onClose,
traderData
}: TraderConfigViewModalProps) {
const [copiedField, setCopiedField] = useState<string | null>(null);
if (!isOpen || !traderData) return null;
const copyToClipboard = async (text: string, fieldName: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedField(fieldName);
setTimeout(() => setCopiedField(null), 2000);
} catch (error) {
console.error('Failed to copy:', error);
}
};
const CopyButton = ({ text, fieldName }: { text: string; fieldName: string }) => (
<button
onClick={() => copyToClipboard(text, fieldName)}
className="ml-2 px-2 py-1 text-xs rounded transition-all duration-200 hover:scale-105"
style={{
background: copiedField === fieldName ? 'rgba(14, 203, 129, 0.1)' : 'rgba(240, 185, 11, 0.1)',
color: copiedField === fieldName ? '#0ECB81' : '#F0B90B',
border: `1px solid ${copiedField === fieldName ? 'rgba(14, 203, 129, 0.3)' : 'rgba(240, 185, 11, 0.3)'}`
}}
>
{copiedField === fieldName ? '✓ 已复制' : '📋 复制'}
</button>
);
const InfoRow = ({ label, value, copyable = false, fieldName = '' }: {
label: string;
value: string | number | boolean;
copyable?: boolean;
fieldName?: string;
}) => (
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
<div className="flex items-center text-right">
<span className="text-sm text-[#EAECEF] font-mono">
{typeof value === 'boolean' ? (value ? '是' : '否') : value}
</span>
{copyable && typeof value === 'string' && value && (
<CopyButton text={value} fieldName={fieldName} />
)}
</div>
</div>
);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
<div
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#F0B90B] to-[#E1A706] flex items-center justify-center">
<span className="text-lg">👁</span>
</div>
<div>
<h2 className="text-xl font-bold text-[#EAECEF]">
</h2>
<p className="text-sm text-[#848E9C] mt-1">
{traderData.trader_name}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Running Status */}
<div
className="px-3 py-1 rounded-full text-xs font-bold flex items-center gap-1"
style={traderData.is_running
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
}
>
<span>{traderData.is_running ? '●' : '○'}</span>
{traderData.is_running ? '运行中' : '已停止'}
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-lg text-[#848E9C] hover:text-[#EAECEF] hover:bg-[#2B3139] transition-colors flex items-center justify-center"
>
</button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Basic Info */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
🤖
</h3>
<div className="space-y-3">
<InfoRow label="交易员ID" value={traderData.trader_id || ''} copyable fieldName="trader_id" />
<InfoRow label="交易员名称" value={traderData.trader_name} copyable fieldName="trader_name" />
<InfoRow label="AI模型" value={getShortName(traderData.ai_model).toUpperCase()} />
<InfoRow label="交易所" value={getShortName(traderData.exchange_id).toUpperCase()} />
<InfoRow label="初始余额" value={`$${traderData.initial_balance.toLocaleString()}`} />
</div>
</div>
{/* Trading Configuration */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
</h3>
<div className="space-y-3">
<InfoRow label="保证金模式" value={traderData.is_cross_margin ? '全仓' : '逐仓'} />
<InfoRow label="BTC/ETH 杠杆" value={`${traderData.btc_eth_leverage}x`} />
<InfoRow label="山寨币杠杆" value={`${traderData.altcoin_leverage}x`} />
<InfoRow
label="交易币种"
value={traderData.trading_symbols || '使用默认币种'}
copyable
fieldName="trading_symbols"
/>
</div>
</div>
{/* Signal Sources */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
📡
</h3>
<div className="space-y-3">
<InfoRow label="Coin Pool 信号" value={traderData.use_coin_pool} />
<InfoRow label="OI Top 信号" value={traderData.use_oi_top} />
</div>
</div>
{/* Custom Prompt */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[#EAECEF] flex items-center gap-2">
💬
</h3>
{traderData.custom_prompt && (
<CopyButton text={traderData.custom_prompt} fieldName="custom_prompt" />
)}
</div>
<div className="space-y-3">
<InfoRow label="覆盖默认提示词" value={traderData.override_base_prompt} />
{traderData.custom_prompt ? (
<div>
<div className="text-sm text-[#848E9C] mb-2">
{traderData.override_base_prompt ? '自定义提示词' : '附加提示词'}
</div>
<div
className="p-3 rounded border text-sm text-[#EAECEF] font-mono leading-relaxed max-h-48 overflow-y-auto"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
whiteSpace: 'pre-wrap'
}}
>
{traderData.custom_prompt}
</div>
</div>
) : (
<div className="text-sm text-[#848E9C] italic p-3 rounded border" style={{ border: '1px solid #2B3139' }}>
使
</div>
)}
</div>
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<button
onClick={onClose}
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
>
</button>
<button
onClick={() => copyToClipboard(JSON.stringify(traderData, null, 2), 'full_config')}
className="px-6 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 font-medium shadow-lg"
>
{copiedField === 'full_config' ? '✓ 已复制配置' : '📋 复制完整配置'}
</button>
</div>
</div>
</div>
);
}
+38 -8
View File
@@ -172,6 +172,25 @@ export const translations = {
useOfficialAPI: 'Use official API service', useOfficialAPI: 'Use official API service',
useCustomAPI: 'Use custom API endpoint', useCustomAPI: 'Use custom API endpoint',
// Exchange Configuration
secretKey: 'Secret Key',
privateKey: 'Private Key',
walletAddress: 'Wallet Address',
user: 'User',
signer: 'Signer',
passphrase: 'Passphrase',
enterPrivateKey: 'Enter Private Key',
enterWalletAddress: 'Enter Wallet Address',
enterUser: 'Enter User',
enterSigner: 'Enter Signer Address',
enterSecretKey: 'Enter Secret Key',
enterPassphrase: 'Enter Passphrase (Required for OKX)',
hyperliquidPrivateKeyDesc: 'Hyperliquid uses private key for trading authentication',
hyperliquidWalletAddressDesc: 'Wallet address corresponding to the private key',
testnetDescription: 'Enable to connect to exchange test environment for simulated trading',
securityWarning: 'Security Warning',
saveConfiguration: 'Save Configuration',
// Trader Configuration // Trader Configuration
positionMode: 'Position Mode', positionMode: 'Position Mode',
crossMarginMode: 'Cross Margin', crossMarginMode: 'Cross Margin',
@@ -227,10 +246,6 @@ export const translations = {
addExchange: 'Add Exchange', addExchange: 'Add Exchange',
confirmDeleteExchange: 'Are you sure you want to delete this exchange configuration?', confirmDeleteExchange: 'Are you sure you want to delete this exchange configuration?',
pleaseSelectExchange: 'Please select an exchange', pleaseSelectExchange: 'Please select an exchange',
enterSecretKey: 'Enter secret key',
enterPassphrase: 'Enter Passphrase (Required for OKX)',
testnetDescription: 'Enable to connect to exchange test environment for simulated trading',
securityWarning: 'Security Warning',
exchangeConfigWarning1: '• API keys will be encrypted, recommend using read-only or futures trading permissions', exchangeConfigWarning1: '• API keys will be encrypted, recommend using read-only or futures trading permissions',
exchangeConfigWarning2: '• Do not grant withdrawal permissions to ensure fund security', exchangeConfigWarning2: '• Do not grant withdrawal permissions to ensure fund security',
exchangeConfigWarning3: '• After deleting configuration, related traders will not be able to trade', exchangeConfigWarning3: '• After deleting configuration, related traders will not be able to trade',
@@ -474,6 +489,25 @@ export const translations = {
useOfficialAPI: '使用官方API服务', useOfficialAPI: '使用官方API服务',
useCustomAPI: '使用自定义API端点', useCustomAPI: '使用自定义API端点',
// Exchange Configuration
secretKey: '密钥',
privateKey: '私钥',
walletAddress: '钱包地址',
user: '用户名',
signer: '签名者',
passphrase: '口令',
enterSecretKey: '输入密钥',
enterPrivateKey: '输入私钥',
enterWalletAddress: '输入钱包地址',
enterUser: '输入用户名',
enterSigner: '输入签名者地址',
enterPassphrase: '输入Passphrase (OKX必填)',
hyperliquidPrivateKeyDesc: 'Hyperliquid 使用私钥进行交易认证',
hyperliquidWalletAddressDesc: '与私钥对应的钱包地址',
testnetDescription: '启用后将连接到交易所测试环境,用于模拟交易',
securityWarning: '安全提示',
saveConfiguration: '保存配置',
// Trader Configuration // Trader Configuration
positionMode: '仓位模式', positionMode: '仓位模式',
crossMarginMode: '全仓模式', crossMarginMode: '全仓模式',
@@ -529,10 +563,6 @@ export const translations = {
addExchange: '添加交易所', addExchange: '添加交易所',
confirmDeleteExchange: '确定要删除此交易所配置吗?', confirmDeleteExchange: '确定要删除此交易所配置吗?',
pleaseSelectExchange: '请选择交易所', pleaseSelectExchange: '请选择交易所',
enterSecretKey: '输入密钥',
enterPassphrase: '输入Passphrase (OKX必填)',
testnetDescription: '启用后将连接到交易所测试环境,用于模拟交易',
securityWarning: '安全提示',
exchangeConfigWarning1: '• API密钥将被加密存储,建议使用只读或期货交易权限', exchangeConfigWarning1: '• API密钥将被加密存储,建议使用只读或期货交易权限',
exchangeConfigWarning2: '• 不要授予提现权限,确保资金安全', exchangeConfigWarning2: '• 不要授予提现权限,确保资金安全',
exchangeConfigWarning3: '• 删除配置后,相关交易员将无法正常交易', exchangeConfigWarning3: '• 删除配置后,相关交易员将无法正常交易',
+18
View File
@@ -182,3 +182,21 @@ export interface CompetitionData {
traders: CompetitionTraderData[]; traders: CompetitionTraderData[];
count: number; count: number;
} }
// Trader Configuration Data for View Modal
export interface TraderConfigData {
trader_id?: string;
trader_name: string;
ai_model: string;
exchange_id: string;
btc_eth_leverage: number;
altcoin_leverage: number;
trading_symbols: string;
custom_prompt: string;
override_base_prompt: boolean;
is_cross_margin: boolean;
use_coin_pool: boolean;
use_oi_top: boolean;
initial_balance: number;
is_running: boolean;
}