feat(gateway): 补齐桌面端高级媒体直连接口
ci / verify (pull_request) Successful in 15m34s
ci / verify (pull_request) Successful in 15m34s
新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。 已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
This commit is contained in:
@@ -8,10 +8,12 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
@@ -85,6 +87,11 @@ func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, task
|
||||
if err != nil {
|
||||
return nil, &clients.ClientError{Code: "upload_config_failed", Message: err.Error(), Retryable: true}
|
||||
}
|
||||
// Topaz download URLs are short-lived. Persist them before the task can be
|
||||
// marked succeeded even when the global policy normally keeps URL media.
|
||||
if taskKind == "videos.upscales" {
|
||||
policy.UploadURLMedia = true
|
||||
}
|
||||
decisions := make([]generatedAssetDecision, len(data))
|
||||
needsUpload := false
|
||||
changed := false
|
||||
@@ -723,7 +730,8 @@ func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURL
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
allowLocal := strings.HasPrefix(strings.TrimSpace(asset.URL), "/")
|
||||
resp, err := generatedAssetHTTPClient(allowLocal).Do(req)
|
||||
if err != nil {
|
||||
return nil, "", &clients.ClientError{Code: "upload_source_fetch_failed", Message: err.Error(), Retryable: true}
|
||||
}
|
||||
@@ -752,6 +760,47 @@ func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURL
|
||||
return payload, strings.TrimSpace(strings.Split(contentType, ";")[0]), nil
|
||||
}
|
||||
|
||||
func generatedAssetHTTPClient(allowLocal bool) *http.Client {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = nil
|
||||
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host)
|
||||
if err != nil || len(addresses) == 0 {
|
||||
return nil, errors.New("generated media DNS resolution failed")
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if generatedAssetBlockedAddress(address.IP) && !(allowLocal && address.IP.IsLoopback()) {
|
||||
return nil, errors.New("generated media resolved to a blocked network")
|
||||
}
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: 10 * time.Second}
|
||||
var attempts []error
|
||||
for _, resolved := range addresses {
|
||||
connection, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(resolved.IP.String(), port))
|
||||
if dialErr == nil {
|
||||
return connection, nil
|
||||
}
|
||||
attempts = append(attempts, dialErr)
|
||||
}
|
||||
return nil, errors.Join(attempts...)
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 10 * time.Minute,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func generatedAssetBlockedAddress(ip net.IP) bool {
|
||||
return ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast()
|
||||
}
|
||||
|
||||
func (s *Service) generatedAssetFetchURL(raw string) (string, error) {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
@@ -1304,6 +1353,9 @@ func mediaContentTypeFromItem(item map[string]any) string {
|
||||
|
||||
func mediaKindForAsset(taskKind string, item map[string]any, sourceKey string, contentType string) string {
|
||||
contentType = strings.ToLower(strings.TrimSpace(contentType))
|
||||
if generatedContentTypeIsDocument(contentType) {
|
||||
return "file"
|
||||
}
|
||||
if strings.HasPrefix(contentType, "image/") {
|
||||
return "image"
|
||||
}
|
||||
@@ -1314,6 +1366,9 @@ func mediaKindForAsset(taskKind string, item map[string]any, sourceKey string, c
|
||||
return "audio"
|
||||
}
|
||||
itemType := strings.ToLower(strings.TrimSpace(stringFromAny(item["type"])))
|
||||
if itemType == "file" || strings.Contains(itemType, "document") {
|
||||
return "file"
|
||||
}
|
||||
if strings.Contains(itemType, "video") {
|
||||
return "video"
|
||||
}
|
||||
@@ -1346,6 +1401,8 @@ func defaultContentTypeForGeneratedAsset(kind string) string {
|
||||
return "video/mp4"
|
||||
case "audio":
|
||||
return "audio/mpeg"
|
||||
case "file":
|
||||
return "application/octet-stream"
|
||||
default:
|
||||
return "image/png"
|
||||
}
|
||||
@@ -1354,10 +1411,10 @@ func defaultContentTypeForGeneratedAsset(kind string) string {
|
||||
func resolvedGeneratedAssetContentType(declared string, kind string, payload []byte) string {
|
||||
declared = normalizeGeneratedContentType(declared)
|
||||
detected := detectGeneratedAssetContentType(payload)
|
||||
if generatedContentTypeIsMedia(detected) {
|
||||
if generatedContentTypeIsMedia(detected) || generatedContentTypeIsDocument(detected) {
|
||||
return detected
|
||||
}
|
||||
if generatedContentTypeIsMedia(declared) {
|
||||
if generatedContentTypeIsMedia(declared) || generatedContentTypeIsDocument(declared) {
|
||||
return declared
|
||||
}
|
||||
return defaultContentTypeForGeneratedAsset(kind)
|
||||
@@ -1380,6 +1437,15 @@ func generatedContentTypeIsMedia(contentType string) bool {
|
||||
strings.HasPrefix(contentType, "audio/")
|
||||
}
|
||||
|
||||
func generatedContentTypeIsDocument(contentType string) bool {
|
||||
switch normalizeGeneratedContentType(contentType) {
|
||||
case "application/pdf", "application/postscript", "application/eps", "application/dxf", "image/vnd.dxf":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func generatedAssetKindFromContentType(fallback string, contentType string) string {
|
||||
contentType = normalizeGeneratedContentType(contentType)
|
||||
if strings.HasPrefix(contentType, "image/") {
|
||||
@@ -1391,6 +1457,9 @@ func generatedAssetKindFromContentType(fallback string, contentType string) stri
|
||||
if strings.HasPrefix(contentType, "audio/") {
|
||||
return "audio"
|
||||
}
|
||||
if generatedContentTypeIsDocument(contentType) {
|
||||
return "file"
|
||||
}
|
||||
fallback = strings.ToLower(strings.TrimSpace(fallback))
|
||||
if fallback != "" {
|
||||
return fallback
|
||||
@@ -1434,6 +1503,14 @@ func randomHexSuffix(byteCount int) string {
|
||||
func fileExtensionForContentType(contentType string, kind string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
|
||||
switch normalized {
|
||||
case "application/pdf":
|
||||
return ".pdf"
|
||||
case "application/postscript", "application/eps":
|
||||
return ".eps"
|
||||
case "application/dxf", "image/vnd.dxf":
|
||||
return ".dxf"
|
||||
}
|
||||
switch normalized {
|
||||
case "image/jpeg", "image/jpg":
|
||||
return ".jpg"
|
||||
case "image/webp":
|
||||
|
||||
Reference in New Issue
Block a user