refactor(agent): Pi 会话运行时 - 接入原生 Agent 与 Skills 工具链

This commit is contained in:
ddcat666
2026-09-29 08:21:11 +08:00
parent f9b8c5a43e
commit 387d3562ad
69 changed files with 11460 additions and 207 deletions

View File

@@ -0,0 +1,249 @@
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 ""
}