Files
ds2api/internal/deepseek/client/client_file_status.go
2026-05-02 14:33:42 +07:00

194 lines
5.1 KiB
Go

package client
import (
"context"
dsprotocol "ds2api/internal/deepseek/protocol"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"ds2api/internal/auth"
"ds2api/internal/config"
)
const (
fileReadyPollAttempts = 60
fileReadyPollInterval = time.Second
fileReadyPollTimeout = 65 * time.Second
)
var fileReadySleep = time.Sleep
// ErrUploadFileNotFound indicates that DeepSeek returned no matching uploaded file.
var ErrUploadFileNotFound = errors.New("uploaded file not found")
func (c *Client) waitForUploadedFile(ctx context.Context, a *auth.RequestAuth, result *UploadFileResult) error {
if result == nil || strings.TrimSpace(result.ID) == "" {
return nil
}
if isReadyUploadFileStatus(result.Status) {
return nil
}
pollCtx, cancel := context.WithTimeout(ctx, fileReadyPollTimeout)
defer cancel()
var lastErr error
for attempt := 0; attempt < fileReadyPollAttempts; attempt++ {
if err := pollCtx.Err(); err != nil {
if lastErr != nil {
return fmt.Errorf("waiting for file %s to become ready: %w", result.ID, lastErr)
}
return fmt.Errorf("waiting for file %s to become ready: %w", result.ID, err)
}
fetched, err := c.FetchUploadedFile(pollCtx, a, result.ID)
if err == nil && fetched != nil {
mergeUploadFileResults(result, fetched)
if isReadyUploadFileStatus(result.Status) {
return nil
}
lastErr = fmt.Errorf("status=%s", strings.TrimSpace(result.Status))
} else if err != nil {
lastErr = err
config.Logger.Debug("[upload_file] waiting for file readiness", "file_id", result.ID, "attempt", attempt+1, "error", err)
}
if attempt < fileReadyPollAttempts-1 {
fileReadySleep(fileReadyPollInterval)
}
}
if lastErr == nil {
lastErr = fmt.Errorf("status=%s", strings.TrimSpace(result.Status))
}
return fmt.Errorf("file %s did not become ready: %w", result.ID, lastErr)
}
// FetchUploadedFile returns metadata for an uploaded DeepSeek file by ID.
func (c *Client) FetchUploadedFile(ctx context.Context, a *auth.RequestAuth, fileID string) (*UploadFileResult, error) {
fileID = strings.TrimSpace(fileID)
if fileID == "" {
return nil, errors.New("file id is required")
}
clients := c.requestClientsForAuth(ctx, a)
reqURL := dsprotocol.DeepSeekFetchFilesURL + "?file_ids=" + url.QueryEscape(fileID)
headers := c.authHeaders(a.DeepSeekToken)
resp, status, err := c.getJSONWithStatus(ctx, clients.regular, reqURL, headers)
if err != nil {
return nil, err
}
code, bizCode, msg, bizMsg := extractResponseStatus(resp)
if status != http.StatusOK || code != 0 || bizCode != 0 {
if strings.TrimSpace(bizMsg) != "" {
msg = bizMsg
}
if msg == "" {
msg = http.StatusText(status)
}
return nil, fmt.Errorf("request failed: status=%d, code=%d, msg=%s", status, code, msg)
}
result := extractFetchedUploadFileResult(resp, fileID)
if result == nil || strings.TrimSpace(result.ID) == "" {
return nil, ErrUploadFileNotFound
}
result.Raw = resp
return result, nil
}
func extractFetchedUploadFileResult(resp map[string]any, targetID string) *UploadFileResult {
targetID = strings.TrimSpace(targetID)
if resp == nil || targetID == "" {
return nil
}
var walk func(any) *UploadFileResult
walk = func(v any) *UploadFileResult {
switch x := v.(type) {
case map[string]any:
if result := buildUploadFileResultFromMap(x, targetID); result != nil {
return result
}
for _, nested := range x {
if result := walk(nested); result != nil {
return result
}
}
case []any:
for _, item := range x {
if result := walk(item); result != nil {
return result
}
}
}
return nil
}
if result := walk(resp); result != nil {
return result
}
return nil
}
func buildUploadFileResultFromMap(m map[string]any, targetID string) *UploadFileResult {
fileID := strings.TrimSpace(firstNonEmptyString(m, "id", "file_id"))
if fileID == "" || !strings.EqualFold(fileID, targetID) {
return nil
}
result := &UploadFileResult{
ID: fileID,
Filename: firstNonEmptyString(m, "name", "filename", "file_name"),
Status: firstNonEmptyString(m, "status", "file_status"),
Purpose: firstNonEmptyString(m, "purpose"),
IsImage: firstBool(m, "is_image", "isImage"),
Bytes: firstPositiveInt64(m, "bytes", "size", "file_size"),
}
if result.Status == "" {
result.Status = "uploaded"
}
return result
}
func mergeUploadFileResults(dst, src *UploadFileResult) {
if dst == nil || src == nil {
return
}
if strings.TrimSpace(src.ID) != "" {
dst.ID = strings.TrimSpace(src.ID)
}
if strings.TrimSpace(src.Filename) != "" {
dst.Filename = strings.TrimSpace(src.Filename)
}
if src.Bytes > 0 {
dst.Bytes = src.Bytes
}
if strings.TrimSpace(src.Status) != "" {
dst.Status = strings.TrimSpace(src.Status)
}
if strings.TrimSpace(src.Purpose) != "" {
dst.Purpose = strings.TrimSpace(src.Purpose)
}
dst.IsImage = src.IsImage
if len(src.Raw) > 0 {
dst.Raw = src.Raw
}
if src.RawHeaders != nil {
dst.RawHeaders = src.RawHeaders.Clone()
}
}
func isReadyUploadFileStatus(status string) bool {
switch strings.ToLower(strings.TrimSpace(status)) {
case "processed", "ready", "done", "available", "success", "completed", "finished":
return true
default:
return false
}
}