250 lines
8.9 KiB
Go
250 lines
8.9 KiB
Go
package app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"log"
|
||
"regexp"
|
||
"strings"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
|
||
"infinite-canvas/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 ""
|
||
}
|