package deepseek import ( "context" "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 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) } 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 := 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, errors.New("fetch files succeeded without matching file data") } 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 } }