mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
5cff32e4f2
* feat: add Strategy Studio with multi-timeframe support - Add Strategy Studio page with three-column layout for strategy management - Support multi-timeframe K-line data selection (5m, 15m, 1h, 4h, etc.) - Add GetWithTimeframes() function in market package for fetching multiple timeframes - Add TimeframeSeriesData struct for storing per-timeframe technical indicators - Update formatMarketData() to display all selected timeframes in AI prompt - Add strategy API endpoints for CRUD operations and test run - Integrate real AI test runs with configured AI models - Support custom AI500 and OI Top API URLs from strategy config * docs: add Strategy Studio screenshot to README files * fix: correct strategy-studio.png filename case in README * refactor: remove legacy signal source config and simplify trader creation - Remove signal source configuration from traders page (now handled by strategy) - Remove advanced options (legacy config) from TraderConfigModal - Rename default strategy to "默认山寨策略" with AI500 coin pool URL - Delete SignalSourceModal and SignalSourceWarning components - Clean up related stores, hooks, and page components
596 lines
17 KiB
Go
596 lines
17 KiB
Go
package api
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"nofx/decision"
|
||
"nofx/market"
|
||
"nofx/mcp"
|
||
"nofx/store"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// handleGetStrategies 获取策略列表
|
||
func (s *Server) handleGetStrategies(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||
return
|
||
}
|
||
|
||
strategies, err := s.store.Strategy().List(userID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取策略列表失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 转换为前端格式
|
||
result := make([]gin.H, 0, len(strategies))
|
||
for _, st := range strategies {
|
||
var config store.StrategyConfig
|
||
json.Unmarshal([]byte(st.Config), &config)
|
||
|
||
result = append(result, gin.H{
|
||
"id": st.ID,
|
||
"name": st.Name,
|
||
"description": st.Description,
|
||
"is_active": st.IsActive,
|
||
"is_default": st.IsDefault,
|
||
"config": config,
|
||
"created_at": st.CreatedAt,
|
||
"updated_at": st.UpdatedAt,
|
||
})
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"strategies": result,
|
||
})
|
||
}
|
||
|
||
// handleGetStrategy 获取单个策略
|
||
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": "未授权"})
|
||
return
|
||
}
|
||
|
||
strategy, err := s.store.Strategy().Get(userID, strategyID)
|
||
if err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "策略不存在"})
|
||
return
|
||
}
|
||
|
||
var config store.StrategyConfig
|
||
json.Unmarshal([]byte(strategy.Config), &config)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"id": strategy.ID,
|
||
"name": strategy.Name,
|
||
"description": strategy.Description,
|
||
"is_active": strategy.IsActive,
|
||
"is_default": strategy.IsDefault,
|
||
"config": config,
|
||
"created_at": strategy.CreatedAt,
|
||
"updated_at": strategy.UpdatedAt,
|
||
})
|
||
}
|
||
|
||
// handleCreateStrategy 创建策略
|
||
func (s *Server) handleCreateStrategy(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Name string `json:"name" binding:"required"`
|
||
Description string `json:"description"`
|
||
Config store.StrategyConfig `json:"config" binding:"required"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 序列化配置
|
||
configJSON, err := json.Marshal(req.Config)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化配置失败"})
|
||
return
|
||
}
|
||
|
||
strategy := &store.Strategy{
|
||
ID: uuid.New().String(),
|
||
UserID: userID,
|
||
Name: req.Name,
|
||
Description: req.Description,
|
||
IsActive: false,
|
||
IsDefault: false,
|
||
Config: string(configJSON),
|
||
}
|
||
|
||
if err := s.store.Strategy().Create(strategy); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建策略失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"id": strategy.ID,
|
||
"message": "策略创建成功",
|
||
})
|
||
}
|
||
|
||
// handleUpdateStrategy 更新策略
|
||
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": "未授权"})
|
||
return
|
||
}
|
||
|
||
// 检查是否是系统默认策略
|
||
existing, err := s.store.Strategy().Get(userID, strategyID)
|
||
if err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "策略不存在"})
|
||
return
|
||
}
|
||
if existing.IsDefault {
|
||
c.JSON(http.StatusForbidden, gin.H{"error": "不能修改系统默认策略"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
Config store.StrategyConfig `json:"config"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 序列化配置
|
||
configJSON, err := json.Marshal(req.Config)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化配置失败"})
|
||
return
|
||
}
|
||
|
||
strategy := &store.Strategy{
|
||
ID: strategyID,
|
||
UserID: userID,
|
||
Name: req.Name,
|
||
Description: req.Description,
|
||
Config: string(configJSON),
|
||
}
|
||
|
||
if err := s.store.Strategy().Update(strategy); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新策略失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"message": "策略更新成功"})
|
||
}
|
||
|
||
// handleDeleteStrategy 删除策略
|
||
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": "未授权"})
|
||
return
|
||
}
|
||
|
||
if err := s.store.Strategy().Delete(userID, strategyID); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除策略失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"message": "策略删除成功"})
|
||
}
|
||
|
||
// handleActivateStrategy 激活策略
|
||
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": "未授权"})
|
||
return
|
||
}
|
||
|
||
if err := s.store.Strategy().SetActive(userID, strategyID); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "激活策略失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"message": "策略激活成功"})
|
||
}
|
||
|
||
// handleDuplicateStrategy 复制策略
|
||
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": "未授权"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Name string `json:"name" binding:"required"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + 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()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"id": newID,
|
||
"message": "策略复制成功",
|
||
})
|
||
}
|
||
|
||
// handleGetActiveStrategy 获取当前激活的策略
|
||
func (s *Server) handleGetActiveStrategy(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
|
||
if userID == "" {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||
return
|
||
}
|
||
|
||
strategy, err := s.store.Strategy().GetActive(userID)
|
||
if err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "没有激活的策略"})
|
||
return
|
||
}
|
||
|
||
var config store.StrategyConfig
|
||
json.Unmarshal([]byte(strategy.Config), &config)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"id": strategy.ID,
|
||
"name": strategy.Name,
|
||
"description": strategy.Description,
|
||
"is_active": strategy.IsActive,
|
||
"is_default": strategy.IsDefault,
|
||
"config": config,
|
||
"created_at": strategy.CreatedAt,
|
||
"updated_at": strategy.UpdatedAt,
|
||
})
|
||
}
|
||
|
||
// handleGetDefaultStrategyConfig 获取默认策略配置模板
|
||
func (s *Server) handleGetDefaultStrategyConfig(c *gin.Context) {
|
||
// 返回默认配置结构,供前端创建新策略时使用
|
||
defaultConfig := store.StrategyConfig{
|
||
CoinSource: store.CoinSourceConfig{
|
||
SourceType: "coinpool",
|
||
UseCoinPool: true,
|
||
CoinPoolLimit: 30,
|
||
UseOITop: true,
|
||
OITopLimit: 20,
|
||
StaticCoins: []string{},
|
||
},
|
||
Indicators: store.IndicatorConfig{
|
||
Klines: store.KlineConfig{
|
||
PrimaryTimeframe: "5m",
|
||
PrimaryCount: 30,
|
||
LongerTimeframe: "4h",
|
||
LongerCount: 10,
|
||
EnableMultiTimeframe: true,
|
||
SelectedTimeframes: []string{"5m", "15m", "1h", "4h"},
|
||
},
|
||
EnableEMA: true,
|
||
EnableMACD: true,
|
||
EnableRSI: true,
|
||
EnableATR: true,
|
||
EnableVolume: true,
|
||
EnableOI: true,
|
||
EnableFundingRate: true,
|
||
EMAPeriods: []int{20, 50},
|
||
RSIPeriods: []int{7, 14},
|
||
ATRPeriods: []int{14},
|
||
},
|
||
RiskControl: store.RiskControlConfig{
|
||
MaxPositions: 3,
|
||
BTCETHMaxLeverage: 5,
|
||
AltcoinMaxLeverage: 5,
|
||
MinRiskRewardRatio: 3.0,
|
||
MaxMarginUsage: 0.9,
|
||
MaxPositionRatio: 1.5,
|
||
MinPositionSize: 12,
|
||
MinConfidence: 75,
|
||
},
|
||
PromptSections: store.PromptSectionsConfig{
|
||
RoleDefinition: `# 你是专业的加密货币交易AI
|
||
|
||
你专注于技术分析和风险管理,基于市场数据做出理性的交易决策。
|
||
你的目标是在控制风险的前提下,捕捉高概率的交易机会。`,
|
||
TradingFrequency: `# ⏱️ 交易频率认知
|
||
|
||
- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔
|
||
- 每小时>2笔 = 过度交易
|
||
- 单笔持仓时间≥30-60分钟
|
||
如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`,
|
||
EntryStandards: `# 🎯 开仓标准(严格)
|
||
|
||
只在多重信号共振时开仓:
|
||
- 趋势方向明确(EMA排列、价格位置)
|
||
- 动量确认(MACD、RSI协同)
|
||
- 波动率适中(ATR合理范围)
|
||
- 量价配合(成交量支持方向)
|
||
|
||
避免:单一指标、信号矛盾、横盘震荡、刚平仓即重启。`,
|
||
DecisionProcess: `# 📋 决策流程
|
||
|
||
1. 检查持仓 → 是否该止盈/止损
|
||
2. 扫描候选币 + 多时间框 → 是否存在强信号
|
||
3. 评估风险回报比 → 是否满足最小要求
|
||
4. 先写思维链,再输出结构化JSON`,
|
||
},
|
||
}
|
||
|
||
c.JSON(http.StatusOK, defaultConfig)
|
||
}
|
||
|
||
// handlePreviewPrompt 预览策略生成的 Prompt
|
||
func (s *Server) handlePreviewPrompt(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Config store.StrategyConfig `json:"config" binding:"required"`
|
||
AccountEquity float64 `json:"account_equity"`
|
||
PromptVariant string `json:"prompt_variant"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 使用默认值
|
||
if req.AccountEquity <= 0 {
|
||
req.AccountEquity = 1000.0 // 默认模拟账户净值
|
||
}
|
||
if req.PromptVariant == "" {
|
||
req.PromptVariant = "balanced"
|
||
}
|
||
|
||
// 创建策略引擎来构建 prompt
|
||
engine := decision.NewStrategyEngine(&req.Config)
|
||
|
||
// 构建系统 prompt(使用策略引擎内置的方法)
|
||
systemPrompt := engine.BuildSystemPrompt(
|
||
req.AccountEquity,
|
||
req.PromptVariant,
|
||
)
|
||
|
||
// 获取可用的 prompt 模板列表
|
||
templateNames := decision.GetAllPromptTemplateNames()
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"system_prompt": systemPrompt,
|
||
"prompt_variant": req.PromptVariant,
|
||
"available_templates": templateNames,
|
||
"config_summary": gin.H{
|
||
"coin_source": req.Config.CoinSource.SourceType,
|
||
"primary_tf": req.Config.Indicators.Klines.PrimaryTimeframe,
|
||
"btc_eth_leverage": req.Config.RiskControl.BTCETHMaxLeverage,
|
||
"altcoin_leverage": req.Config.RiskControl.AltcoinMaxLeverage,
|
||
"max_positions": req.Config.RiskControl.MaxPositions,
|
||
},
|
||
})
|
||
}
|
||
|
||
// handleStrategyTestRun AI 测试运行(不执行交易,只返回 AI 分析结果)
|
||
func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Config store.StrategyConfig `json:"config" binding:"required"`
|
||
PromptVariant string `json:"prompt_variant"`
|
||
AIModelID string `json:"ai_model_id"`
|
||
RunRealAI bool `json:"run_real_ai"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
if req.PromptVariant == "" {
|
||
req.PromptVariant = "balanced"
|
||
}
|
||
|
||
// 创建策略引擎来构建 prompt
|
||
engine := decision.NewStrategyEngine(&req.Config)
|
||
|
||
// 获取候选币种
|
||
candidates, err := engine.GetCandidateCoins()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "获取候选币种失败: " + err.Error(),
|
||
"ai_response": "",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 获取时间周期配置
|
||
timeframes := req.Config.Indicators.Klines.SelectedTimeframes
|
||
primaryTimeframe := req.Config.Indicators.Klines.PrimaryTimeframe
|
||
klineCount := req.Config.Indicators.Klines.PrimaryCount
|
||
|
||
// 如果没有选择时间周期,使用默认值
|
||
if len(timeframes) == 0 {
|
||
// 兼容旧配置:使用主周期和长周期
|
||
if primaryTimeframe != "" {
|
||
timeframes = append(timeframes, primaryTimeframe)
|
||
} else {
|
||
timeframes = append(timeframes, "3m")
|
||
}
|
||
if req.Config.Indicators.Klines.LongerTimeframe != "" {
|
||
timeframes = append(timeframes, req.Config.Indicators.Klines.LongerTimeframe)
|
||
}
|
||
}
|
||
if primaryTimeframe == "" {
|
||
primaryTimeframe = timeframes[0]
|
||
}
|
||
if klineCount <= 0 {
|
||
klineCount = 30
|
||
}
|
||
|
||
fmt.Printf("📊 使用时间周期: %v, 主周期: %s, K线数量: %d\n", timeframes, primaryTimeframe, klineCount)
|
||
|
||
// 获取真实市场数据(使用多时间周期)
|
||
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)
|
||
continue
|
||
}
|
||
marketDataMap[coin.Symbol] = data
|
||
}
|
||
|
||
// 构建真实的上下文(用于生成 User Prompt)
|
||
testContext := &decision.Context{
|
||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||
RuntimeMinutes: 0,
|
||
CallCount: 1,
|
||
Account: decision.AccountInfo{
|
||
TotalEquity: 1000.0,
|
||
AvailableBalance: 1000.0,
|
||
UnrealizedPnL: 0,
|
||
TotalPnL: 0,
|
||
TotalPnLPct: 0,
|
||
MarginUsed: 0,
|
||
MarginUsedPct: 0,
|
||
PositionCount: 0,
|
||
},
|
||
Positions: []decision.PositionInfo{},
|
||
CandidateCoins: candidates,
|
||
PromptVariant: req.PromptVariant,
|
||
MarketDataMap: marketDataMap,
|
||
}
|
||
|
||
// 构建 System Prompt
|
||
systemPrompt := engine.BuildSystemPrompt(1000.0, req.PromptVariant)
|
||
|
||
// 构建 User Prompt(使用真实市场数据)
|
||
userPrompt := engine.BuildUserPrompt(testContext)
|
||
|
||
// 如果请求真实 AI 调用
|
||
if req.RunRealAI && req.AIModelID != "" {
|
||
aiResponse, aiErr := s.runRealAITest(userID, req.AIModelID, systemPrompt, userPrompt)
|
||
if aiErr != nil {
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"system_prompt": systemPrompt,
|
||
"user_prompt": userPrompt,
|
||
"candidate_count": len(candidates),
|
||
"candidates": candidates,
|
||
"prompt_variant": req.PromptVariant,
|
||
"ai_response": fmt.Sprintf("❌ AI 调用失败: %s", aiErr.Error()),
|
||
"ai_error": aiErr.Error(),
|
||
"note": "AI 调用出错",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"system_prompt": systemPrompt,
|
||
"user_prompt": userPrompt,
|
||
"candidate_count": len(candidates),
|
||
"candidates": candidates,
|
||
"prompt_variant": req.PromptVariant,
|
||
"ai_response": aiResponse,
|
||
"note": "✅ 真实 AI 测试运行成功",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 返回结果(不实际调用 AI,只返回构建的 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 调用",
|
||
})
|
||
}
|
||
|
||
// runRealAITest 执行真实的 AI 测试调用
|
||
func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string) (string, error) {
|
||
// 获取 AI 模型配置
|
||
model, err := s.store.AIModel().Get(userID, modelID)
|
||
if err != nil {
|
||
return "", fmt.Errorf("获取 AI 模型失败: %w", err)
|
||
}
|
||
|
||
if !model.Enabled {
|
||
return "", fmt.Errorf("AI 模型 %s 尚未启用", model.Name)
|
||
}
|
||
|
||
if model.APIKey == "" {
|
||
return "", fmt.Errorf("AI 模型 %s 缺少 API Key", model.Name)
|
||
}
|
||
|
||
// 创建 AI 客户端
|
||
var aiClient mcp.AIClient
|
||
provider := model.Provider
|
||
|
||
switch provider {
|
||
case "qwen":
|
||
aiClient = mcp.NewQwenClient()
|
||
aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName)
|
||
case "deepseek":
|
||
aiClient = mcp.NewDeepSeekClient()
|
||
aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName)
|
||
default:
|
||
// 使用通用客户端
|
||
aiClient = mcp.NewClient()
|
||
aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName)
|
||
}
|
||
|
||
// 调用 AI API
|
||
response, err := aiClient.CallWithMessages(systemPrompt, userPrompt)
|
||
if err != nil {
|
||
return "", fmt.Errorf("AI API 调用失败: %w", err)
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|