mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 15:26:55 +08:00
refactor: standardize code comments
This commit is contained in:
@@ -69,7 +69,7 @@ func (s *Server) handleBacktestStart(c *gin.Context) {
|
||||
cfg.PromptTemplate = "default"
|
||||
}
|
||||
if _, err := decision.GetPromptTemplate(cfg.PromptTemplate); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("提示词模板不存在: %s", cfg.PromptTemplate)})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Prompt template does not exist: %s", cfg.PromptTemplate)})
|
||||
return
|
||||
}
|
||||
cfg.CustomPrompt = strings.TrimSpace(cfg.CustomPrompt)
|
||||
@@ -498,9 +498,9 @@ func writeBacktestAccessError(c *gin.Context, err error) bool {
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, errBacktestForbidden):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该回测任务"})
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "No permission to access this backtest task"})
|
||||
case errors.Is(err, os.ErrNotExist), errors.Is(err, sql.ErrNoRows):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "回测任务不存在"})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Backtest task does not exist"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
@@ -512,7 +512,7 @@ func (s *Server) resolveBacktestAIConfig(cfg *backtest.BacktestConfig, userID st
|
||||
return fmt.Errorf("config is nil")
|
||||
}
|
||||
if s.store == nil {
|
||||
return fmt.Errorf("系统数据库未就绪,无法加载AI模型配置")
|
||||
return fmt.Errorf("System database not ready, cannot load AI model configuration")
|
||||
}
|
||||
|
||||
cfg.UserID = normalizeUserID(userID)
|
||||
@@ -525,7 +525,7 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error {
|
||||
return fmt.Errorf("config is nil")
|
||||
}
|
||||
if s.store == nil {
|
||||
return fmt.Errorf("系统数据库未就绪,无法加载AI模型配置")
|
||||
return fmt.Errorf("System database not ready, cannot load AI model configuration")
|
||||
}
|
||||
|
||||
cfg.UserID = normalizeUserID(cfg.UserID)
|
||||
@@ -539,23 +539,23 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error {
|
||||
if modelID != "" {
|
||||
model, err = s.store.AIModel().Get(cfg.UserID, modelID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载AI模型失败: %w", err)
|
||||
return fmt.Errorf("Failed to load AI model: %w", err)
|
||||
}
|
||||
} else {
|
||||
model, err = s.store.AIModel().GetDefault(cfg.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("未找到可用的AI模型: %w", err)
|
||||
return fmt.Errorf("No available AI model found: %w", err)
|
||||
}
|
||||
cfg.AIModelID = model.ID
|
||||
}
|
||||
|
||||
if !model.Enabled {
|
||||
return fmt.Errorf("AI模型 %s 尚未启用", model.Name)
|
||||
return fmt.Errorf("AI model %s is not enabled yet", model.Name)
|
||||
}
|
||||
|
||||
apiKey := strings.TrimSpace(model.APIKey)
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("AI模型 %s 缺少API Key,请先在系统中配置", model.Name)
|
||||
return fmt.Errorf("AI model %s is missing API Key, please configure it in the system first", model.Name)
|
||||
}
|
||||
|
||||
cfg.AICfg.Provider = strings.ToLower(model.Provider)
|
||||
@@ -569,10 +569,10 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error {
|
||||
|
||||
if cfg.AICfg.Provider == "custom" {
|
||||
if cfg.AICfg.BaseURL == "" {
|
||||
return fmt.Errorf("自定义AI模型需要配置 API 地址")
|
||||
return fmt.Errorf("Custom AI model requires API URL configuration")
|
||||
}
|
||||
if cfg.AICfg.Model == "" {
|
||||
return fmt.Errorf("自定义AI模型需要配置模型名称")
|
||||
return fmt.Errorf("Custom AI model requires model name configuration")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,21 +8,21 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CryptoHandler 加密 API 處理器
|
||||
// CryptoHandler Encryption API handler
|
||||
type CryptoHandler struct {
|
||||
cryptoService *crypto.CryptoService
|
||||
}
|
||||
|
||||
// NewCryptoHandler 創建加密處理器
|
||||
// NewCryptoHandler Creates encryption handler
|
||||
func NewCryptoHandler(cryptoService *crypto.CryptoService) *CryptoHandler {
|
||||
return &CryptoHandler{
|
||||
cryptoService: cryptoService,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公鑰端點 ====================
|
||||
// ==================== Public Key Endpoint ====================
|
||||
|
||||
// HandleGetPublicKey 獲取伺服器公鑰
|
||||
// HandleGetPublicKey Get server public key
|
||||
func (h *CryptoHandler) HandleGetPublicKey(c *gin.Context) {
|
||||
publicKey := h.cryptoService.GetPublicKeyPEM()
|
||||
|
||||
@@ -32,9 +32,9 @@ func (h *CryptoHandler) HandleGetPublicKey(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 加密數據解密端點 ====================
|
||||
// ==================== Encrypted Data Decryption Endpoint ====================
|
||||
|
||||
// HandleDecryptSensitiveData 解密客戶端傳送的加密数据
|
||||
// HandleDecryptSensitiveData Decrypt encrypted data sent from client
|
||||
func (h *CryptoHandler) HandleDecryptSensitiveData(c *gin.Context) {
|
||||
var payload crypto.EncryptedPayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
@@ -42,10 +42,10 @@ func (h *CryptoHandler) HandleDecryptSensitiveData(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解密
|
||||
// Decrypt
|
||||
decrypted, err := h.cryptoService.DecryptSensitiveData(&payload)
|
||||
if err != nil {
|
||||
log.Printf("❌ 解密失敗: %v", err)
|
||||
log.Printf("❌ Decryption failed: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Decryption failed"})
|
||||
return
|
||||
}
|
||||
@@ -55,18 +55,18 @@ func (h *CryptoHandler) HandleDecryptSensitiveData(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 審計日誌查詢端點 ====================
|
||||
// ==================== Audit Log Query Endpoint ====================
|
||||
|
||||
// 删除审计日志相关功能,在当前简化的实现中不需要
|
||||
// Audit log functionality removed, not needed in current simplified implementation
|
||||
|
||||
// ==================== 工具函數 ====================
|
||||
// ==================== Utility Functions ====================
|
||||
|
||||
// isValidPrivateKey 驗證私鑰格式
|
||||
// isValidPrivateKey Validate private key format
|
||||
func isValidPrivateKey(key string) bool {
|
||||
// EVM 私鑰: 64 位十六進制 (可選 0x 前綴)
|
||||
// EVM private key: 64 hex characters (optional 0x prefix)
|
||||
if len(key) == 64 || (len(key) == 66 && key[:2] == "0x") {
|
||||
return true
|
||||
}
|
||||
// TODO: 添加其他鏈的驗證
|
||||
// TODO: Add validation for other chains
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// MockUser 模擬用戶結構
|
||||
// MockUser Mock user structure
|
||||
type MockUser struct {
|
||||
ID int
|
||||
Email string
|
||||
@@ -12,7 +12,7 @@ type MockUser struct {
|
||||
OTPVerified bool
|
||||
}
|
||||
|
||||
// TestOTPRefetchLogic 測試 OTP 重新獲取邏輯
|
||||
// TestOTPRefetchLogic Test OTP refetch logic
|
||||
func TestOTPRefetchLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -22,14 +22,14 @@ func TestOTPRefetchLogic(t *testing.T) {
|
||||
expectedMessage string
|
||||
}{
|
||||
{
|
||||
name: "新用戶註冊_郵箱不存在",
|
||||
name: "New user registration - email does not exist",
|
||||
existingUser: nil,
|
||||
userExists: false,
|
||||
expectedAction: "create_new",
|
||||
expectedMessage: "創建新用戶",
|
||||
expectedMessage: "Create new user",
|
||||
},
|
||||
{
|
||||
name: "未完成OTP驗證_允許重新獲取",
|
||||
name: "Incomplete OTP verification - allow refetch",
|
||||
existingUser: &MockUser{
|
||||
ID: 1,
|
||||
Email: "test@example.com",
|
||||
@@ -38,10 +38,10 @@ func TestOTPRefetchLogic(t *testing.T) {
|
||||
},
|
||||
userExists: true,
|
||||
expectedAction: "allow_refetch",
|
||||
expectedMessage: "检测到未完成的注册,请继续完成OTP设置",
|
||||
expectedMessage: "Incomplete registration detected, please continue OTP setup",
|
||||
},
|
||||
{
|
||||
name: "已完成OTP驗證_拒絕重複註冊",
|
||||
name: "Completed OTP verification - reject duplicate registration",
|
||||
existingUser: &MockUser{
|
||||
ID: 2,
|
||||
Email: "verified@example.com",
|
||||
@@ -50,45 +50,45 @@ func TestOTPRefetchLogic(t *testing.T) {
|
||||
},
|
||||
userExists: true,
|
||||
expectedAction: "reject_duplicate",
|
||||
expectedMessage: "邮箱已被注册",
|
||||
expectedMessage: "Email already registered",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 模擬邏輯處理流程
|
||||
// Simulate logic processing flow
|
||||
var actualAction string
|
||||
var actualMessage string
|
||||
|
||||
if !tt.userExists {
|
||||
// 用戶不存在,創建新用戶
|
||||
// User does not exist, create new user
|
||||
actualAction = "create_new"
|
||||
actualMessage = "創建新用戶"
|
||||
actualMessage = "Create new user"
|
||||
} else {
|
||||
// 用戶已存在,檢查 OTP 驗證狀態
|
||||
// User exists, check OTP verification status
|
||||
if !tt.existingUser.OTPVerified {
|
||||
// 未完成 OTP 驗證,允許重新獲取
|
||||
// OTP verification incomplete, allow refetch
|
||||
actualAction = "allow_refetch"
|
||||
actualMessage = "检测到未完成的注册,请继续完成OTP设置"
|
||||
actualMessage = "Incomplete registration detected, please continue OTP setup"
|
||||
} else {
|
||||
// 已完成驗證,拒絕重複註冊
|
||||
// Verification completed, reject duplicate registration
|
||||
actualAction = "reject_duplicate"
|
||||
actualMessage = "邮箱已被注册"
|
||||
actualMessage = "Email already registered"
|
||||
}
|
||||
}
|
||||
|
||||
// 驗證結果
|
||||
// Verify results
|
||||
if actualAction != tt.expectedAction {
|
||||
t.Errorf("Action 不符: got %s, want %s", actualAction, tt.expectedAction)
|
||||
t.Errorf("Action mismatch: got %s, want %s", actualAction, tt.expectedAction)
|
||||
}
|
||||
if actualMessage != tt.expectedMessage {
|
||||
t.Errorf("Message 不符: got %s, want %s", actualMessage, tt.expectedMessage)
|
||||
t.Errorf("Message mismatch: got %s, want %s", actualMessage, tt.expectedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOTPVerificationStates 測試 OTP 驗證狀態判斷
|
||||
// TestOTPVerificationStates Test OTP verification state determination
|
||||
func TestOTPVerificationStates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -96,12 +96,12 @@ func TestOTPVerificationStates(t *testing.T) {
|
||||
shouldAllowRefetch bool
|
||||
}{
|
||||
{
|
||||
name: "OTP已驗證_不允許重新獲取",
|
||||
name: "OTP verified - disallow refetch",
|
||||
otpVerified: true,
|
||||
shouldAllowRefetch: false,
|
||||
},
|
||||
{
|
||||
name: "OTP未驗證_允許重新獲取",
|
||||
name: "OTP not verified - allow refetch",
|
||||
otpVerified: false,
|
||||
shouldAllowRefetch: true,
|
||||
},
|
||||
@@ -109,7 +109,7 @@ func TestOTPVerificationStates(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 模擬驗證邏輯
|
||||
// Simulate verification logic
|
||||
allowRefetch := !tt.otpVerified
|
||||
|
||||
if allowRefetch != tt.shouldAllowRefetch {
|
||||
@@ -120,72 +120,72 @@ func TestOTPVerificationStates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegistrationFlow 測試完整註冊流程的邏輯分支
|
||||
// TestRegistrationFlow Test complete registration flow logic branches
|
||||
func TestRegistrationFlow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scenario string
|
||||
userExists bool
|
||||
otpVerified bool
|
||||
expectHTTPCode int // 模擬的 HTTP 狀態碼
|
||||
expectHTTPCode int // Simulated HTTP status code
|
||||
expectResponse string
|
||||
}{
|
||||
{
|
||||
name: "場景1_新用戶首次註冊",
|
||||
scenario: "新用戶首次訪問註冊接口",
|
||||
name: "Scenario 1: New user first registration",
|
||||
scenario: "New user first accesses registration endpoint",
|
||||
userExists: false,
|
||||
otpVerified: false,
|
||||
expectHTTPCode: 200,
|
||||
expectResponse: "創建用戶並返回 OTP 設置信息",
|
||||
expectResponse: "Create user and return OTP setup information",
|
||||
},
|
||||
{
|
||||
name: "場景2_用戶中斷註冊後重新訪問",
|
||||
scenario: "用戶之前註冊但未完成 OTP 設置,現在重新訪問",
|
||||
name: "Scenario 2: User re-accesses after interrupting registration",
|
||||
scenario: "User registered previously but did not complete OTP setup, now re-accessing",
|
||||
userExists: true,
|
||||
otpVerified: false,
|
||||
expectHTTPCode: 200,
|
||||
expectResponse: "返回現有用戶的 OTP 信息,允許繼續完成",
|
||||
expectResponse: "Return existing user's OTP information, allow continuation",
|
||||
},
|
||||
{
|
||||
name: "場景3_已註冊用戶嘗試重複註冊",
|
||||
scenario: "用戶已完成註冊,嘗試用同一郵箱再次註冊",
|
||||
name: "Scenario 3: Registered user attempts duplicate registration",
|
||||
scenario: "User already completed registration, attempts to register again with same email",
|
||||
userExists: true,
|
||||
otpVerified: true,
|
||||
expectHTTPCode: 409, // Conflict
|
||||
expectResponse: "邮箱已被注册",
|
||||
expectResponse: "Email already registered",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 模擬註冊流程邏輯
|
||||
// Simulate registration flow logic
|
||||
var actualHTTPCode int
|
||||
var actualResponse string
|
||||
|
||||
if !tt.userExists {
|
||||
// 新用戶,創建並返回 OTP 信息
|
||||
// New user, create and return OTP information
|
||||
actualHTTPCode = 200
|
||||
actualResponse = "創建用戶並返回 OTP 設置信息"
|
||||
actualResponse = "Create user and return OTP setup information"
|
||||
} else {
|
||||
// 用戶已存在
|
||||
// User exists
|
||||
if !tt.otpVerified {
|
||||
// 未完成 OTP 驗證,允許重新獲取
|
||||
// OTP verification incomplete, allow refetch
|
||||
actualHTTPCode = 200
|
||||
actualResponse = "返回現有用戶的 OTP 信息,允許繼續完成"
|
||||
actualResponse = "Return existing user's OTP information, allow continuation"
|
||||
} else {
|
||||
// 已完成驗證,拒絕重複註冊
|
||||
// Verification completed, reject duplicate registration
|
||||
actualHTTPCode = 409
|
||||
actualResponse = "邮箱已被注册"
|
||||
actualResponse = "Email already registered"
|
||||
}
|
||||
}
|
||||
|
||||
// 驗證
|
||||
// Verify
|
||||
if actualHTTPCode != tt.expectHTTPCode {
|
||||
t.Errorf("HTTP code 不符: got %d, want %d (scenario: %s)",
|
||||
t.Errorf("HTTP code mismatch: got %d, want %d (scenario: %s)",
|
||||
actualHTTPCode, tt.expectHTTPCode, tt.scenario)
|
||||
}
|
||||
if actualResponse != tt.expectResponse {
|
||||
t.Errorf("Response 不符: got %s, want %s (scenario: %s)",
|
||||
t.Errorf("Response mismatch: got %s, want %s (scenario: %s)",
|
||||
actualResponse, tt.expectResponse, tt.scenario)
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ func TestRegistrationFlow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEdgeCases 測試邊界情況
|
||||
// TestEdgeCases Test edge cases
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -203,17 +203,17 @@ func TestEdgeCases(t *testing.T) {
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "用戶ID為0_視為新用戶",
|
||||
name: "User ID is 0 - treated as new user",
|
||||
user: &MockUser{
|
||||
ID: 0,
|
||||
Email: "new@example.com",
|
||||
OTPVerified: false,
|
||||
},
|
||||
expectAllow: true,
|
||||
description: "ID為0通常表示用戶還未創建",
|
||||
description: "ID of 0 usually indicates user has not been created yet",
|
||||
},
|
||||
{
|
||||
name: "OTPSecret為空_仍可重新獲取",
|
||||
name: "OTPSecret is empty - still can refetch",
|
||||
user: &MockUser{
|
||||
ID: 1,
|
||||
Email: "test@example.com",
|
||||
@@ -221,10 +221,10 @@ func TestEdgeCases(t *testing.T) {
|
||||
OTPVerified: false,
|
||||
},
|
||||
expectAllow: true,
|
||||
description: "即使 OTPSecret 為空,只要未驗證就允許重新獲取",
|
||||
description: "Even if OTPSecret is empty, as long as not verified, refetch is allowed",
|
||||
},
|
||||
{
|
||||
name: "OTPSecret存在但已驗證_不允許",
|
||||
name: "OTPSecret exists but already verified - not allowed",
|
||||
user: &MockUser{
|
||||
ID: 2,
|
||||
Email: "verified@example.com",
|
||||
@@ -232,13 +232,13 @@ func TestEdgeCases(t *testing.T) {
|
||||
OTPVerified: true,
|
||||
},
|
||||
expectAllow: false,
|
||||
description: "OTP 已驗證的用戶不能重新獲取",
|
||||
description: "Users with verified OTP cannot refetch",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 核心邏輯:只要 OTPVerified 為 false,就允許重新獲取
|
||||
// Core logic: as long as OTPVerified is false, refetch is allowed
|
||||
allowRefetch := !tt.user.OTPVerified
|
||||
|
||||
if allowRefetch != tt.expectAllow {
|
||||
|
||||
974
api/server.go
974
api/server.go
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import (
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
// TestUpdateTraderRequest_SystemPromptTemplate 测试更新交易员时 SystemPromptTemplate 字段是否存在
|
||||
// TestUpdateTraderRequest_SystemPromptTemplate Test whether SystemPromptTemplate field exists when updating trader
|
||||
func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -15,7 +15,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) {
|
||||
expectedPromptTemplate string
|
||||
}{
|
||||
{
|
||||
name: "更新时应该能接收 system_prompt_template=nof1",
|
||||
name: "Should accept system_prompt_template=nof1 during update",
|
||||
requestJSON: `{
|
||||
"name": "Test Trader",
|
||||
"ai_model_id": "gpt-4",
|
||||
@@ -33,7 +33,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) {
|
||||
expectedPromptTemplate: "nof1",
|
||||
},
|
||||
{
|
||||
name: "更新时应该能接收 system_prompt_template=default",
|
||||
name: "Should accept system_prompt_template=default during update",
|
||||
requestJSON: `{
|
||||
"name": "Test Trader",
|
||||
"ai_model_id": "gpt-4",
|
||||
@@ -51,7 +51,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) {
|
||||
expectedPromptTemplate: "default",
|
||||
},
|
||||
{
|
||||
name: "更新时应该能接收 system_prompt_template=custom",
|
||||
name: "Should accept system_prompt_template=custom during update",
|
||||
requestJSON: `{
|
||||
"name": "Test Trader",
|
||||
"ai_model_id": "gpt-4",
|
||||
@@ -72,20 +72,20 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 测试 UpdateTraderRequest 结构体是否能正确解析 system_prompt_template 字段
|
||||
// Test whether UpdateTraderRequest struct can correctly parse system_prompt_template field
|
||||
var req UpdateTraderRequest
|
||||
err := json.Unmarshal([]byte(tt.requestJSON), &req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
|
||||
// ✅ 验证 SystemPromptTemplate 字段是否被正确读取
|
||||
// Verify SystemPromptTemplate field is correctly read
|
||||
if req.SystemPromptTemplate != tt.expectedPromptTemplate {
|
||||
t.Errorf("Expected SystemPromptTemplate=%q, got %q",
|
||||
tt.expectedPromptTemplate, req.SystemPromptTemplate)
|
||||
}
|
||||
|
||||
// 验证其他字段也被正确解析
|
||||
// Verify other fields are also correctly parsed
|
||||
if req.Name != "Test Trader" {
|
||||
t.Errorf("Name not parsed correctly")
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTraderConfigResponse_SystemPromptTemplate 测试获取交易员配置时返回值是否包含 system_prompt_template
|
||||
// TestGetTraderConfigResponse_SystemPromptTemplate Test whether return value contains system_prompt_template when getting trader config
|
||||
func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -104,7 +104,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) {
|
||||
expectedTemplate string
|
||||
}{
|
||||
{
|
||||
name: "获取配置应该返回 system_prompt_template=nof1",
|
||||
name: "Get config should return system_prompt_template=nof1",
|
||||
traderConfig: &store.Trader{
|
||||
ID: "trader-123",
|
||||
UserID: "user-1",
|
||||
@@ -125,7 +125,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) {
|
||||
expectedTemplate: "nof1",
|
||||
},
|
||||
{
|
||||
name: "获取配置应该返回 system_prompt_template=default",
|
||||
name: "Get config should return system_prompt_template=default",
|
||||
traderConfig: &store.Trader{
|
||||
ID: "trader-456",
|
||||
UserID: "user-1",
|
||||
@@ -149,7 +149,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 模拟 handleGetTraderConfig 的返回值构造逻辑(修复后的实现)
|
||||
// Simulate handleGetTraderConfig return value construction logic (fixed implementation)
|
||||
result := map[string]interface{}{
|
||||
"trader_id": tt.traderConfig.ID,
|
||||
"trader_name": tt.traderConfig.Name,
|
||||
@@ -167,7 +167,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) {
|
||||
"is_running": tt.traderConfig.IsRunning,
|
||||
}
|
||||
|
||||
// ✅ 检查响应中是否包含 system_prompt_template
|
||||
// Check if response contains system_prompt_template
|
||||
if _, exists := result["system_prompt_template"]; !exists {
|
||||
t.Errorf("Response is missing 'system_prompt_template' field")
|
||||
} else {
|
||||
@@ -178,7 +178,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证其他字段是否正确
|
||||
// Verify other fields are correct
|
||||
if result["trader_id"] != tt.traderConfig.ID {
|
||||
t.Errorf("trader_id mismatch")
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateTraderRequest_CompleteFields 验证 UpdateTraderRequest 结构体定义完整性
|
||||
// TestUpdateTraderRequest_CompleteFields Verify UpdateTraderRequest struct definition completeness
|
||||
func TestUpdateTraderRequest_CompleteFields(t *testing.T) {
|
||||
jsonData := `{
|
||||
"name": "Test Trader",
|
||||
@@ -212,7 +212,7 @@ func TestUpdateTraderRequest_CompleteFields(t *testing.T) {
|
||||
t.Fatalf("Failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
|
||||
// 验证基本字段是否正确解析
|
||||
// Verify basic fields are correctly parsed
|
||||
if req.Name != "Test Trader" {
|
||||
t.Errorf("Name mismatch: got %q", req.Name)
|
||||
}
|
||||
@@ -220,15 +220,15 @@ func TestUpdateTraderRequest_CompleteFields(t *testing.T) {
|
||||
t.Errorf("AIModelID mismatch: got %q", req.AIModelID)
|
||||
}
|
||||
|
||||
// ✅ 验证 SystemPromptTemplate 字段已正确添加到结构体
|
||||
// Verify SystemPromptTemplate field has been correctly added to struct
|
||||
if req.SystemPromptTemplate != "nof1" {
|
||||
t.Errorf("SystemPromptTemplate mismatch: expected %q, got %q", "nof1", req.SystemPromptTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraderListResponse_SystemPromptTemplate 测试 handleTraderList API 返回的 trader 对象是否包含 system_prompt_template 字段
|
||||
// TestTraderListResponse_SystemPromptTemplate Test whether trader object returned by handleTraderList API contains system_prompt_template field
|
||||
func TestTraderListResponse_SystemPromptTemplate(t *testing.T) {
|
||||
// 模拟 handleTraderList 中的 trader 对象构造
|
||||
// Simulate trader object construction in handleTraderList
|
||||
trader := &store.Trader{
|
||||
ID: "trader-001",
|
||||
UserID: "user-1",
|
||||
@@ -240,7 +240,7 @@ func TestTraderListResponse_SystemPromptTemplate(t *testing.T) {
|
||||
IsRunning: true,
|
||||
}
|
||||
|
||||
// 构造 API 响应对象(与 api/server.go 中的逻辑一致)
|
||||
// Construct API response object (consistent with logic in api/server.go)
|
||||
response := map[string]interface{}{
|
||||
"trader_id": trader.ID,
|
||||
"trader_name": trader.Name,
|
||||
@@ -251,20 +251,20 @@ func TestTraderListResponse_SystemPromptTemplate(t *testing.T) {
|
||||
"system_prompt_template": trader.SystemPromptTemplate,
|
||||
}
|
||||
|
||||
// ✅ 验证 system_prompt_template 字段存在
|
||||
// Verify system_prompt_template field exists
|
||||
if _, exists := response["system_prompt_template"]; !exists {
|
||||
t.Errorf("Trader list response is missing 'system_prompt_template' field")
|
||||
}
|
||||
|
||||
// ✅ 验证 system_prompt_template 值正确
|
||||
// Verify system_prompt_template value is correct
|
||||
if response["system_prompt_template"] != "nof1" {
|
||||
t.Errorf("Expected system_prompt_template='nof1', got %v", response["system_prompt_template"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestPublicTraderListResponse_SystemPromptTemplate 测试 handlePublicTraderList API 返回的 trader 对象是否包含 system_prompt_template 字段
|
||||
// TestPublicTraderListResponse_SystemPromptTemplate Test whether trader object returned by handlePublicTraderList API contains system_prompt_template field
|
||||
func TestPublicTraderListResponse_SystemPromptTemplate(t *testing.T) {
|
||||
// 模拟 getConcurrentTraderData 返回的 trader 数据
|
||||
// Simulate trader data returned by getConcurrentTraderData
|
||||
traderData := map[string]interface{}{
|
||||
"trader_id": "trader-002",
|
||||
"trader_name": "Public Trader",
|
||||
@@ -279,7 +279,7 @@ func TestPublicTraderListResponse_SystemPromptTemplate(t *testing.T) {
|
||||
"system_prompt_template": "default",
|
||||
}
|
||||
|
||||
// 构造 API 响应对象(与 api/server.go handlePublicTraderList 中的逻辑一致)
|
||||
// Construct API response object (consistent with logic in api/server.go handlePublicTraderList)
|
||||
response := map[string]interface{}{
|
||||
"trader_id": traderData["trader_id"],
|
||||
"trader_name": traderData["trader_name"],
|
||||
@@ -293,12 +293,12 @@ func TestPublicTraderListResponse_SystemPromptTemplate(t *testing.T) {
|
||||
"system_prompt_template": traderData["system_prompt_template"],
|
||||
}
|
||||
|
||||
// ✅ 验证 system_prompt_template 字段存在
|
||||
// Verify system_prompt_template field exists
|
||||
if _, exists := response["system_prompt_template"]; !exists {
|
||||
t.Errorf("Public trader list response is missing 'system_prompt_template' field")
|
||||
}
|
||||
|
||||
// ✅ 验证 system_prompt_template 值正确
|
||||
// Verify system_prompt_template value is correct
|
||||
if response["system_prompt_template"] != "default" {
|
||||
t.Errorf("Expected system_prompt_template='default', got %v", response["system_prompt_template"])
|
||||
}
|
||||
|
||||
204
api/strategy.go
204
api/strategy.go
@@ -14,21 +14,21 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// handleGetStrategies 获取策略列表
|
||||
// handleGetStrategies Get strategy list
|
||||
func (s *Server) handleGetStrategies(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
strategies, err := s.store.Strategy().List(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取策略列表失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get strategy list: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为前端格式
|
||||
// Convert to frontend format
|
||||
result := make([]gin.H, 0, len(strategies))
|
||||
for _, st := range strategies {
|
||||
var config store.StrategyConfig
|
||||
@@ -51,19 +51,19 @@ func (s *Server) handleGetStrategies(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetStrategy 获取单个策略
|
||||
// handleGetStrategy Get single strategy
|
||||
func (s *Server) handleGetStrategy(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
strategyID := c.Param("id")
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
strategy, err := s.store.Strategy().Get(userID, strategyID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "策略不存在"})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Strategy not found"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,11 +82,11 @@ func (s *Server) handleGetStrategy(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleCreateStrategy 创建策略
|
||||
// handleCreateStrategy Create strategy
|
||||
func (s *Server) handleCreateStrategy(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,14 +97,14 @@ func (s *Server) handleCreateStrategy(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 序列化配置
|
||||
// Serialize configuration
|
||||
configJSON, err := json.Marshal(req.Config)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化配置失败"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,34 +119,34 @@ func (s *Server) handleCreateStrategy(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := s.store.Strategy().Create(strategy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建策略失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create strategy: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": strategy.ID,
|
||||
"message": "策略创建成功",
|
||||
"message": "Strategy created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdateStrategy 更新策略
|
||||
// handleUpdateStrategy Update strategy
|
||||
func (s *Server) handleUpdateStrategy(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
strategyID := c.Param("id")
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是系统默认策略
|
||||
// Check if it's a system default strategy
|
||||
existing, err := s.store.Strategy().Get(userID, strategyID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "策略不存在"})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Strategy not found"})
|
||||
return
|
||||
}
|
||||
if existing.IsDefault {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "不能修改系统默认策略"})
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Cannot modify system default strategy"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -157,14 +157,14 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 序列化配置
|
||||
// Serialize configuration
|
||||
configJSON, err := json.Marshal(req.Config)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化配置失败"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,56 +177,56 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := s.store.Strategy().Update(strategy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新策略失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update strategy: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "策略更新成功"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Strategy updated successfully"})
|
||||
}
|
||||
|
||||
// handleDeleteStrategy 删除策略
|
||||
// handleDeleteStrategy Delete strategy
|
||||
func (s *Server) handleDeleteStrategy(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
strategyID := c.Param("id")
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.store.Strategy().Delete(userID, strategyID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除策略失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete strategy: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "策略删除成功"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Strategy deleted successfully"})
|
||||
}
|
||||
|
||||
// handleActivateStrategy 激活策略
|
||||
// handleActivateStrategy Activate strategy
|
||||
func (s *Server) handleActivateStrategy(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
strategyID := c.Param("id")
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.store.Strategy().SetActive(userID, strategyID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "激活策略失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to activate strategy: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "策略激活成功"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Strategy activated successfully"})
|
||||
}
|
||||
|
||||
// handleDuplicateStrategy 复制策略
|
||||
// handleDuplicateStrategy Duplicate strategy
|
||||
func (s *Server) handleDuplicateStrategy(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
sourceID := c.Param("id")
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,34 +235,34 @@ func (s *Server) handleDuplicateStrategy(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newID := uuid.New().String()
|
||||
if err := s.store.Strategy().Duplicate(userID, sourceID, newID, req.Name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "复制策略失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to duplicate strategy: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": newID,
|
||||
"message": "策略复制成功",
|
||||
"message": "Strategy duplicated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetActiveStrategy 获取当前激活的策略
|
||||
// handleGetActiveStrategy Get currently active strategy
|
||||
func (s *Server) handleGetActiveStrategy(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
strategy, err := s.store.Strategy().GetActive(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "没有激活的策略"})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "No active strategy"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -281,9 +281,9 @@ func (s *Server) handleGetActiveStrategy(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetDefaultStrategyConfig 获取默认策略配置模板
|
||||
// handleGetDefaultStrategyConfig Get default strategy configuration template
|
||||
func (s *Server) handleGetDefaultStrategyConfig(c *gin.Context) {
|
||||
// 返回默认配置结构,供前端创建新策略时使用
|
||||
// Return default configuration structure for frontend to use when creating new strategies
|
||||
defaultConfig := store.StrategyConfig{
|
||||
CoinSource: store.CoinSourceConfig{
|
||||
SourceType: "coinpool",
|
||||
@@ -324,42 +324,42 @@ func (s *Server) handleGetDefaultStrategyConfig(c *gin.Context) {
|
||||
MinConfidence: 75,
|
||||
},
|
||||
PromptSections: store.PromptSectionsConfig{
|
||||
RoleDefinition: `# 你是专业的加密货币交易AI
|
||||
RoleDefinition: `# You are a professional cryptocurrency trading AI
|
||||
|
||||
你专注于技术分析和风险管理,基于市场数据做出理性的交易决策。
|
||||
你的目标是在控制风险的前提下,捕捉高概率的交易机会。`,
|
||||
TradingFrequency: `# ⏱️ 交易频率认知
|
||||
You focus on technical analysis and risk management, making rational trading decisions based on market data.
|
||||
Your goal is to capture high-probability trading opportunities while controlling risk.`,
|
||||
TradingFrequency: `# ⏱️ Trading Frequency Awareness
|
||||
|
||||
- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔
|
||||
- 每小时>2笔 = 过度交易
|
||||
- 单笔持仓时间≥30-60分钟
|
||||
如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`,
|
||||
EntryStandards: `# 🎯 开仓标准(严格)
|
||||
- Excellent traders: 2-4 trades per day ≈ 0.1-0.2 trades per hour
|
||||
- >2 trades per hour = overtrading
|
||||
- Single position holding time ≥30-60 minutes
|
||||
If you find yourself trading every cycle → standards too low; if closing positions <30 minutes → too impatient.`,
|
||||
EntryStandards: `# 🎯 Entry Standards (Strict)
|
||||
|
||||
只在多重信号共振时开仓:
|
||||
- 趋势方向明确(EMA排列、价格位置)
|
||||
- 动量确认(MACD、RSI协同)
|
||||
- 波动率适中(ATR合理范围)
|
||||
- 量价配合(成交量支持方向)
|
||||
Only enter when multiple signals align:
|
||||
- Clear trend direction (EMA alignment, price position)
|
||||
- Momentum confirmation (MACD, RSI cooperation)
|
||||
- Moderate volatility (ATR reasonable range)
|
||||
- Volume-price coordination (volume supports direction)
|
||||
|
||||
避免:单一指标、信号矛盾、横盘震荡、刚平仓即重启。`,
|
||||
DecisionProcess: `# 📋 决策流程
|
||||
Avoid: single indicator, conflicting signals, sideways consolidation, reopening immediately after closing.`,
|
||||
DecisionProcess: `# 📋 Decision Process
|
||||
|
||||
1. 检查持仓 → 是否该止盈/止损
|
||||
2. 扫描候选币 + 多时间框 → 是否存在强信号
|
||||
3. 评估风险回报比 → 是否满足最小要求
|
||||
4. 先写思维链,再输出结构化JSON`,
|
||||
1. Check positions → Should take profit/stop loss
|
||||
2. Scan candidate coins + multiple timeframes → Are there strong signals
|
||||
3. Evaluate risk-reward ratio → Does it meet minimum requirements
|
||||
4. Write chain of thought first, then output structured JSON`,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, defaultConfig)
|
||||
}
|
||||
|
||||
// handlePreviewPrompt 预览策略生成的 Prompt
|
||||
// handlePreviewPrompt Preview prompt generated by strategy
|
||||
func (s *Server) handlePreviewPrompt(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -370,28 +370,28 @@ func (s *Server) handlePreviewPrompt(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用默认值
|
||||
// Use default values
|
||||
if req.AccountEquity <= 0 {
|
||||
req.AccountEquity = 1000.0 // 默认模拟账户净值
|
||||
req.AccountEquity = 1000.0 // Default simulated account equity
|
||||
}
|
||||
if req.PromptVariant == "" {
|
||||
req.PromptVariant = "balanced"
|
||||
}
|
||||
|
||||
// 创建策略引擎来构建 prompt
|
||||
// Create strategy engine to build prompt
|
||||
engine := decision.NewStrategyEngine(&req.Config)
|
||||
|
||||
// 构建系统 prompt(使用策略引擎内置的方法)
|
||||
// Build system prompt (using built-in method from strategy engine)
|
||||
systemPrompt := engine.BuildSystemPrompt(
|
||||
req.AccountEquity,
|
||||
req.PromptVariant,
|
||||
)
|
||||
|
||||
// 获取可用的 prompt 模板列表
|
||||
// Get list of available prompt templates
|
||||
templateNames := decision.GetAllPromptTemplateNames()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -408,11 +408,11 @@ func (s *Server) handlePreviewPrompt(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleStrategyTestRun AI 测试运行(不执行交易,只返回 AI 分析结果)
|
||||
// handleStrategyTestRun AI test run (does not execute trades, only returns AI analysis results)
|
||||
func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -432,27 +432,27 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
req.PromptVariant = "balanced"
|
||||
}
|
||||
|
||||
// 创建策略引擎来构建 prompt
|
||||
// Create strategy engine to build prompt
|
||||
engine := decision.NewStrategyEngine(&req.Config)
|
||||
|
||||
// 获取候选币种
|
||||
// Get candidate coins
|
||||
candidates, err := engine.GetCandidateCoins()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "获取候选币种失败: " + err.Error(),
|
||||
"error": "Failed to get candidate coins: " + err.Error(),
|
||||
"ai_response": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取时间周期配置
|
||||
// Get timeframe configuration
|
||||
timeframes := req.Config.Indicators.Klines.SelectedTimeframes
|
||||
primaryTimeframe := req.Config.Indicators.Klines.PrimaryTimeframe
|
||||
klineCount := req.Config.Indicators.Klines.PrimaryCount
|
||||
|
||||
// 如果没有选择时间周期,使用默认值
|
||||
// If no timeframes selected, use default values
|
||||
if len(timeframes) == 0 {
|
||||
// 兼容旧配置:使用主周期和长周期
|
||||
// Backward compatibility: use primary and longer timeframes
|
||||
if primaryTimeframe != "" {
|
||||
timeframes = append(timeframes, primaryTimeframe)
|
||||
} else {
|
||||
@@ -469,21 +469,21 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
klineCount = 30
|
||||
}
|
||||
|
||||
fmt.Printf("📊 使用时间周期: %v, 主周期: %s, K线数量: %d\n", timeframes, primaryTimeframe, klineCount)
|
||||
fmt.Printf("📊 Using timeframes: %v, primary: %s, kline count: %d\n", timeframes, primaryTimeframe, klineCount)
|
||||
|
||||
// 获取真实市场数据(使用多时间周期)
|
||||
// Get real market data (using multiple timeframes)
|
||||
marketDataMap := make(map[string]*market.Data)
|
||||
for _, coin := range candidates {
|
||||
data, err := market.GetWithTimeframes(coin.Symbol, timeframes, primaryTimeframe, klineCount)
|
||||
if err != nil {
|
||||
// 如果获取某个币种数据失败,记录日志但继续
|
||||
fmt.Printf("⚠️ 获取 %s 市场数据失败: %v\n", coin.Symbol, err)
|
||||
// If getting data for a coin fails, log but continue
|
||||
fmt.Printf("⚠️ Failed to get market data for %s: %v\n", coin.Symbol, err)
|
||||
continue
|
||||
}
|
||||
marketDataMap[coin.Symbol] = data
|
||||
}
|
||||
|
||||
// 构建真实的上下文(用于生成 User Prompt)
|
||||
// Build real context (for generating User Prompt)
|
||||
testContext := &decision.Context{
|
||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
RuntimeMinutes: 0,
|
||||
@@ -504,13 +504,13 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
MarketDataMap: marketDataMap,
|
||||
}
|
||||
|
||||
// 构建 System Prompt
|
||||
// Build System Prompt
|
||||
systemPrompt := engine.BuildSystemPrompt(1000.0, req.PromptVariant)
|
||||
|
||||
// 构建 User Prompt(使用真实市场数据)
|
||||
// Build User Prompt (using real market data)
|
||||
userPrompt := engine.BuildUserPrompt(testContext)
|
||||
|
||||
// 如果请求真实 AI 调用
|
||||
// If requesting real AI call
|
||||
if req.RunRealAI && req.AIModelID != "" {
|
||||
aiResponse, aiErr := s.runRealAITest(userID, req.AIModelID, systemPrompt, userPrompt)
|
||||
if aiErr != nil {
|
||||
@@ -520,9 +520,9 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
"candidate_count": len(candidates),
|
||||
"candidates": candidates,
|
||||
"prompt_variant": req.PromptVariant,
|
||||
"ai_response": fmt.Sprintf("❌ AI 调用失败: %s", aiErr.Error()),
|
||||
"ai_response": fmt.Sprintf("❌ AI call failed: %s", aiErr.Error()),
|
||||
"ai_error": aiErr.Error(),
|
||||
"note": "AI 调用出错",
|
||||
"note": "AI call error",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -534,40 +534,40 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
"candidates": candidates,
|
||||
"prompt_variant": req.PromptVariant,
|
||||
"ai_response": aiResponse,
|
||||
"note": "✅ 真实 AI 测试运行成功",
|
||||
"note": "✅ Real AI test run successful",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回结果(不实际调用 AI,只返回构建的 prompt)
|
||||
// Return result (without actually calling AI, only return built prompt)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"system_prompt": systemPrompt,
|
||||
"user_prompt": userPrompt,
|
||||
"candidate_count": len(candidates),
|
||||
"candidates": candidates,
|
||||
"prompt_variant": req.PromptVariant,
|
||||
"ai_response": "请选择 AI 模型并点击「运行测试」来执行真实的 AI 分析。",
|
||||
"note": "未选择 AI 模型或未启用真实 AI 调用",
|
||||
"ai_response": "Please select an AI model and click 'Run Test' to perform real AI analysis.",
|
||||
"note": "AI model not selected or real AI call not enabled",
|
||||
})
|
||||
}
|
||||
|
||||
// runRealAITest 执行真实的 AI 测试调用
|
||||
// runRealAITest Execute real AI test call
|
||||
func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string) (string, error) {
|
||||
// 获取 AI 模型配置
|
||||
// Get AI model configuration
|
||||
model, err := s.store.AIModel().Get(userID, modelID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取 AI 模型失败: %w", err)
|
||||
return "", fmt.Errorf("failed to get AI model: %w", err)
|
||||
}
|
||||
|
||||
if !model.Enabled {
|
||||
return "", fmt.Errorf("AI 模型 %s 尚未启用", model.Name)
|
||||
return "", fmt.Errorf("AI model %s is not enabled", model.Name)
|
||||
}
|
||||
|
||||
if model.APIKey == "" {
|
||||
return "", fmt.Errorf("AI 模型 %s 缺少 API Key", model.Name)
|
||||
return "", fmt.Errorf("AI model %s is missing API Key", model.Name)
|
||||
}
|
||||
|
||||
// 创建 AI 客户端
|
||||
// Create AI client
|
||||
var aiClient mcp.AIClient
|
||||
provider := model.Provider
|
||||
|
||||
@@ -579,15 +579,15 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string)
|
||||
aiClient = mcp.NewDeepSeekClient()
|
||||
aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName)
|
||||
default:
|
||||
// 使用通用客户端
|
||||
// Use generic client
|
||||
aiClient = mcp.NewClient()
|
||||
aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName)
|
||||
}
|
||||
|
||||
// 调用 AI API
|
||||
// Call AI API
|
||||
response, err := aiClient.CallWithMessages(systemPrompt, userPrompt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("AI API 调用失败: %w", err)
|
||||
return "", fmt.Errorf("AI API call failed: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
|
||||
@@ -8,67 +8,67 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TestTraderIDUniqueness 测试 traderID 的唯一性(修复 Issue #893)
|
||||
// 验证即使在相同的 exchange 和 AI model 下,也能生成唯一的 traderID
|
||||
// TestTraderIDUniqueness Test traderID uniqueness (fixes Issue #893)
|
||||
// Verify that unique traderIDs can be generated even with the same exchange and AI model
|
||||
func TestTraderIDUniqueness(t *testing.T) {
|
||||
exchangeID := "binance"
|
||||
aiModelID := "gpt-4"
|
||||
|
||||
// 模拟同时创建 100 个 trader(相同参数)
|
||||
// Simulate creating 100 traders simultaneously (with same parameters)
|
||||
traderIDs := make(map[string]bool)
|
||||
const numTraders = 100
|
||||
|
||||
for i := 0; i < numTraders; i++ {
|
||||
// 模拟 api/server.go:497 的 traderID 生成逻辑
|
||||
// Simulate traderID generation logic from api/server.go:497
|
||||
traderID := generateTraderID(exchangeID, aiModelID)
|
||||
|
||||
// ✅ 检查是否重复
|
||||
// Check for duplicates
|
||||
if traderIDs[traderID] {
|
||||
t.Errorf("Duplicate traderID detected: %s", traderID)
|
||||
}
|
||||
traderIDs[traderID] = true
|
||||
|
||||
// ✅ 验证格式:应该是 "exchange_model_uuid"
|
||||
// Verify format: should be "exchange_model_uuid"
|
||||
if !isValidTraderIDFormat(traderID, exchangeID, aiModelID) {
|
||||
t.Errorf("Invalid traderID format: %s", traderID)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 验证生成了预期数量的唯一 ID
|
||||
// Verify expected number of unique IDs were generated
|
||||
if len(traderIDs) != numTraders {
|
||||
t.Errorf("Expected %d unique traderIDs, got %d", numTraders, len(traderIDs))
|
||||
}
|
||||
}
|
||||
|
||||
// generateTraderID 辅助函数,模拟 api/server.go 中的 traderID 生成逻辑
|
||||
// generateTraderID Helper function that simulates traderID generation logic from api/server.go
|
||||
func generateTraderID(exchangeID, aiModelID string) string {
|
||||
return fmt.Sprintf("%s_%s_%s", exchangeID, aiModelID, uuid.New().String())
|
||||
}
|
||||
|
||||
// isValidTraderIDFormat 验证 traderID 格式是否符合预期
|
||||
// isValidTraderIDFormat Verify traderID format matches expected format
|
||||
func isValidTraderIDFormat(traderID, expectedExchange, expectedModel string) bool {
|
||||
// 格式:exchange_model_uuid
|
||||
// 例如:binance_gpt-4_a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
// Format: exchange_model_uuid
|
||||
// Example: binance_gpt-4_a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
parts := strings.Split(traderID, "_")
|
||||
if len(parts) < 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证前缀
|
||||
// Verify prefix
|
||||
if parts[0] != expectedExchange {
|
||||
return false
|
||||
}
|
||||
|
||||
// AI model 可能包含连字符(如 gpt-4),所以需要重组
|
||||
// 最后一部分应该是 UUID
|
||||
// AI model may contain hyphens (e.g. gpt-4), so need to reconstruct
|
||||
// Last part should be UUID
|
||||
uuidPart := parts[len(parts)-1]
|
||||
|
||||
// 验证 UUID 格式(36 个字符,包含 4 个连字符)
|
||||
// Verify UUID format (36 characters, containing 4 hyphens)
|
||||
_, err := uuid.Parse(uuidPart)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// TestTraderIDFormat 测试 traderID 格式的正确性
|
||||
// TestTraderIDFormat Test traderID format correctness
|
||||
func TestTraderIDFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -84,18 +84,18 @@ func TestTraderIDFormat(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
traderID := generateTraderID(tt.exchangeID, tt.aiModelID)
|
||||
|
||||
// ✅ 验证包含正确的前缀
|
||||
// Verify correct prefix
|
||||
if !strings.HasPrefix(traderID, tt.exchangeID+"_"+tt.aiModelID+"_") {
|
||||
t.Errorf("traderID does not have correct prefix. Got: %s", traderID)
|
||||
}
|
||||
|
||||
// ✅ 验证格式有效
|
||||
// Verify format is valid
|
||||
if !isValidTraderIDFormat(traderID, tt.exchangeID, tt.aiModelID) {
|
||||
t.Errorf("Invalid traderID format: %s", traderID)
|
||||
}
|
||||
|
||||
// ✅ 验证长度合理(至少应该有 exchange + model + "_" + UUID(36) 的长度)
|
||||
minLength := len(tt.exchangeID) + len(tt.aiModelID) + 2 + 36 // 2个下划线 + 36字符UUID
|
||||
// Verify reasonable length (should be at least exchange + model + "_" + UUID(36))
|
||||
minLength := len(tt.exchangeID) + len(tt.aiModelID) + 2 + 36 // 2 underscores + 36 character UUID
|
||||
if len(traderID) < minLength {
|
||||
t.Errorf("traderID too short: expected at least %d chars, got %d", minLength, len(traderID))
|
||||
}
|
||||
@@ -103,12 +103,12 @@ func TestTraderIDFormat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraderIDNoCollision 测试在高并发场景下不会产生碰撞
|
||||
// TestTraderIDNoCollision Test that no collisions occur in high concurrency scenarios
|
||||
func TestTraderIDNoCollision(t *testing.T) {
|
||||
const iterations = 1000
|
||||
uniqueIDs := make(map[string]bool, iterations)
|
||||
|
||||
// 模拟高并发场景
|
||||
// Simulate high concurrency scenario
|
||||
for i := 0; i < iterations; i++ {
|
||||
id := generateTraderID("binance", "gpt-4")
|
||||
if uniqueIDs[id] {
|
||||
|
||||
18
api/utils.go
18
api/utils.go
@@ -2,20 +2,20 @@ package api
|
||||
|
||||
import "strings"
|
||||
|
||||
// MaskSensitiveString 脱敏敏感字符串,只显示前4位和后4位
|
||||
// 用于脱敏 API Key、Secret Key、Private Key 等敏感信息
|
||||
// MaskSensitiveString Mask sensitive strings, showing only first 4 and last 4 characters
|
||||
// Used to mask API Key, Secret Key, Private Key and other sensitive information
|
||||
func MaskSensitiveString(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
length := len(s)
|
||||
if length <= 8 {
|
||||
return "****" // 字符串太短,全部隐藏
|
||||
return "****" // String too short, hide everything
|
||||
}
|
||||
return s[:4] + "****" + s[length-4:]
|
||||
}
|
||||
|
||||
// SanitizeModelConfigForLog 脱敏模型配置用于日志输出
|
||||
// SanitizeModelConfigForLog Sanitize model configuration for log output
|
||||
func SanitizeModelConfigForLog(models map[string]struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"api_key"`
|
||||
@@ -34,7 +34,7 @@ func SanitizeModelConfigForLog(models map[string]struct {
|
||||
return safe
|
||||
}
|
||||
|
||||
// SanitizeExchangeConfigForLog 脱敏交易所配置用于日志输出
|
||||
// SanitizeExchangeConfigForLog Sanitize exchange configuration for log output
|
||||
func SanitizeExchangeConfigForLog(exchanges map[string]struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"api_key"`
|
||||
@@ -54,7 +54,7 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct {
|
||||
"testnet": cfg.Testnet,
|
||||
}
|
||||
|
||||
// 只在有值时才添加脱敏后的敏感字段
|
||||
// Only add masked sensitive fields when they have values
|
||||
if cfg.APIKey != "" {
|
||||
safeExchange["api_key"] = MaskSensitiveString(cfg.APIKey)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct {
|
||||
safeExchange["lighter_private_key"] = MaskSensitiveString(cfg.LighterPrivateKey)
|
||||
}
|
||||
|
||||
// 非敏感字段直接添加
|
||||
// Add non-sensitive fields directly
|
||||
if cfg.HyperliquidWalletAddr != "" {
|
||||
safeExchange["hyperliquid_wallet_addr"] = cfg.HyperliquidWalletAddr
|
||||
}
|
||||
@@ -87,14 +87,14 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct {
|
||||
return safe
|
||||
}
|
||||
|
||||
// MaskEmail 脱敏邮箱地址,保留前2位和@后部分
|
||||
// MaskEmail Mask email address, keeping first 2 characters and domain part
|
||||
func MaskEmail(email string) string {
|
||||
if email == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return "****" // 格式不正确
|
||||
return "****" // Incorrect format
|
||||
}
|
||||
username := parts[0]
|
||||
domain := parts[1]
|
||||
|
||||
@@ -11,27 +11,27 @@ func TestMaskSensitiveString(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "空字符串",
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "短字符串(小于等于8位)",
|
||||
name: "Short string (8 characters or less)",
|
||||
input: "short",
|
||||
expected: "****",
|
||||
},
|
||||
{
|
||||
name: "正常API key",
|
||||
name: "Normal API key",
|
||||
input: "sk-1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
expected: "sk-1****wxyz",
|
||||
},
|
||||
{
|
||||
name: "正常私钥",
|
||||
name: "Normal private key",
|
||||
input: "0x1234567890abcdef1234567890abcdef12345678",
|
||||
expected: "0x12****5678",
|
||||
},
|
||||
{
|
||||
name: "刚好9位",
|
||||
name: "Exactly 9 characters",
|
||||
input: "123456789",
|
||||
expected: "1234****6789",
|
||||
},
|
||||
@@ -119,7 +119,7 @@ func TestSanitizeExchangeConfigForLog(t *testing.T) {
|
||||
|
||||
result := SanitizeExchangeConfigForLog(exchanges)
|
||||
|
||||
// 检查币安配置
|
||||
// Check Binance configuration
|
||||
binanceConfig, ok := result["binance"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("binance config not found or wrong type")
|
||||
@@ -143,7 +143,7 @@ func TestSanitizeExchangeConfigForLog(t *testing.T) {
|
||||
t.Errorf("expected masked secret_key='bina****cdef', got %q", maskedSecretKey)
|
||||
}
|
||||
|
||||
// 检查 Hyperliquid 配置
|
||||
// Check Hyperliquid configuration
|
||||
hlConfig, ok := result["hyperliquid"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("hyperliquid config not found or wrong type")
|
||||
@@ -154,7 +154,7 @@ func TestSanitizeExchangeConfigForLog(t *testing.T) {
|
||||
t.Fatal("hyperliquid_wallet_addr not found or wrong type")
|
||||
}
|
||||
|
||||
// 钱包地址不应该被脱敏
|
||||
// Wallet address should not be masked
|
||||
if walletAddr != "0x1234567890abcdef1234567890abcdef12345678" {
|
||||
t.Errorf("wallet address should not be masked, got %q", walletAddr)
|
||||
}
|
||||
@@ -167,22 +167,22 @@ func TestMaskEmail(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "空邮箱",
|
||||
name: "Empty email",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "格式错误",
|
||||
name: "Invalid format",
|
||||
input: "notanemail",
|
||||
expected: "****",
|
||||
},
|
||||
{
|
||||
name: "正常邮箱",
|
||||
name: "Normal email",
|
||||
input: "user@example.com",
|
||||
expected: "us****@example.com",
|
||||
},
|
||||
{
|
||||
name: "短用户名",
|
||||
name: "Short username",
|
||||
input: "a@example.com",
|
||||
expected: "**@example.com",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user