Files
knowledge-governance/internal/qaopsai/status.go
T
姚章浩 533db535f1
build / build (push) Successful in 1m12s
feat(qaops): 接入真实模型补缺任务与总表看板
按已确认需求提供独立安全模型配置、真实探测、正式字典字段评估与原问题译中,持久批次支持只补空白、暂停恢复、有限重试、幂等和逐字段依据。覆盖固定规则、本地语言、组织关联与主表有效看板共用治理侧读模型,保留源生产和冻结历史。

验证:完整 make verify 退出0,702项Go与329项前端通过;默认118项外部/隔离集成明确跳过,另跑完整镜像及组织隔离PG竞态。12并发、输入漂移、过期租约、暂停/恢复及有限重试通过。用户授权的百炼 deepseek-v4.1-flash 完成真实后台与浏览器评估/翻译;552行、16列、11块看板及完整PNG、独立配置页测试/保存、重开恢复和零JS异常通过。仅非业务样本外发,自动处理关闭,原payload不变。

上线前治理库已做导出快照原生备份并真实恢复,60表行数与17个QA表内容哈希一致;不修改DobotAI生产或隧道。本提交时尚未部署,线上验收须独立完成。
2026-10-19 14:29:49 +08:00

141 lines
5.5 KiB
Go

package qaopsai
import (
"context"
"encoding/json"
"time"
)
type WorkState struct {
ID string `json:"id"`
RecordID string `json:"record_id"`
Kind string `json:"kind"`
State string `json:"state"`
Stage string `json:"stage"`
Attempt int `json:"attempt"`
ErrorCode string `json:"error_code"`
SkipReason string `json:"skip_reason"`
StartedAt *time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type QueueCount struct {
Kind string `json:"kind"`
Queued int `json:"queued"`
Running int `json:"running"`
Retry int `json:"retry"`
Failed int `json:"failed"`
Paused int `json:"paused"`
Succeeded int `json:"succeeded"`
Skipped int `json:"skipped"`
}
type Status struct {
Settings Settings `json:"settings"`
WorkerReady bool `json:"worker_ready"`
Queues []QueueCount `json:"queues"`
Batches []Batch `json:"batches"`
RecentTasks []WorkState `json:"recent_tasks"`
RecentTasksLimited bool `json:"recent_tasks_limited"`
}
func (s *Store) GetStatus(ctx context.Context) (Status, error) {
out := Status{Queues: []QueueCount{}, Batches: []Batch{}, RecentTasks: []WorkState{}}
cfg, err := s.GetSettings(ctx)
if err != nil {
return out, err
}
out.Settings = cfg
if s.pool.QueryRow(ctx, `SELECT COALESCE((SELECT heartbeat_at>clock_timestamp()-interval '30 seconds' OR lease_until>clock_timestamp() FROM qa_ops_ai_runtime WHERE tenant_key=$1),false)`, s.tenant).Scan(&out.WorkerReady) != nil {
return out, ErrUnavailable
}
for _, kind := range []string{"evaluation", "translation"} {
q := QueueCount{Kind: kind}
if s.pool.QueryRow(ctx, `SELECT count(*)FILTER(WHERE state='queued'),count(*)FILTER(WHERE state='running'),count(*)FILTER(WHERE state='retry'),count(*)FILTER(WHERE state='failed'),count(*)FILTER(WHERE state='paused'),count(*)FILTER(WHERE state='succeeded'),count(*)FILTER(WHERE state='skipped') FROM qa_ops_ai_work WHERE tenant_key=$1 AND kind=$2`, s.tenant, kind).Scan(&q.Queued, &q.Running, &q.Retry, &q.Failed, &q.Paused, &q.Succeeded, &q.Skipped) != nil {
return out, ErrUnavailable
}
out.Queues = append(out.Queues, q)
}
rows, err := s.pool.Query(ctx, `SELECT id FROM qa_ops_ai_batches WHERE tenant_key=$1 ORDER BY created_at DESC,id DESC LIMIT 10`, s.tenant)
if err != nil {
return out, ErrUnavailable
}
ids := []string{}
for rows.Next() {
var id string
if rows.Scan(&id) != nil {
rows.Close()
return out, ErrUnavailable
}
ids = append(ids, id)
}
err = rows.Err()
rows.Close()
if err != nil {
return out, ErrUnavailable
}
for _, id := range ids {
batch, e := s.GetBatch(ctx, id)
if e != nil {
return out, e
}
out.Batches = append(out.Batches, batch)
}
rows, err = s.pool.Query(ctx, `SELECT id,record_id,kind,state,stage,attempt,error_code,skip_reason,started_at,updated_at FROM qa_ops_ai_work WHERE tenant_key=$1 ORDER BY updated_at DESC,id DESC LIMIT 1001`, s.tenant)
if err != nil {
return out, ErrUnavailable
}
for rows.Next() {
var work WorkState
if rows.Scan(&work.ID, &work.RecordID, &work.Kind, &work.State, &work.Stage, &work.Attempt, &work.ErrorCode, &work.SkipReason, &work.StartedAt, &work.UpdatedAt) != nil {
rows.Close()
return out, ErrUnavailable
}
out.RecentTasks = append(out.RecentTasks, work)
}
err = rows.Err()
rows.Close()
if err != nil {
return out, ErrUnavailable
}
if len(out.RecentTasks) > 1000 {
out.RecentTasks = out.RecentTasks[:1000]
out.RecentTasksLimited = true
}
return out, nil
}
type WorkEvidence struct {
WorkState
InputHash string `json:"input_hash"`
ConfigRevision int64 `json:"config_revision"`
PromptVersion string `json:"prompt_version"`
DictionaryVersion string `json:"dictionary_version"`
ActualModel string `json:"actual_model"`
Result json.RawMessage `json:"result"`
AppliedFields map[string]string `json:"applied_fields"`
SkippedFields map[string]string `json:"skipped_fields"`
}
func (s *Store) RecordEvidence(ctx context.Context, recordID string) ([]WorkEvidence, error) {
if !s.valid() {
return nil, ErrUnavailable
}
rows, err := s.pool.Query(ctx, `SELECT w.id,w.record_id,w.kind,w.state,w.stage,w.attempt,w.error_code,w.skip_reason,w.started_at,w.updated_at,w.input_hash,w.config_revision,w.prompt_version,w.dictionary_version,COALESCE(o.actual_model,''),COALESCE(o.payload,'{}'::jsonb),COALESCE(o.applied_fields,'{}'::jsonb),COALESCE(o.skipped_fields,'{}'::jsonb) FROM qa_ops_ai_work w LEFT JOIN qa_ops_ai_outputs o ON o.tenant_key=w.tenant_key AND o.work_id=w.id WHERE w.tenant_key=$1 AND w.record_id=$2 ORDER BY w.created_at DESC,w.id DESC LIMIT 50`, s.tenant, recordID)
if err != nil {
return nil, ErrUnavailable
}
defer rows.Close()
out := []WorkEvidence{}
for rows.Next() {
var work WorkEvidence
var applied, skipped []byte
if rows.Scan(&work.ID, &work.RecordID, &work.Kind, &work.State, &work.Stage, &work.Attempt, &work.ErrorCode, &work.SkipReason, &work.StartedAt, &work.UpdatedAt, &work.InputHash, &work.ConfigRevision, &work.PromptVersion, &work.DictionaryVersion, &work.ActualModel, &work.Result, &applied, &skipped) != nil || json.Unmarshal(applied, &work.AppliedFields) != nil || json.Unmarshal(skipped, &work.SkippedFields) != nil {
return nil, ErrUnavailable
}
out = append(out, work)
}
if rows.Err() != nil {
return nil, ErrUnavailable
}
return out, nil
}