mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-07 03:07:56 +08:00
89085173f9
* feat: remove admin mode * feat: bugfix * feat(crypto): 添加RSA-OAEP + AES-GCM混合加密服务 - 实现CryptoService加密服务,支持RSA-OAEP-2048 + AES-256-GCM混合加密 - 集成数据库层加密,自动加密存储敏感字段(API密钥、私钥等) - 支持环境变量DATA_ENCRYPTION_KEY配置数据加密密钥 - 适配SQLite数据库加密存储(从PostgreSQL移植) - 保持Hyperliquid代理钱包处理兼容性 - 更新.gitignore以正确处理crypto模块代码 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(scripts): 添加加密环境一键设置脚本 - setup_encryption.sh: 一键生成RSA密钥对+数据加密密钥+JWT密钥 - generate_rsa_keys.sh: 专业的RSA-2048密钥对生成工具 - generate_data_key.sh: 生成AES-256数据加密密钥和JWT认证密钥 - ENCRYPTION_README.md: 详细的加密系统说明文档 - 支持自动检测现有密钥并只生成缺失的密钥 - 完善的权限管理和安全验证 - 兼容macOS和Linux的跨平台支持 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(api): 添加加密API端点和Gin框架集成 - 新增CryptoHandler处理加密相关API请求 - 提供/api/crypto/public-key端点获取RSA公钥 - 提供/api/crypto/decrypt端点解密敏感数据 - 适配Gin框架的HTTP处理器格式 - 集成CryptoService到API服务器 - 支持前端加密数据传输和解密 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(web): 添加前端加密服务和两阶段密钥输入组件 - CryptoService: Web Crypto API集成,支持RSA-OAEP加密 - TwoStageKeyModal: 安全的两阶段私钥输入组件,支持剪贴板混淆 - 完善国际化翻译支持加密相关UI文本 - 修复TypeScript类型错误和编译问题 - 支持前端敏感数据加密传输到后端 - 增强用户隐私保护和数据安全 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(auth): 增强JWT认证安全性 - 优先使用环境变量JWT_SECRET而不是数据库配置 - 支持通过.env文件安全配置JWT认证密钥 - 保留数据库配置作为回退机制 - 改进JWT密钥来源日志显示 - 增强系统启动时的安全配置检查 - 支持运行时动态JWT密钥切换 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(docker): 集成加密环境变量到Docker部署 - 添加DATA_ENCRYPTION_KEY环境变量传递到容器 - 添加JWT_SECRET环境变量支持 - 挂载secrets目录使容器可访问RSA密钥文件 - 确保容器内加密服务正常工作 - 解决容器启动失败和加密初始化问题 - 完善Docker Compose加密环境配置 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(start): 集成自动加密环境检测和设置 - 增强check_encryption()函数检测JWT_SECRET和DATA_ENCRYPTION_KEY - 自动运行setup_encryption.sh当检测到缺失密钥时 - 改进加密状态显示,包含RSA+AES+JWT全套加密信息 - 优化用户体验,提供清晰的加密配置反馈 - 支持一键设置完整加密环境 - 确保容器启动前加密环境就绪 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat: format fix * fix(security): 修复前端模型和交易所配置敏感数据明文传输 - 在handleSaveModelConfig中对API密钥进行RSA-OAEP加密 - 在handleSaveExchangeConfig中对API密钥、Secret密钥和Aster私钥进行加密 - 只有非空敏感数据才进行加密处理 - 添加加密失败错误处理和用户友好提示 - 增加encryptionFailed翻译键的中英文支持 - 使用用户ID和会话ID作为加密上下文增强安全性 这修复了之前敏感数据在网络传输中以明文形式发送的安全漏洞。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix(crypto): 修复后端加密服务集成和缺失的加密端点 - 添加Server结构体缺少的cryptoService字段 - 实现handleUpdateModelConfigsEncrypted处理器用于模型配置加密传输 - 修复handleUpdateExchangeConfigsEncrypted中的函数调用 - 在前端API中添加updateModelConfigsEncrypted方法 - 统一RSA密钥路径从secrets/rsa_key改为keys/rsa_private.key - 确保前端可以使用加密端点安全传输敏感数据 - 兼容原有加密通信模式和二段输入私钥功能 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> --------- Co-authored-by: icy <icyoung520@gmail.com> Co-authored-by: tinkle-community <tinklefund@gmail.com>
356 lines
12 KiB
Go
356 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"nofx/api"
|
|
"nofx/auth"
|
|
"nofx/config"
|
|
"nofx/crypto"
|
|
"nofx/manager"
|
|
"nofx/market"
|
|
"nofx/pool"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
// ConfigFile 配置文件结构,只包含需要同步到数据库的字段
|
|
// TODO 现在与config.Config相同,未来会被替换, 现在为了兼容性不得不保留当前文件
|
|
type ConfigFile struct {
|
|
BetaMode bool `json:"beta_mode"`
|
|
APIServerPort int `json:"api_server_port"`
|
|
UseDefaultCoins bool `json:"use_default_coins"`
|
|
DefaultCoins []string `json:"default_coins"`
|
|
CoinPoolAPIURL string `json:"coin_pool_api_url"`
|
|
OITopAPIURL string `json:"oi_top_api_url"`
|
|
MaxDailyLoss float64 `json:"max_daily_loss"`
|
|
MaxDrawdown float64 `json:"max_drawdown"`
|
|
StopTradingMinutes int `json:"stop_trading_minutes"`
|
|
Leverage config.LeverageConfig `json:"leverage"`
|
|
JWTSecret string `json:"jwt_secret"`
|
|
DataKLineTime string `json:"data_k_line_time"`
|
|
Log *config.LogConfig `json:"log"` // 日志配置
|
|
}
|
|
|
|
// loadConfigFile 读取并解析config.json文件
|
|
func loadConfigFile() (*ConfigFile, error) {
|
|
// 检查config.json是否存在
|
|
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
|
|
log.Printf("📄 config.json不存在,使用默认配置")
|
|
return &ConfigFile{}, nil
|
|
}
|
|
|
|
// 读取config.json
|
|
data, err := os.ReadFile("config.json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取config.json失败: %w", err)
|
|
}
|
|
|
|
// 解析JSON
|
|
var configFile ConfigFile
|
|
if err := json.Unmarshal(data, &configFile); err != nil {
|
|
return nil, fmt.Errorf("解析config.json失败: %w", err)
|
|
}
|
|
|
|
return &configFile, nil
|
|
}
|
|
|
|
// syncConfigToDatabase 将配置同步到数据库
|
|
func syncConfigToDatabase(database *config.Database, configFile *ConfigFile) error {
|
|
if configFile == nil {
|
|
return nil
|
|
}
|
|
|
|
log.Printf("🔄 开始同步config.json到数据库...")
|
|
|
|
// 同步各配置项到数据库
|
|
configs := map[string]string{
|
|
"beta_mode": fmt.Sprintf("%t", configFile.BetaMode),
|
|
"api_server_port": strconv.Itoa(configFile.APIServerPort),
|
|
"use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins),
|
|
"coin_pool_api_url": configFile.CoinPoolAPIURL,
|
|
"oi_top_api_url": configFile.OITopAPIURL,
|
|
"max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss),
|
|
"max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown),
|
|
"stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes),
|
|
}
|
|
|
|
// 同步default_coins(转换为JSON字符串存储)
|
|
if len(configFile.DefaultCoins) > 0 {
|
|
defaultCoinsJSON, err := json.Marshal(configFile.DefaultCoins)
|
|
if err == nil {
|
|
configs["default_coins"] = string(defaultCoinsJSON)
|
|
}
|
|
}
|
|
|
|
// 同步杠杆配置
|
|
if configFile.Leverage.BTCETHLeverage > 0 {
|
|
configs["btc_eth_leverage"] = strconv.Itoa(configFile.Leverage.BTCETHLeverage)
|
|
}
|
|
if configFile.Leverage.AltcoinLeverage > 0 {
|
|
configs["altcoin_leverage"] = strconv.Itoa(configFile.Leverage.AltcoinLeverage)
|
|
}
|
|
|
|
// 如果JWT密钥不为空,也同步
|
|
if configFile.JWTSecret != "" {
|
|
configs["jwt_secret"] = configFile.JWTSecret
|
|
}
|
|
|
|
// 更新数据库配置
|
|
for key, value := range configs {
|
|
if err := database.SetSystemConfig(key, value); err != nil {
|
|
log.Printf("⚠️ 更新配置 %s 失败: %v", key, err)
|
|
} else {
|
|
log.Printf("✓ 同步配置: %s = %s", key, value)
|
|
}
|
|
}
|
|
|
|
log.Printf("✅ config.json同步完成")
|
|
return nil
|
|
}
|
|
|
|
// loadBetaCodesToDatabase 加载内测码文件到数据库
|
|
func loadBetaCodesToDatabase(database *config.Database) error {
|
|
betaCodeFile := "beta_codes.txt"
|
|
|
|
// 检查内测码文件是否存在
|
|
if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) {
|
|
log.Printf("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile)
|
|
return nil
|
|
}
|
|
|
|
// 获取文件信息
|
|
fileInfo, err := os.Stat(betaCodeFile)
|
|
if err != nil {
|
|
return fmt.Errorf("获取内测码文件信息失败: %w", err)
|
|
}
|
|
|
|
log.Printf("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024)
|
|
|
|
// 加载内测码到数据库
|
|
err = database.LoadBetaCodesFromFile(betaCodeFile)
|
|
if err != nil {
|
|
return fmt.Errorf("加载内测码失败: %w", err)
|
|
}
|
|
|
|
// 显示统计信息
|
|
total, used, err := database.GetBetaCodeStats()
|
|
if err != nil {
|
|
log.Printf("⚠️ 获取内测码统计失败: %v", err)
|
|
} else {
|
|
log.Printf("✅ 内测码加载完成: 总计 %d 个,已使用 %d 个,剩余 %d 个", total, used, total-used)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
fmt.Println("╔════════════════════════════════════════════════════════════╗")
|
|
fmt.Println("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║")
|
|
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
|
fmt.Println()
|
|
|
|
// Load environment variables from .env file if present (for local/dev runs)
|
|
// In Docker Compose, variables are injected by the runtime and this is harmless.
|
|
_ = godotenv.Load()
|
|
|
|
// 初始化数据库配置
|
|
dbPath := "config.db"
|
|
if len(os.Args) > 1 {
|
|
dbPath = os.Args[1]
|
|
}
|
|
|
|
// 读取配置文件
|
|
configFile, err := loadConfigFile()
|
|
if err != nil {
|
|
log.Fatalf("❌ 读取config.json失败: %v", err)
|
|
}
|
|
|
|
log.Printf("📋 初始化配置数据库: %s", dbPath)
|
|
database, err := config.NewDatabase(dbPath)
|
|
if err != nil {
|
|
log.Fatalf("❌ 初始化数据库失败: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
// 初始化加密服务
|
|
log.Printf("🔐 初始化加密服务...")
|
|
cryptoService, err := crypto.NewCryptoService("secrets/rsa_key")
|
|
if err != nil {
|
|
log.Fatalf("❌ 初始化加密服务失败: %v", err)
|
|
}
|
|
database.SetCryptoService(cryptoService)
|
|
log.Printf("✅ 加密服务初始化成功")
|
|
|
|
// 同步config.json到数据库
|
|
if err := syncConfigToDatabase(database, configFile); err != nil {
|
|
log.Printf("⚠️ 同步config.json到数据库失败: %v", err)
|
|
}
|
|
|
|
// 加载内测码到数据库
|
|
if err := loadBetaCodesToDatabase(database); err != nil {
|
|
log.Printf("⚠️ 加载内测码到数据库失败: %v", err)
|
|
}
|
|
|
|
// 获取系统配置
|
|
useDefaultCoinsStr, _ := database.GetSystemConfig("use_default_coins")
|
|
useDefaultCoins := useDefaultCoinsStr == "true"
|
|
apiPortStr, _ := database.GetSystemConfig("api_server_port")
|
|
|
|
|
|
// 设置JWT密钥(优先使用环境变量)
|
|
jwtSecret := strings.TrimSpace(os.Getenv("JWT_SECRET"))
|
|
if jwtSecret == "" {
|
|
// 回退到数据库配置
|
|
jwtSecret, _ = database.GetSystemConfig("jwt_secret")
|
|
if jwtSecret == "" {
|
|
jwtSecret = "your-jwt-secret-key-change-in-production-make-it-long-and-random"
|
|
log.Printf("⚠️ 使用默认JWT密钥,建议使用加密设置脚本生成安全密钥")
|
|
} else {
|
|
log.Printf("🔑 使用数据库中JWT密钥")
|
|
}
|
|
} else {
|
|
log.Printf("🔑 使用环境变量JWT密钥")
|
|
}
|
|
auth.SetJWTSecret(jwtSecret)
|
|
|
|
// 管理员模式下需要管理员密码,缺失则退出
|
|
|
|
log.Printf("✓ 配置数据库初始化成功")
|
|
fmt.Println()
|
|
|
|
// 从数据库读取默认主流币种列表
|
|
defaultCoinsJSON, _ := database.GetSystemConfig("default_coins")
|
|
var defaultCoins []string
|
|
|
|
if defaultCoinsJSON != "" {
|
|
// 尝试从JSON解析
|
|
if err := json.Unmarshal([]byte(defaultCoinsJSON), &defaultCoins); err != nil {
|
|
log.Printf("⚠️ 解析default_coins配置失败: %v,使用硬编码默认值", err)
|
|
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
|
|
} else {
|
|
log.Printf("✓ 从数据库加载默认币种列表(共%d个): %v", len(defaultCoins), defaultCoins)
|
|
}
|
|
} else {
|
|
// 如果数据库中没有配置,使用硬编码默认值
|
|
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
|
|
log.Printf("⚠️ 数据库中未配置default_coins,使用硬编码默认值")
|
|
}
|
|
|
|
pool.SetDefaultCoins(defaultCoins)
|
|
// 设置是否使用默认主流币种
|
|
pool.SetUseDefaultCoins(useDefaultCoins)
|
|
if useDefaultCoins {
|
|
log.Printf("✓ 已启用默认主流币种列表")
|
|
}
|
|
|
|
// 设置币种池API URL
|
|
coinPoolAPIURL, _ := database.GetSystemConfig("coin_pool_api_url")
|
|
if coinPoolAPIURL != "" {
|
|
pool.SetCoinPoolAPI(coinPoolAPIURL)
|
|
log.Printf("✓ 已配置AI500币种池API")
|
|
}
|
|
|
|
oiTopAPIURL, _ := database.GetSystemConfig("oi_top_api_url")
|
|
if oiTopAPIURL != "" {
|
|
pool.SetOITopAPI(oiTopAPIURL)
|
|
log.Printf("✓ 已配置OI Top API")
|
|
}
|
|
|
|
// 创建TraderManager
|
|
traderManager := manager.NewTraderManager()
|
|
|
|
// 从数据库加载所有交易员到内存
|
|
err = traderManager.LoadTradersFromDatabase(database)
|
|
if err != nil {
|
|
log.Fatalf("❌ 加载交易员失败: %v", err)
|
|
}
|
|
|
|
// 获取数据库中的所有交易员配置(用于显示,使用default用户)
|
|
traders, err := database.GetTraders("default")
|
|
if err != nil {
|
|
log.Fatalf("❌ 获取交易员列表失败: %v", err)
|
|
}
|
|
|
|
// 显示加载的交易员信息
|
|
fmt.Println()
|
|
fmt.Println("🤖 数据库中的AI交易员配置:")
|
|
if len(traders) == 0 {
|
|
fmt.Println(" • 暂无配置的交易员,请通过Web界面创建")
|
|
} else {
|
|
for _, trader := range traders {
|
|
status := "停止"
|
|
if trader.IsRunning {
|
|
status = "运行中"
|
|
}
|
|
fmt.Printf(" • %s (%s + %s) - 初始资金: %.0f USDT [%s]\n",
|
|
trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID),
|
|
trader.InitialBalance, status)
|
|
}
|
|
}
|
|
|
|
// 创建初始化上下文
|
|
// TODO : 传入实际配置, 现在并未实际使用,未来所有模块初始化都将通过上下文传递配置
|
|
// ctx := bootstrap.NewContext(&config.Config{})
|
|
|
|
// // 执行所有初始化钩子
|
|
// if err := bootstrap.Run(ctx); err != nil {
|
|
// log.Fatalf("初始化失败: %v", err)
|
|
// }
|
|
|
|
fmt.Println()
|
|
fmt.Println("🤖 AI全权决策模式:")
|
|
fmt.Printf(" • AI将自主决定每笔交易的杠杆倍数(山寨币最高5倍,BTC/ETH最高5倍)\n")
|
|
fmt.Println(" • AI将自主决定每笔交易的仓位大小")
|
|
fmt.Println(" • AI将自主设置止损和止盈价格")
|
|
fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
|
|
fmt.Println()
|
|
fmt.Println("⚠️ 风险提示: AI自动交易有风险,建议小额资金测试!")
|
|
fmt.Println()
|
|
fmt.Println("按 Ctrl+C 停止运行")
|
|
fmt.Println(strings.Repeat("=", 60))
|
|
fmt.Println()
|
|
|
|
// 获取API服务器端口
|
|
apiPort := 8080 // 默认端口
|
|
if apiPortStr != "" {
|
|
if port, err := strconv.Atoi(apiPortStr); err == nil {
|
|
apiPort = port
|
|
}
|
|
}
|
|
|
|
// 创建并启动API服务器
|
|
apiServer := api.NewServer(traderManager, database, cryptoService, apiPort)
|
|
go func() {
|
|
if err := apiServer.Start(); err != nil {
|
|
log.Printf("❌ API服务器错误: %v", err)
|
|
}
|
|
}()
|
|
|
|
// 启动流行情数据 - 默认使用所有交易员设置的币种 如果没有设置币种 则优先使用系统默认
|
|
go market.NewWSMonitor(150).Start(database.GetCustomCoins())
|
|
//go market.NewWSMonitor(150).Start([]string{}) //这里是一个使用方式 传入空的话 则使用market市场的所有币种
|
|
// 设置优雅退出
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
|
|
// TODO: 启动数据库中配置为运行状态的交易员
|
|
// traderManager.StartAll()
|
|
|
|
// 等待退出信号
|
|
<-sigChan
|
|
fmt.Println()
|
|
fmt.Println()
|
|
log.Println("📛 收到退出信号,正在停止所有trader...")
|
|
traderManager.StopAll()
|
|
|
|
fmt.Println()
|
|
fmt.Println("👋 感谢使用AI交易系统!")
|
|
}
|