113 lines
2.5 KiB
Go
113 lines
2.5 KiB
Go
package spark
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Message struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type chatRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []Message `json:"messages"`
|
|
Stream bool `json:"stream"`
|
|
Temperature float64 `json:"temperature,omitempty"`
|
|
MaxTokens int `json:"max_tokens,omitempty"`
|
|
}
|
|
|
|
type chatResponse struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Choices []struct {
|
|
Message Message `json:"message"`
|
|
} `json:"choices"`
|
|
}
|
|
|
|
type Client struct {
|
|
APIURL string
|
|
Password string
|
|
Model string
|
|
HTTP *http.Client
|
|
}
|
|
|
|
func NewFromEnv() *Client {
|
|
return &Client{
|
|
APIURL: strings.TrimSpace(envOr("SPARK_API_URL", "https://spark-api-open.xf-yun.com/v1/chat/completions")),
|
|
Password: strings.TrimSpace(os.Getenv("SPARK_API_PASSWORD")),
|
|
Model: strings.TrimSpace(envOr("SPARK_MODEL", "lite")),
|
|
HTTP: &http.Client{Timeout: 45 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (c *Client) Enabled() bool {
|
|
return c != nil && c.Password != ""
|
|
}
|
|
|
|
func (c *Client) Chat(messages []Message) (string, error) {
|
|
if !c.Enabled() {
|
|
return "", fmt.Errorf("AI 未配置")
|
|
}
|
|
body, _ := json.Marshal(chatRequest{
|
|
Model: c.Model,
|
|
Messages: messages,
|
|
Stream: false,
|
|
Temperature: 0.8,
|
|
MaxTokens: 512,
|
|
})
|
|
req, err := http.NewRequest(http.MethodPost, c.APIURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+c.Password)
|
|
|
|
res, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer res.Body.Close()
|
|
raw, _ := io.ReadAll(res.Body)
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return "", fmt.Errorf("星火接口 HTTP %d: %s", res.StatusCode, truncate(string(raw), 200))
|
|
}
|
|
var parsed chatResponse
|
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
|
return "", fmt.Errorf("解析星火响应失败: %w", err)
|
|
}
|
|
if parsed.Code != 0 {
|
|
return "", fmt.Errorf("星火返回错误 %d: %s", parsed.Code, parsed.Message)
|
|
}
|
|
if len(parsed.Choices) == 0 {
|
|
return "", fmt.Errorf("星火未返回内容")
|
|
}
|
|
text := strings.TrimSpace(parsed.Choices[0].Message.Content)
|
|
if text == "" {
|
|
return "", fmt.Errorf("星火返回空内容")
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
func envOr(k, def string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func truncate(s string, n int) string {
|
|
r := []rune(s)
|
|
if len(r) <= n {
|
|
return s
|
|
}
|
|
return string(r[:n]) + "…"
|
|
}
|