Files
gozerosandCursor 0c45b86d67 chore: 初始化 eightbf-analysis-go(对齐 Java 会商作业服务)
Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-03 00:13:46 +08:00

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
}