Files
ttstd-infinite-canvas/backend/internal/app/provider_protocol_result.go
T

219 lines
7.2 KiB
Go

// 声明式协议插件的结果处理:输出提取、媒体下载重试与错误归类。
package app
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"strings"
"time"
"infinite-canvas/backend/internal/protocol"
)
func appendProtocolQuery(rawURL string, values map[string][]string) (string, error) {
if len(values) == 0 {
return rawURL, nil
}
parsed, err := url.Parse(rawURL)
if err != nil {
return "", err
}
query := parsed.Query()
for key, items := range values {
for _, item := range items {
query.Add(key, item)
}
}
parsed.RawQuery = query.Encode()
return parsed.String(), nil
}
func finishProtocolResult(ctx context.Context, config providerConfig, mode string, taskID string, result *protocol.Result, pollPolicy videoPollPolicy) (map[string]interface{}, error) {
if result == nil {
return nil, errors.New("声明式协议已完成但没有返回结果")
}
if mode == "text" {
output := map[string]interface{}{"mode": "text", "text": result.Text}
if strings.TrimSpace(result.Reasoning) != "" {
output["reasoning"] = result.Reasoning
}
return output, nil
}
var references []protocol.MediaReference
switch mode {
case "image":
references = result.Images
case "video":
references = result.Videos
case "audio":
references = result.Audios
default:
return nil, fmt.Errorf("声明式协议不支持生成模式 %s", mode)
}
if len(references) == 0 {
return nil, errors.New("声明式协议已完成但没有返回媒体地址")
}
if output, handled, err := recoverProtocolMedia(ctx, config, mode, references); handled {
return output, err
}
items := make([]interface{}, 0, len(references))
for _, reference := range references {
var data []byte
var mimeType string
var err error
if mode == "video" {
data, mimeType, err = runVideoDownload(ctx, taskID, pollPolicy, func(ctx context.Context) ([]byte, string, error) {
return protocolMediaBytesOnce(ctx, config, reference)
})
} else {
data, mimeType, err = protocolMediaBytes(ctx, config, reference)
}
if err != nil {
return nil, err
}
item := map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}
items = append(items, item)
}
switch mode {
case "image":
return map[string]interface{}{"mode": "image", "images": items}, nil
case "video":
return map[string]interface{}{"mode": "video", "video": items[0]}, nil
default:
return map[string]interface{}{"mode": "audio", "audio": items[0]}, nil
}
}
// finishProtocolAdapterResult 优先消费 create/poll 已返回的内联或 URL 结果,只有插件明确声明独立结果端点时才下载。
// 空响应或下载失败必须向上失败,不能生成伪素材;application/octet-stream 仅表示传输层未知类型,
// 不会把未知内容伪装成具体图片、视频或音频 MIME。
func finishProtocolAdapterResult(ctx context.Context, input canvasGenerationInput, adapter protocol.Adapter, request protocol.GenerationRequest, taskID string, result *protocol.Result, pollPolicy videoPollPolicy) (map[string]interface{}, error) {
if protocolResultHasOutput(input.Mode, result) {
return finishProtocolResult(ctx, input.Config, input.Mode, taskID, result, pollPolicy)
}
resultAdapter, ok := adapter.(protocol.ResultAdapter)
capability, hasCapability := adapter.(protocol.ResultCapability)
if !ok || !hasCapability || !capability.ResultAvailable() {
return finishProtocolResult(ctx, input.Config, input.Mode, taskID, result, pollPolicy)
}
spec, err := resultAdapter.BuildResult(ctx, protocol.PollContext{BaseURL: input.Config.BaseURL, Model: request.Model, Request: request, TaskID: taskID})
if err != nil {
return nil, err
}
download := func(ctx context.Context) ([]byte, string, error) {
return executeProtocolBinaryRequest(withProviderRequestKind(ctx, "download"), input.Config, spec)
}
var data []byte
var mimeType string
if input.Mode == "video" {
data, mimeType, err = runVideoDownload(ctx, taskID, pollPolicy, download)
} else {
data, mimeType, err = download(ctx)
}
if err != nil {
return nil, fmt.Errorf("声明式协议结果下载失败:%w", err)
}
if len(data) == 0 {
return nil, errors.New("声明式协议结果下载返回空内容")
}
mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
if mimeType == "" {
mimeType = "application/octet-stream"
}
reference := protocol.MediaReference{DataURL: dataURL(mimeType, data), MIMEType: mimeType}
downloaded := &protocol.Result{}
switch input.Mode {
case "image":
downloaded.Images = []protocol.MediaReference{reference}
case "video":
downloaded.Videos = []protocol.MediaReference{reference}
case "audio":
downloaded.Audios = []protocol.MediaReference{reference}
default:
return nil, fmt.Errorf("声明式协议结果下载不支持生成模式 %s", input.Mode)
}
return finishProtocolResult(ctx, input.Config, input.Mode, taskID, downloaded, pollPolicy)
}
func protocolResultHasOutput(mode string, result *protocol.Result) bool {
if result == nil {
return false
}
switch mode {
case "text":
return strings.TrimSpace(result.Text) != ""
case "image":
return len(result.Images) > 0
case "video":
return len(result.Videos) > 0
case "audio":
return len(result.Audios) > 0
default:
return false
}
}
func protocolMediaBytes(ctx context.Context, config providerConfig, reference protocol.MediaReference) ([]byte, string, error) {
var data []byte
var mimeType string
var err error
for attempt := 0; attempt < 3; attempt++ {
data, mimeType, err = protocolMediaBytesOnce(ctx, config, reference)
if err == nil {
return data, mimeType, nil
}
if attempt == 2 || !retryableProtocolMediaDownload(err) {
break
}
if waitErr := sleepContext(ctx, time.Duration(attempt+1)*time.Second); waitErr != nil {
return nil, "", fmt.Errorf("声明式协议媒体结果下载失败:%w", waitErr)
}
}
return nil, "", fmt.Errorf("声明式协议媒体结果下载失败:%w", err)
}
func protocolMediaBytesOnce(ctx context.Context, config providerConfig, reference protocol.MediaReference) ([]byte, string, error) {
if strings.TrimSpace(reference.DataURL) != "" {
mimeType, data, err := decodeProviderDataURL(reference.DataURL)
return data, mimeType, err
}
value := strings.TrimSpace(reference.URL)
if value == "" {
return nil, "", errors.New("声明式协议媒体结果地址为空")
}
data, mimeType, err := getProviderExternalBinary(withProviderRequestKind(ctx, "download"), config, value)
return data, normalizedMediaMimeType(mimeType, data), err
}
func retryableProtocolMediaDownload(err error) bool {
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
var networkError net.Error
if errors.As(err, &networkError) && (networkError.Timeout() || networkError.Temporary()) {
return true
}
message := strings.ToLower(err.Error())
for _, marker := range []string{"tls handshake timeout", "connection reset", "unexpected eof", "broken pipe"} {
if strings.Contains(message, marker) {
return true
}
}
return false
}
func protocolResultError(message, taskID string) error {
message = strings.TrimSpace(message)
if message == "" {
message = "上游返回失败状态"
}
if taskID == "" {
return errors.New(message)
}
return fmt.Errorf("声明式协议任务失败(任务 %s):%s", taskID, message)
}