package creator import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) const defaultBailianBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" type BailianClient struct { BaseURL string APIKey string Model string HTTPClient *http.Client } func NewBailianClient(baseURL, apiKey, model string, client *http.Client) (*BailianClient, error) { baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") if baseURL == "" { baseURL = defaultBailianBaseURL } if strings.TrimSpace(apiKey) == "" || strings.TrimSpace(model) == "" { return nil, fmt.Errorf("%w: BAILIAN_API_KEY and AI model are required", ErrUnavailable) } if client == nil { client = &http.Client{Timeout: 60 * time.Second} } return &BailianClient{BaseURL: baseURL, APIKey: apiKey, Model: model, HTTPClient: client}, nil } type bailianChatRequest struct { Model string `json:"model"` Messages []bailianChatMessage `json:"messages"` } type bailianChatMessage struct { Role string `json:"role"` Content string `json:"content"` } type bailianChatResponse struct { Choices []struct { Message bailianChatMessage `json:"message"` } `json:"choices"` } func (c *BailianClient) chat(ctx context.Context, instruction, input string) (string, error) { if c == nil || c.HTTPClient == nil || strings.TrimSpace(c.APIKey) == "" || strings.TrimSpace(c.Model) == "" { return "", fmt.Errorf("%w: BAILIAN client is not configured", ErrUnavailable) } body, err := json.Marshal(bailianChatRequest{ Model: c.Model, Messages: []bailianChatMessage{ {Role: "system", Content: instruction}, {Role: "user", Content: input}, }, }) if err != nil { return "", err } endpoint := c.BaseURL + "/chat/completions" if strings.HasSuffix(c.BaseURL, "/chat/completions") { endpoint = c.BaseURL } request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return "", err } request.Header.Set("Authorization", "Bearer "+c.APIKey) request.Header.Set("Content-Type", "application/json") response, err := c.HTTPClient.Do(request) if err != nil { return "", err } defer response.Body.Close() responseBody, err := io.ReadAll(io.LimitReader(response.Body, 2<<20)) if err != nil { return "", err } if response.StatusCode < 200 || response.StatusCode >= 300 { return "", fmt.Errorf("bailian request failed with HTTP %d: %s", response.StatusCode, strings.TrimSpace(string(responseBody))) } var result bailianChatResponse if err := json.Unmarshal(responseBody, &result); err != nil { return "", fmt.Errorf("decode bailian response: %w", err) } if len(result.Choices) == 0 || strings.TrimSpace(result.Choices[0].Message.Content) == "" { return "", fmt.Errorf("bailian response contained no message") } return strings.TrimSpace(result.Choices[0].Message.Content), nil } func (c *BailianClient) Generate(ctx context.Context, instruction, input string) (string, error) { return c.chat(ctx, instruction, input) } func (c *BailianClient) MatchTheme(ctx context.Context, title, body, topic string) (bool, string, error) { content, err := c.chat(ctx, "判断作品是否符合给定主题。只返回 JSON,不要 Markdown 或额外文字,格式必须是 {\"match\":true或false,\"reason\":\"简短原因\"}。", fmt.Sprintf("主题:%s\n标题:%s\n正文:%s", topic, title, body)) if err != nil { return false, "", err } var result struct { Match *bool `json:"match"` Reason string `json:"reason"` } if err := json.Unmarshal([]byte(content), &result); err != nil || result.Match == nil { if err != nil { return false, "", fmt.Errorf("decode bailian theme result: %w", err) } return false, "", fmt.Errorf("decode bailian theme result: match is required") } return *result.Match, strings.TrimSpace(result.Reason), nil } func (c *BailianClient) MatchLead(ctx context.Context, work, comment, requirement string) (bool, string, error) { content, err := c.chat(ctx, "判断评论是否是有效业务线索。只返回 JSON,不要 Markdown 或额外文字,格式必须是 {\"match\":true或false,\"reason\":\"简短原因\"}。", fmt.Sprintf("判定要求:%s\n作品:%s\n评论:%s", requirement, work, comment)) if err != nil { return false, "", err } var result struct { Match *bool `json:"match"` Reason string `json:"reason"` } if err := json.Unmarshal([]byte(content), &result); err != nil || result.Match == nil { if err != nil { return false, "", fmt.Errorf("decode bailian lead result: %w", err) } return false, "", fmt.Errorf("decode bailian lead result: match is required") } return *result.Match, strings.TrimSpace(result.Reason), nil } type ConfiguredBailian struct { Store *Store APIKey string BaseURL string HTTPClient *http.Client } func (b *ConfiguredBailian) client(ctx context.Context) (*BailianClient, error) { if b == nil || b.Store == nil { return nil, fmt.Errorf("%w: BAILIAN client is not configured", ErrUnavailable) } settings, err := b.Store.GetSettings(ctx) if err != nil { return nil, err } if !settings.AIConfigured || settings.AIProvider != "bailian" { return nil, fmt.Errorf("%w: BAILIAN is not enabled in creator settings", ErrUnavailable) } return NewBailianClient(b.BaseURL, b.APIKey, settings.AIModel, b.HTTPClient) } func (b *ConfiguredBailian) Generate(ctx context.Context, instruction, input string) (string, error) { client, err := b.client(ctx) if err != nil { return "", err } return client.Generate(ctx, instruction, input) } func (b *ConfiguredBailian) MatchTheme(ctx context.Context, title, body, topic string) (bool, string, error) { client, err := b.client(ctx) if err != nil { return false, "", err } return client.MatchTheme(ctx, title, body, topic) } func (b *ConfiguredBailian) MatchLead(ctx context.Context, work, comment, requirement string) (bool, string, error) { client, err := b.client(ctx) if err != nil { return false, "", err } return client.MatchLead(ctx, work, comment, requirement) }