FILE / ScuroNeko/go-deepseek

client.go

Исходный файл и его история в репозитории.
FILE 92ceb8bec3f3c9cbed878886d1257ab462d71145
Files
go-deepseek/client.go
T
2026-07-31 14:21:48 +03:00

216 lines
5.4 KiB
Go

package deepseek
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
)
const baseURL = "https://chat.deepseek.com/api/v0"
// NoParams is an empty JSON object used by endpoints without request fields.
var NoParams struct{}
// Response is the outer response envelope returned by the DeepSeek API.
type Response[T any] struct {
Code int `json:"code"`
Message string `json:"msg"`
Data T `json:"data"`
}
// BizResponse is the business-level response envelope nested inside Response.
type BizResponse[T any] struct {
Code int `json:"biz_code"`
Message string `json:"biz_msg"`
Data T `json:"biz_data"`
}
// Client is a configurable client for the DeepSeek chat API.
//
// A client may be reused for multiple requests. Callers must not mutate its
// configuration concurrently with active requests.
type Client struct {
httpClient *http.Client
token string
baseURL string
deviceIDProvider DeviceIDProvider
}
// NewClient creates a client with the default endpoint and a cookie jar.
func NewClient() *Client {
jar, err := cookiejar.New(nil)
if err != nil {
log.Println(err)
}
return &Client{httpClient: &http.Client{Jar: jar}, baseURL: baseURL}
}
// SetToken sets the bearer token used by subsequent requests.
func (api *Client) SetToken(token string) *Client {
api.token = token
return api
}
// SetDeviceIDProvider sets the source used to obtain login device IDs.
// Passing nil restores the random provider behavior used by Login.
func (api *Client) SetDeviceIDProvider(p DeviceIDProvider) *Client {
api.deviceIDProvider = p
return api
}
// SetHTTPClient replaces the underlying HTTP client when client is non-nil.
func (api *Client) SetHTTPClient(client *http.Client) *Client {
if client != nil {
api.httpClient = client
}
return api
}
// SetBaseURL replaces the API base URL when url is non-empty.
func (api *Client) SetBaseURL(url string) *Client {
if url != "" {
api.baseURL = url
}
return api
}
// Request describes an HTTP request with parameter type P and response data
// type R.
type Request[P, R any] struct {
params P
method string
path string
powAnswer string
}
// NewRequest creates a typed API request.
func NewRequest[R, P any](method, path string, params P) *Request[P, R] {
return &Request[P, R]{params: params, method: method, path: path}
}
func (r *Request[P, R]) marshallRequest() ([]byte, error) {
data, err := json.Marshal(r.params)
if err != nil {
return nil, err
}
return data, err
}
func (r *Request[P, R]) unmarshallResponse(resp *http.Response) (Response[R], error) {
var zero Response[R]
data, err := io.ReadAll(resp.Body)
if err != nil {
return zero, err
}
err = json.Unmarshal(data, &zero)
if err != nil {
return zero, err
}
if zero.Code != 0 {
return zero, fmt.Errorf("unknown error: %s (%d)", zero.Message, zero.Code)
}
return zero, err
}
func (r *Request[P, R]) unmarshallBizResponse(resp *http.Response) (Response[BizResponse[R]], error) {
var zero Response[BizResponse[R]]
data, err := io.ReadAll(resp.Body)
if err != nil {
return zero, err
}
err = json.Unmarshal(data, &zero)
if err != nil {
return zero, err
}
if zero.Code != 0 {
return zero, fmt.Errorf("unknown error: %s (%d)", zero.Message, zero.Code)
}
if zero.Data.Code != 0 {
return zero, fmt.Errorf("unknown error: %s (%d)", zero.Data.Message, zero.Data.Code)
}
return zero, err
}
// SolvePow requests and solves a proof-of-work challenge for r.
func (r *Request[P, R]) SolvePow(ctx context.Context, api *Client) error {
res, err := api.CreatePowChallengeWithContext(ctx)
if err != nil {
return err
}
data, err := res.Challenge.ToBase64()
if err != nil {
return err
}
r.powAnswer = string(data)
return nil
}
// DoWithContext sends r using api and returns the raw HTTP response.
// The caller owns and must close the response body.
func (r *Request[P, R]) DoWithContext(ctx context.Context, api *Client) (*http.Response, error) {
data, err := r.marshallRequest()
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, r.method, api.baseURL+"/"+r.path, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
log.Printf(
"request: method=%s scheme=%s host=%s path=%s rawQuery=%s",
req.Method,
req.URL.Scheme,
req.URL.Host,
req.URL.Path,
req.URL.RawQuery,
)
headers := getHeaders()
if r.powAnswer != "" {
headers["x-ds-pow-response"] = r.powAnswer
}
if api.token != "" {
headers["authorization"] = "Bearer " + api.token
}
for k, v := range headers {
req.Header.Add(k, v)
}
resp, err := api.httpClient.Do(req)
if err != nil {
return nil, err
}
log.Printf("response: method=%s status=%s code=%d",
req.Method,
resp.Status,
resp.StatusCode,
)
return resp, err
}
// Do sends r using a background context.
// The caller owns and must close the response body.
func (r *Request[P, R]) Do(api *Client) (*http.Response, error) {
return r.DoWithContext(context.Background(), api)
}
func getHeaders() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "DeepSeek/2 CFNetwork/1568.100.1 Darwin/24.0.0",
"x-client-platform": "ios",
"x-client-version": "2.0.4",
"x-client-bundle-id": "com.deepseek.chat",
"x-client-locale": "en_US",
"x-client-timezone-offset": "3600",
}
}