Truncate execution.log patches and keep members aligned with patch bodies so verbose MiniMax-M3 runs no longer exceed the Go 8MB fail-closed limit. Surface body-too-large vs typed-reject reasons and map contract failures to fatal restart instead of misleading resumable TASK_INTERRUPTED.
153 lines
3.9 KiB
Go
153 lines
3.9 KiB
Go
package stream
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// ClassifyError maps a raw error into structured ErrorInfo using heuristic rules.
|
|
func ClassifyError(err error, phaseID, requestID, sessionID, workspaceID string) ErrorInfo {
|
|
if err == nil {
|
|
err = errors.New("unknown error")
|
|
}
|
|
|
|
msg := strings.ToLower(err.Error())
|
|
|
|
info := ErrorInfo{
|
|
PhaseID: phaseID,
|
|
RequestID: requestID,
|
|
SessionID: sessionID,
|
|
WorkspaceID: workspaceID,
|
|
}
|
|
|
|
raw := err.Error()
|
|
if !containsInternalLeak(raw) {
|
|
info.Upstream = truncateRunes(raw, 80)
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(msg, "编辑中") || strings.Contains(msg, "禁止并行修改") || strings.Contains(msg, "edit lease"):
|
|
info.Code = "DOC_BUSY"
|
|
info.Class = ErrorClassFatal
|
|
info.UserMessage = "该文档正在由项目助手或其他用户编辑中;当前请求未执行,请等待现有任务完成"
|
|
info.Actions = []string{}
|
|
|
|
case isDocumentLedgerError(msg):
|
|
info.Code = "DOC_LEDGER_ERROR"
|
|
info.Class = ErrorClassFatal
|
|
info.UserMessage = "文档职责账本写入失败,请重新开始会话;若反复出现请联系管理员"
|
|
info.Actions = []string{"restart"}
|
|
|
|
case isTimeoutError(msg):
|
|
info.Code = "CHAT_TIMEOUT"
|
|
info.Class = ErrorClassTransient
|
|
info.UserMessage = "生成回复超时,请稍后重试"
|
|
info.Actions = []string{"retry"}
|
|
|
|
case isNetworkError(err, msg):
|
|
info.Code = "NETWORK_ERROR"
|
|
info.Class = ErrorClassTransient
|
|
info.UserMessage = "网络连接异常,请稍后重试"
|
|
info.Actions = []string{"retry"}
|
|
|
|
case isRunnerContractError(msg):
|
|
// Permanent runner wire/contract failures must not look "resumable".
|
|
info.Code = "RUNNER_RESULT_REJECTED"
|
|
info.Class = ErrorClassFatal
|
|
info.UserMessage = "助手执行结果校验失败,请重新开始任务"
|
|
info.Actions = []string{"restart"}
|
|
|
|
case workspaceID != "":
|
|
info.Code = "TASK_INTERRUPTED"
|
|
info.Class = ErrorClassResumable
|
|
info.UserMessage = "任务执行中断,可继续或重新开始"
|
|
info.Actions = []string{"continue", "restart"}
|
|
|
|
default:
|
|
info.Code = "INTERNAL_ERROR"
|
|
info.Class = ErrorClassFatal
|
|
info.UserMessage = "系统内部错误,请重新开始会话"
|
|
info.Actions = []string{"restart"}
|
|
}
|
|
|
|
info.Message = info.UserMessage
|
|
return info
|
|
}
|
|
|
|
func containsInternalLeak(s string) bool {
|
|
low := strings.ToLower(s)
|
|
for _, n := range []string{"sql", "pq:", "token", "password", "/home/", "prompt"} {
|
|
if strings.Contains(low, n) || strings.Contains(s, n) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isDocumentLedgerError(msg string) bool {
|
|
return strings.Contains(msg, "persist document ledger") ||
|
|
strings.Contains(msg, "sqlstate 23503") ||
|
|
strings.Contains(msg, "foreign key constraint") ||
|
|
strings.Contains(msg, "document_records_reviewer_id_fkey") ||
|
|
strings.Contains(msg, "document_records_author_id_fkey") ||
|
|
strings.Contains(msg, "document_records_approver_id_fkey")
|
|
}
|
|
|
|
func isRunnerContractError(msg string) bool {
|
|
return strings.Contains(msg, "typed result rejected") ||
|
|
strings.Contains(msg, "runner result body too large")
|
|
}
|
|
|
|
func isTimeoutError(msg string) bool {
|
|
return strings.Contains(msg, "timeout") ||
|
|
strings.Contains(msg, "deadline") ||
|
|
strings.Contains(msg, "timed out")
|
|
}
|
|
|
|
func isNetworkError(err error, msg string) bool {
|
|
var netErr net.Error
|
|
if errors.As(err, &netErr) {
|
|
if netErr.Timeout() || netErr.Temporary() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
var opErr *net.OpError
|
|
if errors.As(err, &opErr) {
|
|
if opErr.Op == "dial" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
networkHints := []string{
|
|
"connection refused",
|
|
"eof",
|
|
"broken pipe",
|
|
}
|
|
for _, hint := range networkHints {
|
|
if strings.Contains(msg, hint) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func truncateRunes(s string, maxRunes int) string {
|
|
if maxRunes <= 0 || s == "" {
|
|
return s
|
|
}
|
|
if utf8.RuneCountInString(s) <= maxRunes {
|
|
return s
|
|
}
|
|
runes := []rune(s)
|
|
return string(runes[:maxRunes])
|
|
}
|