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

250 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"encoding/json"
"errors"
"log"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"yingce/backend/internal/model"
)
// 这组函数与前端 web/src/lib/generation-error.ts 的 generationErrorMessage 保持同一套分类,
// 让画布节点和 Agent 对同一个失败说同一句话:审核、存储、网络、HTTP 状态分别归类,
// 其余可读的供应商原因原样保留,而不是统一成"生成失败"。
const (
taskErrorModerationMessage = "内容审核未通过,本次平台积分未扣除或已退还。请修改提示词后重新生成。"
taskErrorDefaultMessage = "生成失败,请稍后重试。"
taskErrorNetworkMessage = "网络异常。"
)
var (
taskErrorNetworkPattern = regexp.MustCompile(`(?i)\b(?:dial tcp|connection refused|connection reset|no such host|i/o timeout|context deadline exceeded|network error|failed to fetch|fetch failed|socket hang up|econnrefused|econnreset|etimedout|broken pipe|unexpected eof|tls handshake timeout)\b`)
taskErrorInfrastructurePattern = regexp.MustCompile(`(?i)(?:接口请求失败|Request failed with status code|https?://|\b(?:GET|POST|PUT|PATCH|DELETE)\s+["']?|Bad Gateway|Service Unavailable|Gateway Timeout|upstream_error|\bread tcp\b|\bwrite tcp\b)`)
taskErrorWrappedInterface = regexp.MustCompile(`(?s)^接口请求失败[::]\s*(.*)$`)
taskErrorWrappedRequest = regexp.MustCompile(`(?is)^Request failed with status code \d{3}\s*[::-]?\s*(.+)$`)
taskErrorWrappedStatusPrefix = regexp.MustCompile(`(?i)^\d{3}(?:\s+(?:Bad Gateway|Service Unavailable|Gateway Timeout|Internal Server Error|Not Found|Unauthorized|Forbidden|Too Many Requests))?\s*[::-]?\s*`)
taskErrorStorageDisabled = regexp.MustCompile(`(?i)\bUserDisable\b`)
taskErrorStorageUpload = regexp.MustCompile(`(?i)(?:参考(?:图片|媒体)上传失败|OSS 上传失败|对象存储|腾讯云 COS|七牛云)`)
)
// userFacingTaskError 把任务里保存的原始错误转换成用户能看懂的中文原因。
func userFacingTaskError(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return taskErrorDefaultMessage
}
if taskErrorIsModeration(raw) {
return taskErrorModerationText(raw)
}
providerMessage := taskErrorStructuredProviderMessage(raw)
if providerMessage == "" {
providerMessage = taskErrorWrappedProviderMessage(raw)
}
display := raw
if providerMessage != "" {
display = providerMessage
}
if taskErrorIsModeration(display) {
return taskErrorModerationText(display)
}
if message := taskErrorStorageMessage(raw); message != "" {
return message
}
if message := taskErrorStorageMessage(display); message != "" {
return message
}
if taskErrorNetworkPattern.MatchString(display) {
return taskErrorNetworkMessage
}
if providerMessage == "" {
if !strings.Contains(display, ";上游:") {
switch {
case taskErrorHasStatus(raw, "429"):
return "服务当前繁忙,请稍后重试。"
case taskErrorHasStatus(raw, "401", "403"):
return "生成服务鉴权失败,请检查渠道配置。"
case taskErrorHasStatus(raw, "404"):
return "生成服务地址不可用,请检查渠道配置。"
case taskErrorHasStatus(raw, "500", "502", "503", "504"):
return taskErrorNetworkMessage
}
}
if taskErrorInfrastructurePattern.MatchString(raw) {
return taskErrorNetworkMessage
}
}
return truncateRunes(display, 240)
}
// userFacingTaskFailure 给任务失败加上"哪一步失败"的前缀,例如"图片生成失败:网络异常。"
func userFacingTaskFailure(task *model.Task) string {
if task == nil {
return "任务失败:" + taskErrorDefaultMessage
}
reason := userFacingTaskError(task.Error)
if task.Status == model.TaskStatusCancelled && strings.TrimSpace(task.Error) == "" {
reason = "任务已取消"
}
return userFacingTaskKind(task) + "失败:" + reason
}
func userFacingTaskKind(task *model.Task) string {
switch {
case task.Operation == cloudAgentStepOperation || task.Operation == cloudAgentOperation || task.Operation == cloudAgentContextCompactionOperation:
return "模型调用"
case task.Type == "canvas_image" || strings.Contains(task.Type, "image"):
return "图片生成"
case task.Type == "canvas_video" || strings.HasPrefix(task.Type, "video_") || strings.Contains(task.Type, "video"):
return "视频生成"
case strings.Contains(task.Type, "audio"):
return "音频生成"
case task.Type == "canvas_text":
return "文本生成"
}
return "任务"
}
// cloudAgentUserFailureMessage 决定 Agent 运行失败时展示给用户的文字。
// 已经是中文、可读的原因(业务错误、任务失败原因)原样展示;内部英文诊断只写日志,
// 用户看到的是可操作的中文提示和诊断号,不会看到 Go/Node 的原始报错。
func cloudAgentUserFailureMessage(runID string, cause error) string {
if cause == nil {
return "Agent 执行中断,请重新发送消息"
}
var appErr *AppError
if errors.As(cause, &appErr) && appErr != nil && cloudAgentReadableMessage(appErr.Message) {
return strings.TrimSpace(appErr.Message)
}
if message := cloudAgentInnermostMessage(cause); cloudAgentReadableMessage(message) {
return message
}
log.Printf("[Agent] internal failure run=%s: %v", runID, cause)
return "Agent 执行中断,请重新发送消息;如反复出现,请把诊断号 " + runID + " 反馈给管理员"
}
// cloudAgentInnermostMessage 沿包装链找到最内层的错误文字。包装前缀("load session: "
// 之类)是给日志看的内部上下文,不展示给用户。
func cloudAgentInnermostMessage(err error) string {
for err != nil {
next := errors.Unwrap(err)
if next == nil {
return strings.TrimSpace(err.Error())
}
err = next
}
return ""
}
// cloudAgentReadableMessage 只放行"以中文为主、不含地址/堆栈/凭证"的文字。
func cloudAgentReadableMessage(message string) bool {
message = strings.TrimSpace(message)
if message == "" || !utf8.ValidString(message) || strings.ContainsAny(message, "\r\n\x00") {
return false
}
han, letters := 0, 0
for _, r := range message {
switch {
case unicode.Is(unicode.Han, r):
han++
case r < unicode.MaxASCII && unicode.IsLetter(r):
letters++
}
}
if han == 0 || letters > han*2 {
return false
}
return safeTaskDiagnosticMessage(message) == truncateRunes(message, 240) && !taskErrorInfrastructurePattern.MatchString(message) && !taskErrorNetworkPattern.MatchString(message)
}
func taskErrorIsModeration(value string) bool {
return strings.Contains(strings.ToLower(value), "sensitive_words_detected") || strings.Contains(value, "内容审核未通过")
}
func taskErrorModerationText(raw string) string {
if index := strings.Index(raw, ";上游:"); index >= 0 && !taskErrorInfrastructurePattern.MatchString(raw) {
return taskErrorModerationMessage + raw[index:]
}
return taskErrorModerationMessage
}
func taskErrorStorageMessage(value string) string {
switch {
case value == "":
return ""
case taskErrorStorageDisabled.MatchString(value):
return "对象存储账号已停用,请检查或更换对象存储配置。"
case taskErrorStorageUpload.MatchString(value):
return "参考素材上传到对象存储失败,请检查对象存储配置后重试。"
}
return ""
}
func taskErrorHasStatus(value string, statuses ...string) bool {
for _, status := range statuses {
if regexp.MustCompile(`\b` + status + `\b`).MatchString(value) {
return true
}
}
return false
}
func taskErrorStructuredProviderMessage(raw string) string {
for index := strings.Index(raw, "{"); index >= 0; {
var payload any
if json.Unmarshal([]byte(strings.TrimSpace(raw[index:])), &payload) == nil {
if message := taskErrorPayloadMessage(payload); message != "" {
return message
}
}
next := strings.Index(raw[index+1:], "{")
if next < 0 {
break
}
index += next + 1
}
return ""
}
func taskErrorWrappedProviderMessage(raw string) string {
var wrapped string
if match := taskErrorWrappedInterface.FindStringSubmatch(raw); match != nil {
wrapped = match[1]
} else if match := taskErrorWrappedRequest.FindStringSubmatch(raw); match != nil {
wrapped = match[1]
} else {
return ""
}
message := strings.TrimSpace(taskErrorWrappedStatusPrefix.ReplaceAllString(wrapped, ""))
if message == "" || taskErrorInfrastructurePattern.MatchString(message) {
return ""
}
return message
}
func taskErrorPayloadMessage(payload any) string {
switch value := payload.(type) {
case string:
return strings.TrimSpace(value)
case map[string]any:
if nested, ok := value["error"].(map[string]any); ok {
if message := taskErrorPayloadMessage(nested); message != "" {
return message
}
}
for _, key := range []string{"message", "msg", "detail"} {
if text, ok := value[key].(string); ok && strings.TrimSpace(text) != "" {
return strings.TrimSpace(text)
}
}
if text, ok := value["error"].(string); ok {
return strings.TrimSpace(text)
}
}
return ""
}