FILE / ScuroNeko/go-deepseek
client.go
Исходный файл и его история в репозитории.
216 lines
5.4 KiB
Go
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",
|
|
}
|
|
}
|