chore: commit workspace updates

This commit is contained in:
ddCat
2026-09-14 01:11:56 +08:00
parent 01a2d171f5
commit 9727656b2b
491 changed files with 12826 additions and 72249 deletions

View File

@@ -122,7 +122,7 @@ func (r *Repository) AdminUserTasks(userID string, limit int, offset int) ([]mod
return nil, 0, err
}
var tasks []model.Task
err := query.Select("id", "user_id", "session_id", "project_id", "type", "status", "stage", "progress", "operation", "provider", "model", "billing_order_id", "provider_request_id", "poll_stage", "attempts", "started_at", "completed_at", "created_at", "updated_at").
err := query.Select("id", "user_id", "project_id", "type", "status", "stage", "progress", "operation", "provider", "model", "billing_order_id", "provider_request_id", "poll_stage", "attempts", "started_at", "completed_at", "created_at", "updated_at").
Order("created_at desc").Limit(limit).Offset(offset).Find(&tasks).Error
return tasks, total, err
}

View File

@@ -0,0 +1,126 @@
package repository
import (
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"infinite-canvas/backend/internal/model"
)
func (r *Repository) EnsureCloudAgent(run *model.CloudAgentExecution) error {
return r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(run).Error
}
func (r *Repository) CloudAgent(userID, id string) (*model.CloudAgentExecution, error) {
var run model.CloudAgentExecution
err := r.db.First(&run, "id = ? AND user_id = ?", id, userID).Error
return &run, err
}
func (r *Repository) CloudAgentRoots() ([]model.Task, error) {
var tasks []model.Task
err := r.db.Where("operation = ? AND id NOT IN (SELECT id FROM cloud_agent_executions)", "cloud_agent").Order("created_at").Limit(50).Find(&tasks).Error
return tasks, err
}
// A stable keyset makes waiting rows yield to later runs without changing business timestamps.
func (r *Repository) ActiveCloudAgentsAfter(after string, limit int) ([]model.CloudAgentExecution, error) {
var runs []model.CloudAgentExecution
if limit < 1 || limit > 50 {
limit = 50
}
err := r.db.Where("(status IN ? OR cleanup_pending = ?) AND id > ?", []string{"running", "queued"}, true, after).Order("id").Limit(limit).Find(&runs).Error
return runs, err
}
func (r *Repository) ActiveCloudAgents() ([]model.CloudAgentExecution, error) {
return r.ActiveCloudAgentsAfter("", 50)
}
// Do not load the transcript, tasks and bills for an unchanged SSE subscription.
func (r *Repository) CloudAgentRevision(userID, id string) (int64, error) {
var run model.CloudAgentExecution
err := r.db.Select("id", "revision").Where("id = ? AND user_id = ?", id, userID).First(&run).Error
return run.Revision, err
}
// Lock before reading: checkpoints, canvas writes and task reservations commit together.
func (r *Repository) MutateCloudAgent(userID, id string, revision int64, fn func(*model.CloudAgentExecution, *Repository) error) error {
return r.db.Transaction(func(tx *gorm.DB) error {
q := tx.Model(&model.CloudAgentExecution{}).Where("id = ? AND user_id = ? AND revision = ?", id, userID, revision).UpdateColumn("revision", gorm.Expr("revision + 1"))
if q.Error != nil {
return q.Error
}
if q.RowsAffected != 1 {
return ErrCreationConflict
}
run, err := New(tx).CloudAgent(userID, id)
if err != nil {
return err
}
if err = fn(run, New(tx)); err != nil {
return err
}
return tx.Save(run).Error
})
}
func (r *Repository) CreateCloudAgentCanvasMutation(mutation *model.CloudAgentCanvasMutation) error {
return r.db.Create(mutation).Error
}
func (r *Repository) LatestCloudAgentCanvasMutation(userID, runID string) (*model.CloudAgentCanvasMutation, error) {
var mutation model.CloudAgentCanvasMutation
err := r.db.Where("user_id = ? AND run_id = ?", userID, runID).
Order("created_at DESC, id DESC").First(&mutation).Error
return &mutation, err
}
func (r *Repository) MarkCloudAgentCanvasMutationUndone(userID, runID, mutationID string, undoneAt time.Time) error {
result := r.db.Model(&model.CloudAgentCanvasMutation{}).
Where("id = ? AND user_id = ? AND run_id = ? AND status = ?", mutationID, userID, runID, "applied").
Updates(map[string]any{"status": "undone", "undone_at": undoneAt})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return ErrCreationConflict
}
return nil
}
// MarkCloudAgentFailed is a terminal CAS transition that does not read or
// rewrite StateJSON. It is deliberately usable when a corrupted runtime state
// can no longer be decoded; leaving such a run in running would make the
// scheduler retry it forever.
func (r *Repository) MarkCloudAgentFailed(userID, id string, revision int64, message ...string) error {
detail := "Agent 运行状态损坏,本轮已停止"
if len(message) > 0 {
detail = message[0]
}
result := r.db.Model(&model.CloudAgentExecution{}).
Where("id = ? AND user_id = ? AND revision = ? AND status IN ?", id, userID, revision, []string{"queued", "running"}).
Updates(map[string]any{"status": "failed", "cleanup_pending": true, "failure_message": detail, "revision": gorm.Expr("revision + 1")})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return ErrCreationConflict
}
return nil
}
// MarkCloudAgentCancelled is the corruption-safe cancellation transition. It
// intentionally does not touch StateJSON: cancellation must still stop the
// scheduler when the orchestration blob can no longer be decoded.
func (r *Repository) MarkCloudAgentCancelled(userID, id string, revision int64) error {
result := r.db.Model(&model.CloudAgentExecution{}).
Where("id = ? AND user_id = ? AND revision = ? AND status IN ?", id, userID, revision, []string{"queued", "running", "waiting_approval"}).
Updates(map[string]any{"status": "cancelled", "cleanup_pending": true, "revision": gorm.Expr("revision + 1")})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return ErrCreationConflict
}
return nil
}

View File

@@ -1,198 +0,0 @@
package repository
import (
"errors"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var ErrComfyBridgeRequestOwnership = errors.New("comfy bridge request belongs to another bridge")
var ErrComfyBridgeRequestState = errors.New("comfy bridge request state conflict")
// ComfyBridgeForUser 按用户作用域读取 Bridge,避免管理接口意外越权访问其他用户的设备。
func (r *Repository) ComfyBridgeForUser(userID string, id string) (*model.ComfyBridge, error) {
var bridge model.ComfyBridge
if err := r.db.First(&bridge, "id = ? AND user_id = ?", id, userID).Error; err != nil {
return nil, err
}
return &bridge, nil
}
func (r *Repository) ComfyBridgesForUser(userID string) ([]model.ComfyBridge, error) {
var bridges []model.ComfyBridge
// 已撤销的 Bridge 仍保留数据库记录用于审计,但不能再出现在可选设备列表中。
err := r.db.Where("user_id = ? AND enabled = ?", userID, true).Order("created_at desc").Find(&bridges).Error
return bridges, err
}
// ComfyBridgeByTokenHash 只返回启用的 Bridge。Token 明文不会进入数据库查询或日志。
func (r *Repository) ComfyBridgeByTokenHash(tokenHash string) (*model.ComfyBridge, error) {
var bridge model.ComfyBridge
if err := r.db.First(&bridge, "token_hash = ? AND enabled = ?", tokenHash, true).Error; err != nil {
return nil, err
}
return &bridge, nil
}
func (r *Repository) DisableComfyBridge(userID string, id string, now time.Time) error {
result := r.db.Model(&model.ComfyBridge{}).
Where("id = ? AND user_id = ? AND enabled = ?", id, userID, true).
Updates(map[string]any{"enabled": false, "updated_at": now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *Repository) TouchComfyBridge(id string, now time.Time) error {
result := r.db.Model(&model.ComfyBridge{}).Where("id = ? AND enabled = ?", id, true).
Updates(map[string]any{"last_seen_at": now, "updated_at": now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *Repository) UpdateComfyBridgeHeartbeat(id string, capabilitiesJSON string, now time.Time) error {
updates := map[string]any{"last_seen_at": now, "updated_at": now}
if capabilitiesJSON != "" {
updates["capabilities_json"] = capabilitiesJSON
}
result := r.db.Model(&model.ComfyBridge{}).Where("id = ? AND enabled = ?", id, true).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *Repository) MarkComfyBridgeTask(id string, now time.Time) error {
result := r.db.Model(&model.ComfyBridge{}).Where("id = ? AND enabled = ?", id, true).
Updates(map[string]any{"last_task_at": now, "last_seen_at": now, "updated_at": now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *Repository) CreateOrGetComfyBridgeRequest(request *model.ComfyBridgeRequest) (*model.ComfyBridgeRequest, bool, error) {
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(request)
if result.Error != nil {
return nil, false, result.Error
}
if result.RowsAffected == 1 {
return request, true, nil
}
var existing model.ComfyBridgeRequest
if err := r.db.First(&existing, "id = ?", request.ID).Error; err != nil {
return nil, false, err
}
return &existing, false, nil
}
func (r *Repository) ComfyBridgeRequest(id string) (*model.ComfyBridgeRequest, error) {
var request model.ComfyBridgeRequest
if err := r.db.First(&request, "id = ?", id).Error; err != nil {
return nil, err
}
return &request, nil
}
func (r *Repository) PendingComfyBridgeRequestCount(bridgeID string, now time.Time) (int64, error) {
var count int64
err := r.db.Model(&model.ComfyBridgeRequest{}).
Where("bridge_id = ? AND status IN ? AND expires_at > ?", bridgeID, []string{"queued", "claimed"}, now).
Count(&count).Error
return count, err
}
// ClaimNextComfyBridgeRequest 以数据库状态机为队列真相,后端重启后仍可继续投递未领取请求。
func (r *Repository) ClaimNextComfyBridgeRequest(bridgeID string, now time.Time) (*model.ComfyBridgeRequest, error) {
var request model.ComfyBridgeRequest
err := r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.ComfyBridgeRequest{}).
Where("status IN ? AND expires_at <= ?", []string{"queued", "claimed"}, now).
Updates(map[string]any{"status": "failed", "error": "本地 ComfyUI Bridge 请求已过期", "completed_at": now, "updated_at": now}).Error; err != nil {
return err
}
query := tx.Where("bridge_id = ? AND status = ? AND expires_at > ?", bridgeID, "queued", now).
Order("created_at asc").Limit(1)
if r.Dialect() == "postgres" {
query = query.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"})
}
result := query.Find(&request)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
request = model.ComfyBridgeRequest{}
return nil
}
updated := tx.Model(&model.ComfyBridgeRequest{}).
Where("id = ? AND status = ?", request.ID, "queued").
Updates(map[string]any{"status": "claimed", "claimed_at": now, "updated_at": now})
if updated.Error != nil {
return updated.Error
}
if updated.RowsAffected != 1 {
request = model.ComfyBridgeRequest{}
return nil
}
return tx.First(&request, "id = ?", request.ID).Error
})
if err != nil || request.ID == "" {
return nil, err
}
return &request, nil
}
func (r *Repository) CompleteComfyBridgeRequest(bridgeID string, id string, status string, resultJSON string, errorText string, now time.Time) (*model.ComfyBridgeRequest, error) {
var request model.ComfyBridgeRequest
err := r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.First(&request, "id = ?", id).Error; err != nil {
return err
}
if request.BridgeID != bridgeID {
return ErrComfyBridgeRequestOwnership
}
if request.Status == "succeeded" || request.Status == "failed" || request.Status == "cancelled" {
return nil
}
if request.Status != "claimed" {
return ErrComfyBridgeRequestState
}
if err := tx.Model(&model.ComfyBridgeRequest{}).Where("id = ? AND status = ?", id, "claimed").Updates(map[string]any{
"status": status, "result_json": resultJSON, "error": errorText, "completed_at": now, "updated_at": now,
}).Error; err != nil {
return err
}
return tx.First(&request, "id = ?", id).Error
})
return &request, err
}
func (r *Repository) CancelComfyBridgeRequest(id string, now time.Time) error {
return r.db.Model(&model.ComfyBridgeRequest{}).
Where("id = ? AND status IN ?", id, []string{"queued", "claimed"}).
Updates(map[string]any{"status": "cancelled", "error": "任务已取消", "completed_at": now, "updated_at": now}).Error
}
func (r *Repository) FailComfyBridgeRequests(bridgeID string, errorText string, now time.Time) error {
return r.db.Model(&model.ComfyBridgeRequest{}).
Where("bridge_id = ? AND status IN ?", bridgeID, []string{"queued", "claimed"}).
Updates(map[string]any{"status": "failed", "error": errorText, "completed_at": now, "updated_at": now}).Error
}

View File

@@ -713,6 +713,10 @@ func (r *Repository) SettleBillingOrder(id string, providerRequestID string) err
if err != nil {
return err
}
chargeCapped := order.ChargeLimitMicrocredits > 0 && actual > order.ChargeLimitMicrocredits
if chargeCapped {
actual = order.ChargeLimitMicrocredits
}
observedActual = actual
observedActualAvailable = true
refund := max(reserved-actual, int64(0))
@@ -746,7 +750,9 @@ func (r *Repository) SettleBillingOrder(id string, providerRequestID string) err
return err
}
consumeNote := ""
if supplement > 0 {
if chargeCapped {
consumeNote = "Token 实际用量超过 Agent 报价,已按本轮硬上限结算"
} else if supplement > 0 {
consumeNote = "Token 实际用量超过预授权,已补扣差额"
}
if err := tx.Create(&model.CreditLedgerEntry{ID: newRepositoryID(), UserID: order.UserID, Type: model.CreditLedgerConsume,

View File

@@ -2,6 +2,7 @@ package repository
import (
"errors"
"strings"
"testing"
"infinite-canvas/backend/internal/model"
@@ -157,6 +158,58 @@ func TestSettleArkVideoTokenOrderSupplementsUnderreservation(t *testing.T) {
}
}
func TestSettleAgentTokenOrderCapsChargeAtQuotedLimit(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:finance-token-agent-cap?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.CreditAccount{}, &model.BillingOrder{}, &model.ApiCallLog{}, &model.CreditLedgerEntry{}); err != nil {
t.Fatal(err)
}
if err := db.Create(&model.CreditAccount{UserID: "user-1", AvailableMicrocredits: 500, ReservedMicrocredits: 100}).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&model.BillingOrder{
ID: "agent-order", UserID: "user-1", IdempotencyKey: "task:agent-task", Capability: "text", BillingMode: "token",
AmountMicrocredits: 100, ReservedAmountMicrocredits: 100, ChargeLimitMicrocredits: 100,
OutputTokenPriceMicrocredits: 1_000_000, MultiplierBasisPoints: 10_000, Status: model.BillingStatusRunning,
}).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&model.ApiCallLog{
ID: "agent-log", BillingOrderID: "agent-order", RequestKind: "generate", Billable: true,
Status: model.ApiCallStatusSucceeded, UsageAvailable: true, OutputTokens: 200,
}).Error; err != nil {
t.Fatal(err)
}
repo := &Repository{db: db}
if err := repo.SettleBillingOrder("agent-order", "provider-request"); err != nil {
t.Fatalf("SettleBillingOrder() error = %v", err)
}
var order model.BillingOrder
if err := db.First(&order, "id = ?", "agent-order").Error; err != nil {
t.Fatal(err)
}
if order.ActualAmountMicrocredits != 100 || order.OutputTokens != 200 || !order.UsageAvailable {
t.Fatalf("capped Agent order = %#v", order)
}
var account model.CreditAccount
if err := db.First(&account, "user_id = ?", "user-1").Error; err != nil {
t.Fatal(err)
}
if account.AvailableMicrocredits != 500 || account.ReservedMicrocredits != 0 {
t.Fatalf("capped Agent account = %#v", account)
}
var entry model.CreditLedgerEntry
if err := db.First(&entry, "billing_order_id = ? AND type = ?", "agent-order", model.CreditLedgerConsume).Error; err != nil {
t.Fatal(err)
}
if entry.AmountMicrocredits != -100 || !strings.Contains(entry.Note, "硬上限") {
t.Fatalf("capped Agent ledger entry = %#v", entry)
}
}
func TestSettleArkVideoTokenOrderAllowsNegativeBalance(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:finance-token-negative-balance?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {

View File

@@ -250,6 +250,18 @@ func (r *Repository) CreateRouteAttempt(item *model.RouteAttempt) error {
}
func (r *Repository) SaveRouteAttempt(item *model.RouteAttempt) error { return r.db.Save(item).Error }
func (r *Repository) MarkRouteAttemptDispatching(id string) error {
result := r.db.Model(&model.RouteAttempt{}).Where("id = ? AND dispatch_state = ?", id, "not_sent").
Updates(map[string]any{"status": "dispatching", "dispatch_state": "submission_unknown"})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return ErrCreationConflict
}
return nil
}
func (r *Repository) LatestRouteAttempt(taskID string) (*model.RouteAttempt, error) {
var item model.RouteAttempt
if err := r.db.Where("task_id = ?", taskID).Order("route_run desc, attempt_number desc").First(&item).Error; err != nil {

View File

@@ -44,8 +44,6 @@ type UserStorageUsage struct {
AssetBytes int64 `json:"assetBytes"`
CanvasCount int64 `json:"canvasCount"`
CanvasBytes int64 `json:"canvasBytes"`
SessionCount int64 `json:"sessionCount"`
SessionBytes int64 `json:"sessionBytes"`
TaskCount int64 `json:"taskCount"`
TaskBytes int64 `json:"taskBytes"`
APICallCount int64 `json:"apiCallCount"`
@@ -108,9 +106,6 @@ func (r *Repository) UserStorageUsage(userID string) (UserStorageUsage, error) {
(SELECT COALESCE(SUM(length(CAST(COALESCE(payload_json, '') AS BLOB))), 0) FROM assets WHERE user_id = ?) AS asset_bytes,
(SELECT COUNT(*) FROM canvas_projects WHERE user_id = ?) AS canvas_count,
(SELECT COALESCE(SUM(length(CAST(COALESCE(payload_json, '') AS BLOB))), 0) FROM canvas_projects WHERE user_id = ?) AS canvas_bytes,
(SELECT COUNT(*) FROM sessions WHERE user_id = ?) AS session_count,
(SELECT COALESCE(SUM(length(CAST(COALESCE(prompt, '') AS BLOB)) + length(CAST(COALESCE(canvas_snapshot_json, '') AS BLOB)) + length(CAST(COALESCE(canvas_ops_json, '') AS BLOB))), 0) FROM sessions WHERE user_id = ?)
+ (SELECT COALESCE(SUM(length(CAST(COALESCE(content, '') AS BLOB)) + length(CAST(COALESCE(payload, '') AS BLOB))), 0) FROM messages WHERE user_id = ?) AS session_bytes,
(SELECT COUNT(*) FROM tasks WHERE user_id = ?) AS task_count,
(SELECT COALESCE(SUM(length(CAST(COALESCE(prompt, '') AS BLOB)) + length(CAST(COALESCE(input_json, '') AS BLOB)) + length(CAST(COALESCE(result_json, '') AS BLOB)) + length(CAST(COALESCE(text_draft, '') AS BLOB)) + length(CAST(COALESCE(error, '') AS BLOB))), 0) FROM tasks WHERE user_id = ?)
+ (SELECT COALESCE(SUM(length(CAST(COALESCE(message, '') AS BLOB)) + length(CAST(COALESCE(payload, '') AS BLOB))), 0) FROM task_logs WHERE user_id = ?)
@@ -123,7 +118,7 @@ func (r *Repository) UserStorageUsage(userID string) (UserStorageUsage, error) {
query = strings.ReplaceAll(query, "length(CAST(COALESCE(", "octet_length(COALESCE(")
query = strings.ReplaceAll(query, ", '') AS BLOB))", ", ''))")
}
err := r.db.Raw(query, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID).Scan(&usage).Error
err := r.db.Raw(query, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID).Scan(&usage).Error
return usage, err
}
@@ -157,7 +152,7 @@ func (r *Repository) CleanupDuplicateTaskPayloads() error {
if err := tx.Model(&model.TaskLog{}).Where("length(payload) > ?", 4000).Update("payload", "").Error; err != nil {
return err
}
return tx.Delete(&model.Result{}, "kind = ? AND session_id = ?", "generation_result", "").Error
return nil
})
}
@@ -537,7 +532,7 @@ func (r *Repository) UpdateTaskProviderProgress(id string, progress int) error {
}).Error
}
func (r *Repository) SaveTaskCompletion(task *model.Task, expected model.TaskStatus, session *model.Session, message *model.Message, results []model.Result) error {
func (r *Repository) SaveTaskCompletion(task *model.Task, expected model.TaskStatus, results []model.Result) error {
return r.db.Transaction(func(tx *gorm.DB) error {
updated := taskLeaseWriter(tx.Model(&model.Task{}), task.LeaseOwner).
Where("id = ? AND status = ?", task.ID, expected).
@@ -548,16 +543,6 @@ func (r *Repository) SaveTaskCompletion(task *model.Task, expected model.TaskSta
if updated.RowsAffected != 1 {
return ErrTaskStateConflict
}
if session != nil {
if err := tx.Save(session).Error; err != nil {
return err
}
}
if message != nil {
if err := tx.Create(message).Error; err != nil {
return err
}
}
for index := range results {
if err := tx.Create(&results[index]).Error; err != nil {
return err
@@ -672,7 +657,7 @@ func (r *Repository) Tasks(userID string, limit int, projectID string, activeOnl
if limit <= 0 || limit > 100 {
limit = 50
}
query := r.db.Select("id", "session_id", "project_id", "type", "status", "stage", "progress", "prompt", "operation", "provider", "model", "input_json", "result_json", "billing_order_id", "provider_request_id", "provider_cancel_status", "provider_cancel_error", "provider_cancel_attempts", "provider_cancel_requested_at", "provider_cancelled_at", "provider_cancel_next_check_at", "attempts", "started_at", "completed_at", "created_at", "updated_at").
query := r.db.Select("id", "project_id", "type", "status", "stage", "progress", "prompt", "operation", "provider", "model", "input_json", "result_json", "billing_order_id", "provider_request_id", "provider_cancel_status", "provider_cancel_error", "provider_cancel_attempts", "provider_cancel_requested_at", "provider_cancelled_at", "provider_cancel_next_check_at", "attempts", "started_at", "completed_at", "created_at", "updated_at").
Where("user_id = ?", userID)
if strings.TrimSpace(projectID) != "" {
query = query.Where("project_id = ?", strings.TrimSpace(projectID))
@@ -693,60 +678,6 @@ func (r *Repository) SuccessfulWorkflowTasksForProject(userID string, projectID
return tasks, err
}
func (r *Repository) Session(id string) (*model.Session, error) {
var session model.Session
if err := r.db.First(&session, "id = ?", id).Error; err != nil {
return nil, err
}
return &session, nil
}
func (r *Repository) SessionForUser(userID string, id string) (*model.Session, error) {
var session model.Session
if err := r.db.First(&session, "id = ? AND user_id = ?", id, userID).Error; err != nil {
return nil, err
}
return &session, nil
}
func (r *Repository) DeleteSessionDraft(userID string, id string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var taskIDs []string
if err := tx.Model(&model.Task{}).Where("user_id = ? AND session_id = ?", userID, id).Pluck("id", &taskIDs).Error; err != nil {
return err
}
if len(taskIDs) > 0 {
if err := tx.Delete(&model.TaskTextDelta{}, "user_id = ? AND task_id IN ?", userID, taskIDs).Error; err != nil {
return err
}
}
if err := tx.Delete(&model.Message{}, "user_id = ? AND session_id = ?", userID, id).Error; err != nil {
return err
}
return tx.Delete(&model.Session{}, "id = ? AND user_id = ?", id, userID).Error
})
}
func (r *Repository) SessionMessages(userID string, sessionID string) ([]model.Message, error) {
var messages []model.Message
err := r.db.Order("created_at asc").Find(&messages, "user_id = ? AND session_id = ?", userID, sessionID).Error
return messages, err
}
func (r *Repository) SessionTasks(userID string, sessionID string) ([]model.Task, error) {
var tasks []model.Task
err := r.db.Select("id", "user_id", "session_id", "project_id", "type", "status", "prompt", "operation", "provider", "model", "attempts", "started_at", "completed_at", "created_at", "updated_at").
Order("created_at asc").
Find(&tasks, "user_id = ? AND session_id = ?", userID, sessionID).Error
return tasks, err
}
func (r *Repository) SessionResults(userID string, sessionID string) ([]model.Result, error) {
var results []model.Result
err := r.db.Order("created_at asc").Find(&results, "user_id = ? AND session_id = ?", userID, sessionID).Error
return results, err
}
func (r *Repository) TaskLogs(userID string, taskID string) ([]model.TaskLog, error) {
var logs []model.TaskLog
err := r.db.Order("created_at asc").Find(&logs, "user_id = ? AND task_id = ?", userID, taskID).Error
@@ -1024,8 +955,7 @@ func (r *Repository) UserStoredFileBytes(userID string) (int64, error) {
GROUP BY COALESCE(NULLIF(provider, ''), 'local'), endpoint, bucket, object_key
) AS physical_resources
), 0)
+ (SELECT COALESCE(SUM(size), 0) FROM session_files WHERE user_id = ?)
`, userID, model.ResourceStatusReady, userID).Scan(&total).Error
`, userID, model.ResourceStatusReady).Scan(&total).Error
return total, err
}
@@ -1256,9 +1186,6 @@ func (r *Repository) DeleteCanvasProject(userID string, id string) error {
if err := tx.Model(&model.Task{}).Where("user_id = ? AND project_id = ?", userID, id).Update("project_id", "").Error; err != nil {
return err
}
if err := tx.Model(&model.Session{}).Where("user_id = ? AND project_id = ?", userID, id).Update("project_id", "").Error; err != nil {
return err
}
return tx.Delete(&model.CanvasProject{}, "id = ? AND user_id = ?", id, userID).Error
})
}
@@ -1388,9 +1315,6 @@ func (r *Repository) DeleteProject(userID string, id string, canvasUpdates []mod
if err := tx.Model(&model.Task{}).Where("user_id = ? AND project_id = ?", userID, id).Update("project_id", "").Error; err != nil {
return err
}
if err := tx.Model(&model.Session{}).Where("user_id = ? AND project_id = ?", userID, id).Update("project_id", "").Error; err != nil {
return err
}
return tx.Delete(&model.Project{}, "id = ? AND user_id = ?", id, userID).Error
})
}

View File

@@ -111,13 +111,6 @@ func (r *Repository) ResourceReferenceSnapshot(userID string, excludingAssetID s
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "任务", ID: task.ID, Title: task.Prompt, PrimaryJSON: task.InputJSON, SecondaryJSON: task.ResultJSON})
}
var sessions []model.Session
if err := r.db.Select("id", "prompt", "canvas_snapshot_json", "canvas_ops_json").Where("user_id = ?", userID).Find(&sessions).Error; err != nil {
return snapshot, err
}
for _, session := range sessions {
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "会话", ID: session.ID, Title: session.Prompt, PrimaryJSON: session.CanvasSnapshotJSON, SecondaryJSON: session.CanvasOpsJSON})
}
var runs []model.CreationRun
if err := r.db.Where("user_id = ?", userID).Find(&runs).Error; err != nil {
return snapshot, err
@@ -133,14 +126,6 @@ func (r *Repository) ResourceReferenceSnapshot(userID string, excludingAssetID s
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "创作执行项", ID: submission.ID, Title: submission.ItemKey, PrimaryJSON: submission.RequestJSON})
}
var messages []model.Message
if err := r.db.Select("id", "content", "payload").Where("user_id = ?", userID).Find(&messages).Error; err != nil {
return snapshot, err
}
for _, message := range messages {
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "会话消息", ID: message.ID, Title: message.Content, PrimaryJSON: message.Payload})
}
var taskLogs []model.TaskLog
if err := r.db.Select("id", "message", "payload").Where("user_id = ?", userID).Find(&taskLogs).Error; err != nil {
return snapshot, err

View File

@@ -15,7 +15,7 @@ func TestTaskLeaseFencesExpiredAndReclaimedWriters(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.Task{}, &model.Result{}, &model.Message{}, &model.Session{}); err != nil {
if err := db.AutoMigrate(&model.Task{}, &model.Result{}); err != nil {
t.Fatal(err)
}
if err := db.Create(&model.Task{ID: "task", UserID: "user", Status: model.TaskStatusQueued}).Error; err != nil {
@@ -49,11 +49,11 @@ func TestTaskLeaseFencesExpiredAndReclaimedWriters(t *testing.T) {
}
}
old.Status = model.TaskStatusSucceeded
if err := repo.SaveTaskCompletion(old, model.TaskStatusRunning, nil, &model.Message{ID: "stale"}, []model.Result{{ID: "stale"}}); !errors.Is(err, ErrTaskStateConflict) {
if err := repo.SaveTaskCompletion(old, model.TaskStatusRunning, []model.Result{{ID: "stale"}}); !errors.Is(err, ErrTaskStateConflict) {
t.Fatalf("stale completion: %v", err)
}
var count int64
for _, table := range []any{&model.Result{}, &model.Message{}} {
for _, table := range []any{&model.Result{}} {
if err := db.Model(table).Count(&count).Error; err != nil || count != 0 {
t.Fatalf("stale related row persisted: %d %v", count, err)
}
@@ -65,7 +65,7 @@ func TestTaskLeaseFencesExpiredAndReclaimedWriters(t *testing.T) {
t.Fatal(err)
}
current.Status = model.TaskStatusSucceeded
if err := repo.SaveTaskCompletion(current, model.TaskStatusRunning, nil, nil, []model.Result{{ID: "valid", TaskID: current.ID}}); err != nil {
if err := repo.SaveTaskCompletion(current, model.TaskStatusRunning, []model.Result{{ID: "valid", TaskID: current.ID}}); err != nil {
t.Fatal(err)
}
stored, err := repo.Task(current.ID)