// session_service.go 实现 SessionService 接口。Create 走完整事务(worktree // acquire + supervisor.Spawn)的实现见 F3;本文件仅放骨架 + ResolveBranch // pure helper。 package acp import ( "context" "fmt" "strings" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/audit" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/project" "github.com/yan1h/agent-coding-workflow/internal/workspace" ) // SessionServiceConfig 是 service 的限流配置(从 AcpConfig 派生)。 type SessionServiceConfig struct { MaxActiveGlobal int MaxActivePerUser int } // ProjectAccess 是 acp 模块对 project 模块的窄接口;adapter 在 app.go 实现。 type ProjectAccess interface { GetProjectByWorkspace(ctx context.Context, wsID uuid.UUID) (*project.Project, error) GetIssueByNumber(ctx context.Context, projectID uuid.UUID, number int) (*project.Issue, error) GetRequirementByNumber(ctx context.Context, projectID uuid.UUID, number int) (*project.Requirement, error) } type sessionService struct { repo Repository wsSvc workspace.WorkspaceService wtSvc workspace.WorktreeService wsRepo workspace.Repository pa ProjectAccess sup *Supervisor audit audit.Recorder cfg SessionServiceConfig } // NewSessionService 构造 SessionService。 func NewSessionService(repo Repository, wsSvc workspace.WorkspaceService, wtSvc workspace.WorktreeService, wsRepo workspace.Repository, pa ProjectAccess, sup *Supervisor, rec audit.Recorder, cfg SessionServiceConfig) SessionService { return &sessionService{ repo: repo, wsSvc: wsSvc, wtSvc: wtSvc, wsRepo: wsRepo, pa: pa, sup: sup, audit: rec, cfg: cfg, } } // ResolveBranch 是 spec §8.1 的纯函数实现:根据 CreateSessionInput 决定 // (branch, isMainWorktree)。允许独立单测,不依赖 service 状态。 // // 规则(按优先级): // 1. in.Branch 显式:直接用;isMain = (== ws.DefaultBranch) // 2. in.IssueNumber:拉 issue,校验 workspace 关联,命名 "issue/-" // 3. in.RequirementNumber:拉 requirement,校验 workspace,命名 "req/-" // 4. 都没给:fallback "acp-auto/<8-hex>",isMain = false func ResolveBranch(ctx context.Context, ws *workspace.Workspace, sessionID uuid.UUID, in CreateSessionInput, projectSlug string, pa ProjectAccess) (string, bool, error) { if in.Branch != nil && *in.Branch != "" { br := *in.Branch return br, br == ws.DefaultBranch, nil } if in.IssueNumber != nil { iss, err := pa.GetIssueByNumber(ctx, ws.ProjectID, *in.IssueNumber) if err != nil { return "", false, err } if iss.WorkspaceID != nil && *iss.WorkspaceID != ws.ID { return "", false, errs.New(errs.CodeInvalidInput, fmt.Sprintf("issue #%d already linked to other workspace", *in.IssueNumber)) } return fmt.Sprintf("issue/%s-%d", projectSlug, iss.Number), false, nil } if in.RequirementNumber != nil { req, err := pa.GetRequirementByNumber(ctx, ws.ProjectID, *in.RequirementNumber) if err != nil { return "", false, err } if req.WorkspaceID != nil && *req.WorkspaceID != ws.ID { return "", false, errs.New(errs.CodeInvalidInput, fmt.Sprintf("requirement #%d already linked to other workspace", *in.RequirementNumber)) } return fmt.Sprintf("req/%s-%d", projectSlug, req.Number), false, nil } short := strings.ReplaceAll(sessionID.String(), "-", "") if len(short) > 8 { short = short[:8] } return fmt.Sprintf("acp-auto/%s", short), false, nil } // Create / Get / List / Terminate 在 F3 实现;先放占位让接口断言通过。 func (s *sessionService) Create(ctx context.Context, c Caller, in CreateSessionInput) (*Session, error) { return nil, errs.New(errs.CodeInternal, "SessionService.Create not implemented (F3)") } func (s *sessionService) Get(ctx context.Context, c Caller, id uuid.UUID) (*Session, error) { return nil, errs.New(errs.CodeInternal, "SessionService.Get not implemented (F3)") } func (s *sessionService) List(ctx context.Context, c Caller, all bool) ([]*Session, error) { return nil, errs.New(errs.CodeInternal, "SessionService.List not implemented (F3)") } func (s *sessionService) Terminate(ctx context.Context, c Caller, id uuid.UUID) error { return errs.New(errs.CodeInternal, "SessionService.Terminate not implemented (F3)") }