Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
299 lines
7.8 KiB
Go
299 lines
7.8 KiB
Go
package repo
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"git.gozeros.com/wsk/eightbf-analysis-go/internal/domain"
|
|
)
|
|
|
|
// JobRepo 会商作业表读写(eba_analysis_job)。大字段整段 JSON 文本存。
|
|
type JobRepo struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func NewJobRepo(db *sql.DB) *JobRepo {
|
|
return &JobRepo{DB: db}
|
|
}
|
|
|
|
// EnsureSchema 执行建表脚本(幂等;重复索引错误由调用方忽略亦可)。
|
|
func (r *JobRepo) EnsureSchema(schemaSQL string) error {
|
|
_, err := r.DB.Exec(schemaSQL)
|
|
return err
|
|
}
|
|
|
|
func (r *JobRepo) Insert(job *domain.AnalysisJob) error {
|
|
now := time.Now()
|
|
job.CreatedAt = &now
|
|
job.UpdatedAt = &now
|
|
_, err := r.DB.Exec(`INSERT INTO eba_analysis_job(
|
|
id,request_id,task_id,draft_id,draft_key,status,phase,cycle,active_agent_id,
|
|
consensus_percentage,pending_clarification,orchestration_mode,model,error_message,
|
|
owner_account_id,agents_json,chain_json,plan_json,access_json,thread_ids_json,
|
|
progress_json,result_json,cancel_flag,created_at,updated_at
|
|
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,?,?)`,
|
|
job.ID, nullStr(job.RequestID), nullStr(job.TaskID), nullStr(job.DraftID), nullStr(job.DraftKey),
|
|
job.Status, nullStr(job.Phase), job.Cycle, nullStr(job.ActiveAgentID),
|
|
job.ConsensusPercentage, nullStr(job.PendingClarification), nullStr(job.OrchestrationMode),
|
|
nullStr(job.Model), nullStr(job.Error), nullStr(job.OwnerAccountID),
|
|
mustJSON(job.Agents), mustJSON(job.Dialogues), mustJSON(job.Plan), mustJSON(job.Access),
|
|
mustJSON(job.ThreadIDs), mustJSON(job.Progress), mustJSON(job.Result),
|
|
now, now,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// UpdateProgress 全量刷新进度相关列;cancelled 时同步 cancel_flag=1。
|
|
func (r *JobRepo) UpdateProgress(job *domain.AnalysisJob) error {
|
|
now := time.Now()
|
|
job.UpdatedAt = &now
|
|
cancel := 0
|
|
if job.Status == domain.StatusCancelled {
|
|
cancel = 1
|
|
}
|
|
_, err := r.DB.Exec(`UPDATE eba_analysis_job SET status=?, phase=?, cycle=?, active_agent_id=?,
|
|
consensus_percentage=?, pending_clarification=?, model=?, error_message=?, owner_account_id=?,
|
|
agents_json=?, chain_json=?, plan_json=?, access_json=?, thread_ids_json=?,
|
|
progress_json=?, result_json=?, cancel_flag=?, updated_at=? WHERE id=?`,
|
|
job.Status, nullStr(job.Phase), job.Cycle, nullStr(job.ActiveAgentID),
|
|
job.ConsensusPercentage, nullStr(job.PendingClarification), nullStr(job.Model),
|
|
nullStr(job.Error), nullStr(job.OwnerAccountID),
|
|
mustJSON(job.Agents), mustJSON(job.Dialogues), mustJSON(job.Plan), mustJSON(job.Access),
|
|
mustJSON(job.ThreadIDs), mustJSON(job.Progress), mustJSON(job.Result),
|
|
cancel, now, job.ID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *JobRepo) FindByID(id string) (*domain.AnalysisJob, error) {
|
|
rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE id=?`, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
return nil, nil
|
|
}
|
|
return scanJob(rows)
|
|
}
|
|
|
|
func (r *JobRepo) FindActiveByTaskID(taskID string) (*domain.AnalysisJob, error) {
|
|
if taskID == "" {
|
|
return nil, nil
|
|
}
|
|
rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE task_id=? AND status IN (?,?,?)
|
|
ORDER BY created_at DESC LIMIT 1`,
|
|
taskID, domain.StatusQueued, domain.StatusRunning, domain.StatusAwaitingInput)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
return nil, nil
|
|
}
|
|
return scanJob(rows)
|
|
}
|
|
|
|
func (r *JobRepo) FindLatestByTaskID(taskID string) (*domain.AnalysisJob, error) {
|
|
if taskID == "" {
|
|
return nil, nil
|
|
}
|
|
rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE task_id=? ORDER BY created_at DESC LIMIT 1`, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
return nil, nil
|
|
}
|
|
return scanJob(rows)
|
|
}
|
|
|
|
func (r *JobRepo) FindActiveJobs() ([]*domain.AnalysisJob, error) {
|
|
rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE status IN (?,?,?) ORDER BY created_at ASC`,
|
|
domain.StatusQueued, domain.StatusRunning, domain.StatusAwaitingInput)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanJobs(rows)
|
|
}
|
|
|
|
func (r *JobRepo) FindQueuedJobs() ([]*domain.AnalysisJob, error) {
|
|
rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE status=? ORDER BY created_at ASC`, domain.StatusQueued)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanJobs(rows)
|
|
}
|
|
|
|
func (r *JobRepo) MarkCancel(id string) (bool, error) {
|
|
res, err := r.DB.Exec(`UPDATE eba_analysis_job SET cancel_flag=1, updated_at=? WHERE id=? AND status IN (?,?,?)`,
|
|
time.Now(), id, domain.StatusQueued, domain.StatusRunning, domain.StatusAwaitingInput)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
return n > 0, nil
|
|
}
|
|
|
|
func (r *JobRepo) IsCancelFlag(id string) (bool, error) {
|
|
var v sql.NullInt64
|
|
err := r.DB.QueryRow(`SELECT cancel_flag FROM eba_analysis_job WHERE id=?`, id).Scan(&v)
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return v.Valid && v.Int64 == 1, nil
|
|
}
|
|
|
|
func scanJobs(rows *sql.Rows) ([]*domain.AnalysisJob, error) {
|
|
var out []*domain.AnalysisJob
|
|
for rows.Next() {
|
|
j, err := scanJob(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, j)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func scanJob(rows *sql.Rows) (*domain.AnalysisJob, error) {
|
|
cols, err := rows.Columns()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vals := make([]interface{}, len(cols))
|
|
ptrs := make([]interface{}, len(cols))
|
|
for i := range vals {
|
|
ptrs[i] = &vals[i]
|
|
}
|
|
if err := rows.Scan(ptrs...); err != nil {
|
|
return nil, err
|
|
}
|
|
m := map[string]interface{}{}
|
|
for i, c := range cols {
|
|
m[c] = vals[i]
|
|
}
|
|
j := &domain.AnalysisJob{}
|
|
j.ID = asString(m["id"])
|
|
j.RequestID = asString(m["request_id"])
|
|
j.TaskID = asString(m["task_id"])
|
|
j.DraftID = asString(m["draft_id"])
|
|
j.DraftKey = asString(m["draft_key"])
|
|
j.Status = asString(m["status"])
|
|
j.Phase = asString(m["phase"])
|
|
j.Cycle = asInt(m["cycle"])
|
|
j.ActiveAgentID = asString(m["active_agent_id"])
|
|
j.ConsensusPercentage = asInt(m["consensus_percentage"])
|
|
j.PendingClarification = asString(m["pending_clarification"])
|
|
j.OrchestrationMode = asString(m["orchestration_mode"])
|
|
j.Model = asString(m["model"])
|
|
j.Error = asString(m["error_message"])
|
|
j.OwnerAccountID = asString(m["owner_account_id"])
|
|
j.Agents = readList(asString(m["agents_json"]))
|
|
j.Dialogues = readList(asString(m["chain_json"]))
|
|
j.Plan = readMap(asString(m["plan_json"]))
|
|
j.Access = readMap(asString(m["access_json"]))
|
|
j.ThreadIDs = readMap(asString(m["thread_ids_json"]))
|
|
j.Progress = readMap(asString(m["progress_json"]))
|
|
j.Result = readMap(asString(m["result_json"]))
|
|
if t := asTime(m["created_at"]); t != nil {
|
|
j.CreatedAt = t
|
|
}
|
|
if t := asTime(m["updated_at"]); t != nil {
|
|
j.UpdatedAt = t
|
|
}
|
|
return j, nil
|
|
}
|
|
|
|
func mustJSON(v interface{}) interface{} {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func readMap(jsonStr string) map[string]interface{} {
|
|
if jsonStr == "" {
|
|
return nil
|
|
}
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal([]byte(jsonStr), &m); err != nil {
|
|
return nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func readList(jsonStr string) []map[string]interface{} {
|
|
if jsonStr == "" {
|
|
return []map[string]interface{}{}
|
|
}
|
|
var list []map[string]interface{}
|
|
if err := json.Unmarshal([]byte(jsonStr), &list); err != nil {
|
|
return []map[string]interface{}{}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func nullStr(s string) interface{} {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return s
|
|
}
|
|
|
|
func asString(v interface{}) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
switch t := v.(type) {
|
|
case string:
|
|
return t
|
|
case []byte:
|
|
return string(t)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func asInt(v interface{}) int {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
switch t := v.(type) {
|
|
case int64:
|
|
return int(t)
|
|
case int32:
|
|
return int(t)
|
|
case int:
|
|
return t
|
|
case []byte:
|
|
var n int
|
|
_ = json.Unmarshal(t, &n)
|
|
return n
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func asTime(v interface{}) *time.Time {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
if t, ok := v.(time.Time); ok {
|
|
tt := t
|
|
return &tt
|
|
}
|
|
return nil
|
|
}
|