package postgres import ( "context" "encoding/json" "errors" ) type MonitorTaskFilter struct { Search, Source, Scope, Action, Status, Stage, ID, Target, Since, Until string Limit, Offset int } type MonitorTaskEvent struct { At string `json:"at"` Kind string `json:"kind"` Detail string `json:"detail"` } type MonitorTask struct { History []MonitorTaskEvent `json:"history"` ID string `json:"id"` Title string `json:"title"` Path string `json:"path"` SourceID string `json:"source_id"` SourceName string `json:"source_name"` TargetScope string `json:"target_scope"` TargetKB string `json:"target_kb"` Action string `json:"action"` Stage string `json:"stage"` Status string `json:"status"` Detail string `json:"detail"` UpdatedAt string `json:"updated_at"` RetryKind string `json:"retry_kind"` FailureID string `json:"failure_id"` RetryCount int `json:"retry_count"` VersionID string `json:"version_id"` } type MonitorTaskPage struct { Items []MonitorTask `json:"items"` Total int `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` Sources []MonitorOption `json:"sources"` Targets []MonitorOption `json:"targets"` Retryable int `json:"retryable"` } type MonitorOption struct { ID string `json:"id"` Name string `json:"name"` } // Reduce publication versions before filtering; totals and pages share one snapshot. const monitorTasksCTE = `WITH history_publications AS ( SELECT hp.* FROM test_publications hp JOIN source_item_versions hv ON hv.id=hp.source_item_version_id WHERE $7<>'' AND (hv.source_item_id,hp.test_kb_id)=(SELECT v.source_item_id,p.test_kb_id FROM test_publications p JOIN source_item_versions v ON v.id=p.source_item_version_id WHERE p.id=$7) ), history_events AS ( SELECT updated_at AS at,'版本状态' AS kind,source_item_version_id || ' · ' || processing_state || ' / ' || retrieval_state AS detail FROM history_publications UNION ALL SELECT f.created_at,'失败记录',f.job_type || ' · ' || f.error_code || ' · 尝试 ' || f.attempt FROM job_failures f LEFT JOIN jobs fj ON fj.id=f.job_id WHERE f.publication_id IN(SELECT id FROM history_publications) OR (f.job_type='artifact.fetch' AND COALESCE(NULLIF(f.business_version_id,''),fj.payload->>'version_id') IN(SELECT source_item_version_id FROM history_publications)) UNION ALL SELECT r.created_at,'重试记录',r.reason || ' · ' || r.outcome FROM job_retry_operations r WHERE r.publication_id IN(SELECT id FROM history_publications) ), latest AS ( SELECT DISTINCT ON(v.source_item_id,p.test_kb_id) p.*,v.source_item_id,v.content_object_id FROM test_publications p JOIN source_item_versions v ON v.id=p.source_item_version_id WHERE p.superseded_at IS NULL ORDER BY v.source_item_id,p.test_kb_id,p.created_at DESC,p.id DESC ), base AS ( SELECT p.id,si.title,si.normalized_path AS path,c.id AS source_id,c.name AS source_name, p.target_scope,p.test_kb_id AS target_kb,p.source_item_version_id AS version_id,p.retry_count, GREATEST(p.updated_at,j.updated_at,f.created_at) AS updated_at, p.processing_state,p.ingest_state,p.retrieval_state,p.last_error,p.knowledge_id, co.content_state,co.local_spool_ref,si.policy_state, CASE WHEN p.retrieval_state IN ('disabling','disabled') OR (p.knowledge_id<>'' AND si.policy_state='blocked') OR j.type='test.remove' THEN 'remove' WHEN EXISTS(SELECT 1 FROM test_publications prev JOIN source_item_versions pv ON pv.id=prev.source_item_version_id WHERE pv.source_item_id=si.id AND prev.test_kb_id=p.test_kb_id AND prev.id<>p.id AND prev.created_at>'publication_id'=p.id OR (j.type='artifact.fetch' AND j.payload->>'version_id'=p.source_item_version_id)) AND j.type IN ('artifact.fetch','test.upload','test.poll','test.remove') ORDER BY CASE WHEN j.state IN ('pending','running','retry') THEN 0 ELSE 1 END,j.updated_at DESC,j.id DESC LIMIT 1) j ON TRUE LEFT JOIN LATERAL(SELECT f.id,f.job_type,f.error_code,f.last_error,f.created_at FROM job_failures f LEFT JOIN jobs fj ON fj.id=f.job_id WHERE f.state='open' AND (f.publication_id=p.id OR (f.job_type='artifact.fetch' AND COALESCE(NULLIF(f.business_version_id,''),fj.payload->>'version_id')=p.source_item_version_id)) ORDER BY f.created_at DESC,f.id DESC LIMIT 1) f ON TRUE ), classified AS ( SELECT *, CASE WHEN action='remove' AND retrieval_state='disabled' THEN 'completed' WHEN action<>'remove' AND processing_state='completed' THEN 'completed' WHEN job_state IN ('pending','retry') AND job_attempt>0 THEN 'retrying' WHEN job_state='running' THEN 'running' WHEN job_state IN ('pending','retry') THEN 'waiting' WHEN job_state='failed' OR failure_id<>'' OR processing_state IN ('failed','stalled') OR ingest_state='failed' OR retrieval_state='failed' THEN 'attention' WHEN processing_state='cancelled' THEN 'cancelled' WHEN action='remove' THEN 'waiting' WHEN processing_state IN ('processing','finalizing') THEN 'running' ELSE 'waiting' END AS status, CASE WHEN action='remove' AND retrieval_state='disabled' THEN 'finished' WHEN action='remove' THEN 'removal' WHEN processing_state IN ('completed','cancelled') THEN 'finished' WHEN job_type='artifact.fetch' AND job_state IN ('pending','running','retry') THEN 'fetch' WHEN processing_state='finalizing' THEN 'indexing' WHEN processing_state='processing' OR job_type='test.poll' THEN 'parsing' WHEN ingest_state='uploading' OR job_type='test.upload' THEN 'upload' WHEN failure_type='artifact.fetch' OR COALESCE(content_state,'')<>'verified' THEN 'fetch' ELSE 'waiting' END AS stage FROM base ), projected AS ( SELECT id,title,path,source_id,source_name,target_scope,target_kb,version_id,retry_count,updated_at,action,stage,status,failure_id, CASE WHEN status<>'attention' THEN '' WHEN action='remove' THEN CASE WHEN job_type='test.remove' AND job_state='failed' AND retrieval_state IN ('failed','disabling') AND policy_state='blocked' THEN 'remove' ELSE '' END WHEN failure_type='artifact.fetch' THEN 'fetch' WHEN content_state='verified' AND local_spool_ref<>'' AND processing_state IN ('failed','stalled','pending') THEN 'upload' ELSE '' END AS retry_kind, CASE WHEN status='completed' THEN CASE WHEN action='remove' THEN '已从测试库移除;来源文件保留' ELSE '测试库处理完成;未代表已发布生产' END WHEN status='cancelled' THEN '处理已取消,记录保留' WHEN status='retrying' THEN '等待自动重试;下次调度 ' || COALESCE(to_char(available_at AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD HH24:MI:SS'),'未提供') WHEN status='attention' THEN COALESCE(NULLIF(last_error,''),NULLIF(failure_error,''),'处理异常,请查看详情') WHEN stage='removal' THEN CASE WHEN status='running' THEN '正在移除测试库知识' ELSE '等待测试库移除任务' END WHEN stage='fetch' THEN '等待或正在获取源文件' WHEN stage='waiting' THEN '文件已就绪,等待后台处理调度' WHEN stage='upload' THEN '等待或正在上传测试知识库' WHEN stage='indexing' THEN '正在索引与增强' ELSE '正在解析处理' END AS detail FROM classified ), filtered AS ( SELECT * FROM projected WHERE ($1='' OR strpos(lower(title || ' ' || path),lower($1))>0) AND ($2='' OR source_id=$2) AND ($3='' OR target_scope=$3) AND ($4='' OR action=$4) AND ($5='' OR ($5='active' AND status NOT IN ('completed','cancelled')) OR status=$5) AND ($6='' OR stage=$6) AND ($7='' OR id=$7) AND ($10='' OR target_kb=$10) AND ($11='' OR updated_at>=NULLIF($11,'')::timestamptz) AND ($12='' OR updated_at<=NULLIF($12,'')::timestamptz) ) ` func (s *Store) ListMonitorTasks(ctx context.Context, f MonitorTaskFilter) (MonitorTaskPage, error) { if f.Limit < 1 || f.Limit > 100 || f.Offset < 0 { return MonitorTaskPage{}, errors.New("invalid monitor pagination") } var raw []byte err := s.db.QueryRow(ctx, monitorTasksCTE+`SELECT jsonb_build_object('items',COALESCE((SELECT jsonb_agg(to_jsonb(x)||jsonb_build_object('history',COALESCE((SELECT jsonb_agg(e ORDER BY e.at DESC,e.kind,e.detail) FROM (SELECT * FROM history_events ORDER BY at DESC,kind,detail LIMIT 50)e),'[]'::jsonb)) ORDER BY x.updated_at DESC,x.id) FROM (SELECT * FROM filtered ORDER BY updated_at DESC,id LIMIT $8 OFFSET $9)x),'[]'::jsonb),'total',(SELECT count(*) FROM filtered),'limit',$8::int,'offset',$9::int,'retryable',(SELECT count(*) FROM filtered WHERE retry_kind<>''),'sources',COALESCE((SELECT jsonb_agg(x ORDER BY x.name) FROM (SELECT DISTINCT source_id AS id,source_name AS name FROM projected)x),'[]'::jsonb),'targets',COALESCE((SELECT jsonb_agg(x ORDER BY x.name) FROM (SELECT DISTINCT target_kb AS id,target_kb AS name FROM projected)x),'[]'::jsonb))`, f.Search, f.Source, f.Scope, f.Action, f.Status, f.Stage, f.ID, f.Limit, f.Offset, f.Target, f.Since, f.Until).Scan(&raw) if err != nil { return MonitorTaskPage{}, err } var result MonitorTaskPage err = json.Unmarshal(raw, &result) return result, err }