Files
creator-hub/internal/creator/bailian.go
T

187 lines
6.0 KiB
Go

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)
}