chore: commit workspace updates
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
126
backend/internal/repository/cloud_agent.go
Normal file
126
backend/internal/repository/cloud_agent.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user