Files
ttstd-infinite-canvas/backend/internal/app/provider_error.go

153 lines
4.2 KiB
Go

package app
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
)
const contentModerationErrorCode = "sensitive_words_detected"
const contentModerationRetryMessage = "内容审核未通过,请修改提示词后重新生成;原任务不能直接重试"
// 只提取供应商明确返回的错误码和短消息,避免把完整响应或用户输入复制到调用日志。
func providerFailureDetails(payload map[string]any) (string, string) {
candidates := make([]map[string]any, 0, 3)
for _, key := range []string{"error", "data"} {
if nested, ok := payload[key].(map[string]any); ok {
candidates = append(candidates, nested)
}
}
// 内层通常是供应商业务错误,外层 code 可能只是 HTTP 包装码。
candidates = append(candidates, payload)
code := ""
message := ""
for _, candidate := range candidates {
if code == "" {
code = normalizedProviderErrorCode(candidate["code"])
}
if message == "" {
message = strings.TrimSpace(stringField(candidate, "message"))
if message == "" {
message = strings.TrimSpace(stringField(candidate, "msg"))
}
}
}
return code, truncateRunes(message, 500)
}
func providerResponseBusinessFailure(responseBody []byte) (string, string, bool) {
if len(responseBody) == 0 {
return "", "", false
}
// 火山单向语音合成把多帧 JSON 直接拼在同一个 HTTP 200 里。只解析第一帧会把后面的业务失败当成成功。
decoder := json.NewDecoder(bytes.NewReader(responseBody))
decoder.UseNumber()
decoded := false
for {
var payload map[string]any
err := decoder.Decode(&payload)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
if !decoded {
return "", "", false
}
break
}
decoded = true
if payload == nil {
continue
}
if code, message, failed := providerPayloadBusinessFailure(payload); failed {
return code, message, true
}
}
return "", "", false
}
func providerPayloadBusinessFailure(payload map[string]any) (string, string, bool) {
if header, ok := payload["header"].(map[string]any); ok {
// 只认火山语音合成这种明确的资源拒绝。header.code 在别的协议里可能是 HTTP 状态,不能一律当成业务失败。
rawMessage := strings.TrimSpace(stringField(header, "message"))
if speechResourceDeniedUserMessage(rawMessage) != "" {
if code, message, failed := providerBusinessFailure(header); failed {
return code, message, true
}
return "resource_not_granted", rawMessage, true
}
}
if code, message, failed := providerBusinessFailure(payload); failed {
return code, message, true
}
// DashScope 业务失败在 output 内
if output, ok := payload["output"].(map[string]any); ok {
return providerBusinessFailure(output)
}
return "", "", false
}
func providerBusinessFailure(payload map[string]any) (string, string, bool) {
if errorValue, ok := payload["error"].(map[string]any); ok {
code, message := providerFailureDetails(map[string]any{"error": errorValue})
if code != "" || message != "" {
return code, message, true
}
}
code := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["code"])))
if code != "" && code != "0" &&
code != "success" && code != "succeeded" &&
code != "ok" && code != "<nil>" {
code, message := providerFailureDetails(payload)
return code, message, true
}
status := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["task_status"])))
switch status {
case "failed", "failure", "error", "expired":
code, message := providerFailureDetails(payload)
if code == "" {
code = "task_failed"
}
return code, message, true
}
return "", "", false
}
func normalizedProviderErrorCode(value any) string {
var code string
switch current := value.(type) {
case string:
code = current
case fmt.Stringer:
code = current.String()
case float64:
if current != 0 {
code = fmt.Sprintf("%g", current)
}
case int:
if current != 0 {
code = fmt.Sprintf("%d", current)
}
case int64:
if current != 0 {
code = fmt.Sprintf("%d", current)
}
}
code = strings.TrimSpace(code)
if code == "0" {
return ""
}
return truncateRunes(code, 80)
}
func isContentModerationFailure(value string) bool {
return strings.Contains(strings.ToLower(value), contentModerationErrorCode)
}