package store import ( "context" "time" ) // 全平台任务观测(管理端「运行观测」页)。系统级口径:跨租户看所有任务, // 故走 WithoutTenant 旁路租户插件 + raw Table 查询(同 RecentRuns 手动控制过滤)。 // AdminTaskRow 是管理端任务流一行:任务 + 租户名 + 提交人邮箱 + 评测分级(LEFT JOIN)。 type AdminTaskRow struct { TaskID string `json:"task_id"` TenantID string `json:"tenant_id"` TenantName string `json:"tenant_name"` Owner string `json:"owner"` OwnerEmail string `json:"owner_email"` Status string `json:"status"` Detail string `json:"detail"` Topic string `json:"topic"` At time.Time `json:"at"` EvalLevel string `json:"eval_level"` EvalOverall float64 `json:"eval_overall"` } // AllTasks 全平台任务流(管理端观测;可按状态/租户过滤)。倒序,翻页。 // status 空=全部;tenantID 空=全租户。 func (p *Postgres) AllTasks(ctx context.Context, status, tenantID string, limit int) []AdminTaskRow { if p.db == nil { return nil } if limit <= 0 || limit > 200 { limit = 50 } ctx = WithoutTenant(ctx) // 系统级:看所有租户的任务 q := p.db.WithContext(ctx).Table("sundynix_task as t"). Select("t.task_id, t.tenant_id, t.owner, t.status, t.detail, t.created_at as at, " + "coalesce(tn.name,'') as tenant_name, coalesce(u.email,'') as owner_email, " + "coalesce(e.level,'') as eval_level, coalesce(e.overall,0) as eval_overall, " + "coalesce(t.graph->>'topic','') as topic"). Joins("left join sundynix_tenant tn on tn.id = t.tenant_id"). Joins("left join sundynix_user u on u.id = t.owner"). Joins("left join sundynix_eval e on e.task_id = t.task_id"). Where("t.deleted_at is null") if status != "" { q = q.Where("t.status = ?", status) } if tenantID != "" { q = q.Where("t.tenant_id = ?", tenantID) } var out []AdminTaskRow q.Order("t.created_at desc").Limit(limit).Scan(&out) return out } // TaskStatusCounts 全平台任务按状态计数(观测页的筛选标签 + 分布)。 func (p *Postgres) TaskStatusCounts(ctx context.Context) map[string]int64 { out := map[string]int64{} if p.db == nil { return out } ctx = WithoutTenant(ctx) var rows []struct { Status string N int64 } p.db.WithContext(ctx).Table("sundynix_task"). Select("status, count(*) as n"). Where("deleted_at is null"). Group("status").Scan(&rows) for _, r := range rows { out[r.Status] = r.N } return out } // AdminTaskDetail 是管理端任务下钻:基本信息 + 持久化的输出/轨迹 + 评测。 // 轨迹与输出取自 sundynix_task 的收尾落库列(Redis 流只有 10min TTL,历史任务只能靠它们)。 type AdminTaskDetail struct { AdminTaskRow Graph string `json:"graph"` // 提交时的 DSL 原文 Output string `json:"output"` // 最终模型输出 Trace string `json:"trace"` // 执行轨迹事件 JSON 数组(原文透传,前端解析) // gorm:"-":这是查完再单独填的组合字段,不是关联;不标记的话 Scan 会当成关系报错。 Eval *AdminEval `gorm:"-" json:"eval"` // 无评测时为 null } // AdminEval 是下钻里的评测明细(比列表页的 level/overall 多出评语与命中项)。 type AdminEval struct { Overall float64 `json:"overall"` Rule float64 `json:"rule"` LLM float64 `json:"llm"` Faithful float64 `json:"faithful"` Level string `json:"level"` Flags string `json:"flags"` Reason string `json:"reason"` Sources int `json:"sources"` Corrected bool `json:"corrected"` } // TaskOwner 按 task_id 返回提交者 user.id(供 SSE/导出等公开 by-id 端点做归属校验)。 // 跨租户查(WithoutTenant):这些端点无租户上下文,靠 owner 判权。不存在返回空串。 // TaskTopic 报告类任务的主题(graph 顶层 topic;普通任务无此字段返回空)。主动播报用。 func (p *Postgres) TaskTopic(ctx context.Context, taskID string) string { if p.db == nil || taskID == "" { return "" } var topic string p.db.WithContext(WithoutTenant(ctx)).Table("sundynix_task"). Where("task_id = ? and deleted_at is null", taskID). Select("coalesce(graph->>'topic','')").Scan(&topic) return topic } func (p *Postgres) TaskOwner(ctx context.Context, taskID string) string { if p.db == nil || taskID == "" { return "" } var owner string p.db.WithContext(WithoutTenant(ctx)).Model(&Task{}). Where("task_id = ? and deleted_at is null", taskID).Select("owner").Scan(&owner) return owner } // TaskDetail 按 task_id 取单条任务的完整下钻数据(管理端,跨租户)。 // 必须 WithoutTenant:Task/Eval 都在租户插件作用域内,用请求 ctx 查别的租户的任务 // 不会报错,而是静默返回空——看起来像"这任务没产出",比报错更难排查。 func (p *Postgres) TaskDetail(ctx context.Context, taskID string) *AdminTaskDetail { if p.db == nil || taskID == "" { return nil } ctx = WithoutTenant(ctx) var d AdminTaskDetail err := p.db.WithContext(ctx).Table("sundynix_task as t"). Select("t.task_id, t.tenant_id, t.owner, t.status, t.detail, t.created_at as at, "+ "t.output, t.trace, coalesce(cast(t.graph as text),'') as graph, "+ "coalesce(tn.name,'') as tenant_name, coalesce(u.email,'') as owner_email, "+ "coalesce(e.level,'') as eval_level, coalesce(e.overall,0) as eval_overall, "+ "coalesce(t.graph->>'topic','') as topic"). Joins("left join sundynix_tenant tn on tn.id = t.tenant_id"). Joins("left join sundynix_user u on u.id = t.owner"). Joins("left join sundynix_eval e on e.task_id = t.task_id"). Where("t.task_id = ? and t.deleted_at is null", taskID). Scan(&d).Error if err != nil || d.TaskID == "" { return nil } var e Eval if p.db.WithContext(ctx).Where("task_id = ?", taskID).First(&e).Error == nil { d.Eval = &AdminEval{ Overall: e.Overall, Rule: e.Rule, LLM: e.LLM, Faithful: e.Faithful, Level: e.Level, Flags: e.Flags, Reason: e.Reason, Sources: e.Sources, Corrected: e.Corrected, } } return &d }