package ai import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" ) // 使用阿里云DashScope的OpenAI兼容模式(官方推荐) const AliyunBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" type AliyunClient struct { apiKey string model string } func NewAliyunClient(cfg *Config) *AliyunClient { model := cfg.Model if model == "" { model = "qwen-turbo" } return &AliyunClient{ apiKey: cfg.APIKey, model: model, } } // OpenAI兼容格式的请求 type aliyunRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` Stream bool `json:"stream,omitempty"` } // OpenAI兼容格式的响应 type aliyunResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Index int `json:"index"` Message struct { Role string `json:"role"` Content string `json:"content"` } `json:"message"` Delta struct { Content string `json:"content"` } `json:"delta"` FinishReason string `json:"finish_reason"` } `json:"choices"` Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` Error *struct { Message string `json:"message"` Type string `json:"type"` Code string `json:"code"` } `json:"error"` } func (c *AliyunClient) Chat(ctx context.Context, messages []Message) (string, error) { if c.apiKey == "" { return "", fmt.Errorf("阿里云通义千问 API Key 未配置,请在 config.yaml 中设置 ai.aliyun.api_key") } reqBody := aliyunRequest{ Model: c.model, Messages: messages, Stream: false, } body, _ := json.Marshal(reqBody) req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.apiKey) resp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("调用AI服务失败: %v", err) } defer resp.Body.Close() var result aliyunResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", fmt.Errorf("解析AI响应失败: %v", err) } // 检查错误 if result.Error != nil { return "", fmt.Errorf("AI服务错误: %s (code: %s)", result.Error.Message, result.Error.Code) } if len(result.Choices) == 0 { return "", fmt.Errorf("AI未返回有效响应") } return result.Choices[0].Message.Content, nil } func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, writer io.Writer) error { if c.apiKey == "" { return fmt.Errorf("阿里云通义千问 API Key 未配置") } reqBody := aliyunRequest{ Model: c.model, Messages: messages, Stream: true, } body, _ := json.Marshal(reqBody) req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.apiKey) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() // 解析 SSE 流 reader := bufio.NewReader(resp.Body) for { line, err := reader.ReadString('\n') if err == io.EOF { break } if err != nil { return err } line = strings.TrimSpace(line) if strings.HasPrefix(line, "data:") { data := strings.TrimPrefix(line, "data:") data = strings.TrimSpace(data) if data == "[DONE]" { break } var streamResp aliyunResponse if err := json.Unmarshal([]byte(data), &streamResp); err != nil { continue } if len(streamResp.Choices) > 0 { content := streamResp.Choices[0].Delta.Content if content != "" { writer.Write([]byte(content)) } } } } return nil }