mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
338 lines
9.2 KiB
Go
338 lines
9.2 KiB
Go
package deepseek
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"ds2api/internal/auth"
|
|
"ds2api/internal/config"
|
|
trans "ds2api/internal/deepseek/transport"
|
|
"ds2api/internal/util"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
)
|
|
|
|
// intFrom is a package-internal alias for the shared util version.
|
|
var intFrom = util.IntFrom
|
|
|
|
type Client struct {
|
|
Store *config.Store
|
|
Auth *auth.Resolver
|
|
regular trans.Doer
|
|
stream trans.Doer
|
|
fallback *http.Client
|
|
fallbackS *http.Client
|
|
powSolver *PowSolver
|
|
maxRetries int
|
|
}
|
|
|
|
func NewClient(store *config.Store, resolver *auth.Resolver) *Client {
|
|
return &Client{
|
|
Store: store,
|
|
Auth: resolver,
|
|
regular: trans.New(60 * time.Second),
|
|
stream: trans.New(0),
|
|
fallback: &http.Client{Timeout: 60 * time.Second},
|
|
fallbackS: &http.Client{Timeout: 0},
|
|
powSolver: NewPowSolver(config.WASMPath()),
|
|
maxRetries: 3,
|
|
}
|
|
}
|
|
|
|
func (c *Client) PreloadPow(ctx context.Context) error {
|
|
return c.powSolver.init(ctx)
|
|
}
|
|
|
|
func (c *Client) Login(ctx context.Context, acc config.Account) (string, error) {
|
|
payload := map[string]any{
|
|
"password": strings.TrimSpace(acc.Password),
|
|
"device_id": "deepseek_to_api",
|
|
"os": "android",
|
|
}
|
|
if email := strings.TrimSpace(acc.Email); email != "" {
|
|
payload["email"] = email
|
|
} else if mobile := strings.TrimSpace(acc.Mobile); mobile != "" {
|
|
payload["mobile"] = mobile
|
|
payload["area_code"] = nil
|
|
} else {
|
|
return "", errors.New("missing email/mobile")
|
|
}
|
|
resp, err := c.postJSON(ctx, c.regular, DeepSeekLoginURL, BaseHeaders, payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
code := intFrom(resp["code"])
|
|
if code != 0 {
|
|
return "", fmt.Errorf("login failed: %v", resp["msg"])
|
|
}
|
|
data, _ := resp["data"].(map[string]any)
|
|
if intFrom(data["biz_code"]) != 0 {
|
|
return "", fmt.Errorf("login failed: %v", data["biz_msg"])
|
|
}
|
|
bizData, _ := data["biz_data"].(map[string]any)
|
|
user, _ := bizData["user"].(map[string]any)
|
|
token, _ := user["token"].(string)
|
|
if strings.TrimSpace(token) == "" {
|
|
return "", errors.New("missing login token")
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
func (c *Client) CreateSession(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error) {
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = c.maxRetries
|
|
}
|
|
attempts := 0
|
|
refreshed := false
|
|
for attempts < maxAttempts {
|
|
headers := c.authHeaders(a.DeepSeekToken)
|
|
resp, status, err := c.postJSONWithStatus(ctx, c.regular, DeepSeekCreateSessionURL, headers, map[string]any{"agent": "chat"})
|
|
if err != nil {
|
|
config.Logger.Warn("[create_session] request error", "error", err, "account", a.AccountID)
|
|
attempts++
|
|
continue
|
|
}
|
|
code := intFrom(resp["code"])
|
|
if status == http.StatusOK && code == 0 {
|
|
data, _ := resp["data"].(map[string]any)
|
|
bizData, _ := data["biz_data"].(map[string]any)
|
|
sessionID, _ := bizData["id"].(string)
|
|
if sessionID != "" {
|
|
return sessionID, nil
|
|
}
|
|
}
|
|
msg, _ := resp["msg"].(string)
|
|
config.Logger.Warn("[create_session] failed", "status", status, "code", code, "msg", msg, "use_config_token", a.UseConfigToken, "account", a.AccountID)
|
|
if a.UseConfigToken {
|
|
if isTokenInvalid(status, code, msg) && !refreshed {
|
|
if c.Auth.RefreshToken(ctx, a) {
|
|
refreshed = true
|
|
continue
|
|
}
|
|
}
|
|
if c.Auth.SwitchAccount(ctx, a) {
|
|
refreshed = false
|
|
attempts++
|
|
continue
|
|
}
|
|
}
|
|
attempts++
|
|
}
|
|
return "", errors.New("create session failed")
|
|
}
|
|
|
|
func (c *Client) GetPow(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error) {
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = c.maxRetries
|
|
}
|
|
attempts := 0
|
|
for attempts < maxAttempts {
|
|
headers := c.authHeaders(a.DeepSeekToken)
|
|
resp, status, err := c.postJSONWithStatus(ctx, c.regular, DeepSeekCreatePowURL, headers, map[string]any{"target_path": "/api/v0/chat/completion"})
|
|
if err != nil {
|
|
config.Logger.Warn("[get_pow] request error", "error", err, "account", a.AccountID)
|
|
attempts++
|
|
continue
|
|
}
|
|
code := intFrom(resp["code"])
|
|
if status == http.StatusOK && code == 0 {
|
|
data, _ := resp["data"].(map[string]any)
|
|
bizData, _ := data["biz_data"].(map[string]any)
|
|
challenge, _ := bizData["challenge"].(map[string]any)
|
|
answer, err := c.powSolver.Compute(ctx, challenge)
|
|
if err != nil {
|
|
attempts++
|
|
continue
|
|
}
|
|
return BuildPowHeader(challenge, answer)
|
|
}
|
|
msg, _ := resp["msg"].(string)
|
|
config.Logger.Warn("[get_pow] failed", "status", status, "code", code, "msg", msg, "use_config_token", a.UseConfigToken, "account", a.AccountID)
|
|
if a.UseConfigToken {
|
|
if isTokenInvalid(status, code, msg) {
|
|
if c.Auth.RefreshToken(ctx, a) {
|
|
continue
|
|
}
|
|
}
|
|
if c.Auth.SwitchAccount(ctx, a) {
|
|
attempts++
|
|
continue
|
|
}
|
|
}
|
|
attempts++
|
|
}
|
|
return "", errors.New("get pow failed")
|
|
}
|
|
|
|
func (c *Client) CallCompletion(ctx context.Context, a *auth.RequestAuth, payload map[string]any, powResp string, maxAttempts int) (*http.Response, error) {
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = c.maxRetries
|
|
}
|
|
headers := c.authHeaders(a.DeepSeekToken)
|
|
headers["x-ds-pow-response"] = powResp
|
|
attempts := 0
|
|
for attempts < maxAttempts {
|
|
resp, err := c.streamPost(ctx, DeepSeekCompletionURL, headers, payload)
|
|
if err != nil {
|
|
attempts++
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
if resp.StatusCode == http.StatusOK {
|
|
return resp, nil
|
|
}
|
|
_ = resp.Body.Close()
|
|
attempts++
|
|
time.Sleep(time.Second)
|
|
}
|
|
return nil, errors.New("completion failed")
|
|
}
|
|
|
|
func (c *Client) postJSON(ctx context.Context, doer trans.Doer, url string, headers map[string]string, payload any) (map[string]any, error) {
|
|
body, status, err := c.postJSONWithStatus(ctx, doer, url, headers, payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status == 0 {
|
|
return nil, errors.New("request failed")
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func (c *Client) postJSONWithStatus(ctx context.Context, doer trans.Doer, url string, headers map[string]string, payload any) (map[string]any, int, error) {
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := doer.Do(req)
|
|
if err != nil {
|
|
config.Logger.Warn("[deepseek] fingerprint request failed, fallback to std transport", "url", url, "error", err)
|
|
req2, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
|
if reqErr != nil {
|
|
return nil, 0, err
|
|
}
|
|
for k, v := range headers {
|
|
req2.Header.Set(k, v)
|
|
}
|
|
resp, err = c.fallback.Do(req2)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
defer resp.Body.Close()
|
|
payloadBytes, err := readResponseBody(resp)
|
|
if err != nil {
|
|
return nil, resp.StatusCode, err
|
|
}
|
|
out := map[string]any{}
|
|
if len(payloadBytes) > 0 {
|
|
if err := json.Unmarshal(payloadBytes, &out); err != nil {
|
|
config.Logger.Warn("[deepseek] json parse failed", "url", url, "status", resp.StatusCode, "content_encoding", resp.Header.Get("Content-Encoding"), "preview", preview(payloadBytes))
|
|
}
|
|
}
|
|
return out, resp.StatusCode, nil
|
|
}
|
|
|
|
func (c *Client) streamPost(ctx context.Context, url string, headers map[string]string, payload any) (*http.Response, error) {
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := c.stream.Do(req)
|
|
if err != nil {
|
|
config.Logger.Warn("[deepseek] fingerprint stream request failed, fallback to std transport", "url", url, "error", err)
|
|
req2, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
|
if reqErr != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range headers {
|
|
req2.Header.Set(k, v)
|
|
}
|
|
return c.fallbackS.Do(req2)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (c *Client) authHeaders(token string) map[string]string {
|
|
headers := make(map[string]string, len(BaseHeaders)+1)
|
|
for k, v := range BaseHeaders {
|
|
headers[k] = v
|
|
}
|
|
headers["authorization"] = "Bearer " + token
|
|
return headers
|
|
}
|
|
|
|
func isTokenInvalid(status int, code int, msg string) bool {
|
|
msg = strings.ToLower(msg)
|
|
if status == http.StatusUnauthorized || status == http.StatusForbidden {
|
|
return true
|
|
}
|
|
if code == 40001 || code == 40002 || code == 40003 {
|
|
return true
|
|
}
|
|
return strings.Contains(msg, "token") || strings.Contains(msg, "unauthorized")
|
|
}
|
|
|
|
func readResponseBody(resp *http.Response) ([]byte, error) {
|
|
encoding := strings.ToLower(strings.TrimSpace(resp.Header.Get("Content-Encoding")))
|
|
var reader io.Reader = resp.Body
|
|
switch encoding {
|
|
case "gzip":
|
|
gz, err := gzip.NewReader(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gz.Close()
|
|
reader = gz
|
|
case "br":
|
|
reader = brotli.NewReader(resp.Body)
|
|
}
|
|
return io.ReadAll(reader)
|
|
}
|
|
|
|
func preview(b []byte) string {
|
|
s := strings.TrimSpace(string(b))
|
|
if len(s) > 160 {
|
|
return s[:160]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func ScanSSELines(resp *http.Response, onLine func([]byte) bool) error {
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
buf := make([]byte, 0, 64*1024)
|
|
scanner.Buffer(buf, 2*1024*1024)
|
|
for scanner.Scan() {
|
|
if !onLine(scanner.Bytes()) {
|
|
break
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|