启动项目逻辑

This commit is contained in:
2026-06-09 22:43:29 +08:00
parent d5a28b11a3
commit f4a7c770af
43 changed files with 3797 additions and 42 deletions
+190
View File
@@ -0,0 +1,190 @@
package run
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
runsqlc "github.com/yan1h/agent-coding-workflow/internal/run/sqlc"
)
// Repository 是 run 模块对 PG 的全部依赖。Service / Supervisor 单测通过手写
// fakeRepo 满足。
type Repository interface {
CreateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error)
GetRunProfileByID(ctx context.Context, id uuid.UUID) (*RunProfile, error)
ListRunProfilesByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*RunProfile, error)
UpdateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error)
DeleteRunProfile(ctx context.Context, id uuid.UUID) error
MarkRunProfileStarted(ctx context.Context, id uuid.UUID) (*RunProfile, error)
UpdateRunProfileExit(ctx context.Context, id uuid.UUID, exitCode *int32, lastErr string) error
}
// IsUniqueViolation reports whether err is a PG unique constraint violation
// (service 层据此把 (workspace_id, slug) 冲突翻成 409)。
func IsUniqueViolation(err error) bool {
var pg *pgconn.PgError
return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
}
type pgRepo struct {
q *runsqlc.Queries
}
// NewPostgresRepository 用 pgxpool 构造 Repository。
func NewPostgresRepository(pool *pgxpool.Pool) Repository {
return &pgRepo{q: runsqlc.New(pool)}
}
// ===== 类型转换 helper =====
func toPgUUID(u uuid.UUID) pgtype.UUID { return pgtype.UUID{Bytes: u, Valid: true} }
func fromPgUUID(p pgtype.UUID) uuid.UUID {
if !p.Valid {
return uuid.Nil
}
return uuid.UUID(p.Bytes)
}
func fromPgTimePtr(t pgtype.Timestamptz) *time.Time {
if !t.Valid {
return nil
}
v := t.Time
return &v
}
// normalizeStrSlice 把 nil 归一为非 nil 空切片,避免 pgx 把 nil []string 写成
// SQL NULL 违反 TEXT[] NOT NULL 约束(同 acp/repository.go 的同名 helper)。
func normalizeStrSlice(s []string) []string {
if s == nil {
return []string{}
}
return s
}
func rowToRunProfile(row runsqlc.WorkspaceRunProfile) *RunProfile {
return &RunProfile{
ID: fromPgUUID(row.ID),
WorkspaceID: fromPgUUID(row.WorkspaceID),
Slug: row.Slug,
Name: row.Name,
Description: row.Description,
Command: row.Command,
Args: row.Args,
EncryptedEnv: row.EncryptedEnv,
Enabled: row.Enabled,
LastStartedAt: fromPgTimePtr(row.LastStartedAt),
LastExitCode: row.LastExitCode,
LastError: row.LastError,
CreatedBy: fromPgUUID(row.CreatedBy),
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
}
}
func (r *pgRepo) CreateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error) {
row, err := r.q.CreateRunProfile(ctx, runsqlc.CreateRunProfileParams{
ID: toPgUUID(p.ID),
WorkspaceID: toPgUUID(p.WorkspaceID),
Slug: p.Slug,
Name: p.Name,
Description: p.Description,
Command: p.Command,
Args: normalizeStrSlice(p.Args),
EncryptedEnv: p.EncryptedEnv,
Enabled: p.Enabled,
CreatedBy: toPgUUID(p.CreatedBy),
})
if err != nil {
if IsUniqueViolation(err) {
return nil, errs.New(errs.CodeRunProfileSlugTaken, "run profile slug already taken in this workspace")
}
return nil, errs.Wrap(err, errs.CodeInternal, "create run profile")
}
return rowToRunProfile(row), nil
}
func (r *pgRepo) GetRunProfileByID(ctx context.Context, id uuid.UUID) (*RunProfile, error) {
row, err := r.q.GetRunProfileByID(ctx, toPgUUID(id))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.New(errs.CodeRunProfileNotFound, "run profile not found")
}
return nil, errs.Wrap(err, errs.CodeInternal, "get run profile")
}
return rowToRunProfile(row), nil
}
func (r *pgRepo) ListRunProfilesByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*RunProfile, error) {
rows, err := r.q.ListRunProfilesByWorkspace(ctx, toPgUUID(wsID))
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "list run profiles")
}
out := make([]*RunProfile, 0, len(rows))
for _, row := range rows {
out = append(out, rowToRunProfile(row))
}
return out, nil
}
func (r *pgRepo) UpdateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error) {
row, err := r.q.UpdateRunProfile(ctx, runsqlc.UpdateRunProfileParams{
ID: toPgUUID(p.ID),
Slug: p.Slug,
Name: p.Name,
Description: p.Description,
Command: p.Command,
Args: normalizeStrSlice(p.Args),
EncryptedEnv: p.EncryptedEnv,
Enabled: p.Enabled,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.New(errs.CodeRunProfileNotFound, "run profile not found")
}
if IsUniqueViolation(err) {
return nil, errs.New(errs.CodeRunProfileSlugTaken, "run profile slug already taken in this workspace")
}
return nil, errs.Wrap(err, errs.CodeInternal, "update run profile")
}
return rowToRunProfile(row), nil
}
func (r *pgRepo) DeleteRunProfile(ctx context.Context, id uuid.UUID) error {
if err := r.q.DeleteRunProfile(ctx, toPgUUID(id)); err != nil {
return errs.Wrap(err, errs.CodeInternal, "delete run profile")
}
return nil
}
func (r *pgRepo) MarkRunProfileStarted(ctx context.Context, id uuid.UUID) (*RunProfile, error) {
row, err := r.q.MarkRunProfileStarted(ctx, toPgUUID(id))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.New(errs.CodeRunProfileNotFound, "run profile not found")
}
return nil, errs.Wrap(err, errs.CodeInternal, "mark run profile started")
}
return rowToRunProfile(row), nil
}
func (r *pgRepo) UpdateRunProfileExit(ctx context.Context, id uuid.UUID, exitCode *int32, lastErr string) error {
if err := r.q.UpdateRunProfileExit(ctx, runsqlc.UpdateRunProfileExitParams{
ID: toPgUUID(id),
LastExitCode: exitCode,
LastError: lastErr,
}); err != nil {
return errs.Wrap(err, errs.CodeInternal, "update run profile exit")
}
return nil
}