106 lines
3.7 KiB
Go
106 lines
3.7 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
)
|
|
|
|
func taskHasAgentOrigin(raw string) bool {
|
|
var input struct {
|
|
Metadata struct {
|
|
Source string `json:"source"`
|
|
} `json:"metadata"`
|
|
}
|
|
return json.Unmarshal([]byte(raw), &input) == nil && input.Metadata.Source == "cloud_agent"
|
|
}
|
|
|
|
func safeTaskDiagnosticMessage(message string) string {
|
|
message = strings.TrimSpace(message)
|
|
if message == "" || !utf8.ValidString(message) || strings.ContainsAny(message, "\r\n\x00") {
|
|
return "任务未完成,请在任务中心查看诊断"
|
|
}
|
|
lower := strings.ToLower(message)
|
|
for _, marker := range []string{"http://", "https://", "file://", "ftp://", "authorization", "cookie", "secret", "token", "api_key", "apikey", "x-api-key", "/var/", "/tmp/", "\\", "stack trace", "traceback", "sql:", "sqlite", "postgres"} {
|
|
if strings.Contains(lower, marker) {
|
|
return "任务未完成,请在任务中心查看诊断"
|
|
}
|
|
}
|
|
return truncateRunes(message, 240)
|
|
}
|
|
|
|
func recordTaskDiagnostic(task *model.Task, phase, code, outcome, retryClass string, cause error) {
|
|
var appErr *AppError
|
|
if errors.As(cause, &appErr) && appErr.Reason != "" {
|
|
code = string(appErr.Reason)
|
|
}
|
|
diagnostic := &model.TaskExecutionDiagnostic{
|
|
Code: code, Phase: phase, TaskSubmitted: outcome == "accepted" || outcome == "submitted", SubmissionOutcome: outcome,
|
|
RetryClass: retryClass, SafeMessage: safeTaskDiagnosticMessage(task.Error), DiagnosticID: task.ID,
|
|
}
|
|
raw, _ := json.Marshal(diagnostic)
|
|
task.ExecutionDiagnosticJSON, task.Diagnostic = string(raw), diagnostic
|
|
}
|
|
|
|
func recordTaskWritebackDiagnostic(task *model.Task, nodeID, outcome, reason string, cause error) {
|
|
if task == nil {
|
|
return
|
|
}
|
|
diagnostic := taskExecutionDiagnostic(task)
|
|
if diagnostic == nil {
|
|
diagnostic = &model.TaskExecutionDiagnostic{
|
|
Code: "task_terminal",
|
|
Phase: "completion",
|
|
TaskSubmitted: true,
|
|
SubmissionOutcome: cloudAgentTaskSubmissionOutcome(task),
|
|
RetryClass: "none",
|
|
DiagnosticID: task.ID,
|
|
}
|
|
if task.Status == model.TaskStatusFailed || task.Status == model.TaskStatusCancelled {
|
|
diagnostic.SafeMessage = cloudAgentSafeMediaTaskError(task)
|
|
}
|
|
}
|
|
diagnostic.WritebackOutcome = outcome
|
|
diagnostic.WritebackReason = strings.TrimSpace(reason)
|
|
diagnostic.WritebackNodeID = strings.TrimSpace(nodeID)
|
|
diagnostic.WritebackMessage = ""
|
|
if cause != nil {
|
|
diagnostic.WritebackMessage = safeTaskDiagnosticMessage(cloudAgentSafeToolError(cause))
|
|
}
|
|
raw, _ := json.Marshal(diagnostic)
|
|
task.ExecutionDiagnosticJSON, task.Diagnostic = string(raw), diagnostic
|
|
}
|
|
|
|
// taskExecutionDiagnostic exposes stored execution facts; it never derives a
|
|
// failure category from a node's presentation state or the model's explanation.
|
|
func taskExecutionDiagnostic(task *model.Task) *model.TaskExecutionDiagnostic {
|
|
if task == nil || task.ExecutionDiagnosticJSON == "" {
|
|
return nil
|
|
}
|
|
var diagnostic model.TaskExecutionDiagnostic
|
|
if json.Unmarshal([]byte(task.ExecutionDiagnosticJSON), &diagnostic) != nil {
|
|
return nil
|
|
}
|
|
// Execution success and writeback failure are separate facts. A successful
|
|
// task has no execution error; writeback errors remain in WritebackMessage.
|
|
if task.Status == model.TaskStatusSucceeded {
|
|
diagnostic.SafeMessage = ""
|
|
} else if diagnostic.SafeMessage != "" || task.Status == model.TaskStatusFailed || task.Status == model.TaskStatusCancelled {
|
|
diagnostic.SafeMessage = safeTaskDiagnosticMessage(diagnostic.SafeMessage)
|
|
}
|
|
return &diagnostic
|
|
}
|
|
|
|
func taskSubmissionOutcome(task *model.Task, notSent bool) string {
|
|
if notSent {
|
|
return "not_submitted"
|
|
}
|
|
if task.ProviderRequestID != "" {
|
|
return "accepted"
|
|
}
|
|
return "unknown"
|
|
}
|