// mcp_bridge.go 实现 mcp.ACPBridge:把 mcp 的 acp-free DTO 转译到本包 // service 调用。放在 acp 包内(acp 已 import mcp,转译逻辑贴近领域类型), // 避免 mcp → acp 的循环依赖。 package acp import ( "context" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/mcp" ) // MCPBridge 适配 AgentKind/Session/Permission 三个服务到 mcp.ACPBridge。 // repo 仅用于只读的回合列表(ListSessionTurns)——scope 校验先经 sessions.Get 完成。 type MCPBridge struct { kinds AgentKindService sessions SessionService perms *PermissionService repo Repository } // NewMCPBridge 构造桥接适配器,由 app.go 装配 mcp.ServerDeps 时调用。 func NewMCPBridge(kinds AgentKindService, sessions SessionService, perms *PermissionService, repo Repository) *MCPBridge { return &MCPBridge{kinds: kinds, sessions: sessions, perms: perms, repo: repo} } var _ mcp.ACPBridge = (*MCPBridge)(nil) func (b *MCPBridge) ListAgentKinds(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]mcp.ACPAgentKind, error) { ks, err := b.kinds.List(ctx, Caller{UserID: userID, IsAdmin: isAdmin}) if err != nil { return nil, err } out := make([]mcp.ACPAgentKind, 0, len(ks)) for _, k := range ks { out = append(out, agentKindToMCP(k)) } return out, nil } func (b *MCPBridge) GetAgentKind(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*mcp.ACPAgentKind, error) { k, err := b.kinds.Get(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, id) if err != nil { return nil, err } out := agentKindToMCP(k) return &out, nil } func (b *MCPBridge) CreateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, in mcp.ACPCreateSessionInput) (*mcp.ACPSession, error) { s, err := b.sessions.Create(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, CreateSessionInput{ WorkspaceID: in.WorkspaceID, AgentKindID: in.AgentKindID, Branch: in.Branch, IssueNumber: in.IssueNumber, RequirementNumber: in.RequirementNumber, InitialPrompt: in.InitialPrompt, }) if err != nil { return nil, err } out := sessionToMCP(s) return &out, nil } func (b *MCPBridge) GetSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*mcp.ACPSession, error) { s, err := b.sessions.Get(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, id) if err != nil { return nil, err } out := sessionToMCP(s) return &out, nil } func (b *MCPBridge) ListSessions(ctx context.Context, userID uuid.UUID, isAdmin bool, f mcp.ACPSessionFilter) ([]mcp.ACPSession, error) { ss, err := b.sessions.List(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, SessionListFilter{ All: f.All, RequirementID: f.RequirementID, }) if err != nil { return nil, err } out := make([]mcp.ACPSession, 0, len(ss)) for _, s := range ss { out = append(out, sessionToMCP(s)) } return out, nil } func (b *MCPBridge) TerminateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) error { return b.sessions.Terminate(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, id) } func (b *MCPBridge) ListPendingPermissions(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]mcp.ACPPermission, error) { ps, err := b.perms.ListPendingBySession(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, sessionID) if err != nil { return nil, err } out := make([]mcp.ACPPermission, 0, len(ps)) for _, p := range ps { out = append(out, mcp.ACPPermission{ ID: p.ID, SessionID: p.SessionID, AgentRequestID: p.AgentRequestID, ToolName: p.ToolName, Status: string(p.Status), CreatedAt: p.CreatedAt, }) } return out, nil } // ListPendingApprovals 返回调用者跨所有会话的待审权限请求(HITL inbox,read-only)。 func (b *MCPBridge) ListPendingApprovals(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]mcp.ACPPermission, error) { ps, err := b.perms.ListPendingForUser(ctx, Caller{UserID: userID, IsAdmin: isAdmin}) if err != nil { return nil, err } out := make([]mcp.ACPPermission, 0, len(ps)) for _, p := range ps { out = append(out, mcp.ACPPermission{ ID: p.ID, SessionID: p.SessionID, AgentRequestID: p.AgentRequestID, ToolName: p.ToolName, Status: string(p.Status), CreatedAt: p.CreatedAt, }) } return out, nil } // ListSessionTurns 返回会话的回合列表(只读)。先经 sessions.Get 做用户 + 项目 // scope 校验(与 GetSession 同语义),再用 repo 读 acp_turns。 func (b *MCPBridge) ListSessionTurns(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]mcp.ACPTurn, error) { if _, err := b.sessions.Get(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, sessionID); err != nil { return nil, err } ts, err := b.repo.ListTurnsBySession(ctx, sessionID) if err != nil { return nil, err } out := make([]mcp.ACPTurn, 0, len(ts)) for _, t := range ts { out = append(out, mcp.ACPTurn{ TurnIndex: t.TurnIndex, Status: string(t.Status), StopReason: t.StopReason, UpdateCount: t.UpdateCount, StartedAt: t.StartedAt, CompletedAt: t.CompletedAt, }) } return out, nil } // GetRunDashboard 返回 run-history 汇总(owner scope;admin 全量,read-only)。 func (b *MCPBridge) GetRunDashboard(ctx context.Context, userID uuid.UUID, isAdmin bool, in mcp.ACPRunDashboardInput) (*mcp.ACPRunDashboard, error) { res, err := b.sessions.Dashboard(ctx, Caller{UserID: userID, IsAdmin: isAdmin}, DashboardInput{ ProjectID: in.ProjectID, RequirementID: in.RequirementID, From: in.From, To: in.To, }) if err != nil { return nil, err } out := &mcp.ACPRunDashboard{ From: res.From, To: res.To, Total: res.Rollup.Total, Succeeded: res.Rollup.Succeeded, Crashed: res.Rollup.Crashed, Active: res.Rollup.Active, SuccessRate: res.Rollup.SuccessRate(), TotalCostUSD: res.Rollup.TotalCostUSD, TotalTokens: res.Rollup.TotalTokens, AvgDurationSeconds: res.Rollup.AvgDurationSeconds, ByDay: make([]mcp.ACPRunDayRow, 0, len(res.ByDay)), } for _, d := range res.ByDay { out.ByDay = append(out.ByDay, mcp.ACPRunDayRow{ Day: d.Day, Total: d.Total, Succeeded: d.Succeeded, Crashed: d.Crashed, TotalCostUSD: d.TotalCostUSD, }) } return out, nil } func agentKindToMCP(k *AgentKind) mcp.ACPAgentKind { return mcp.ACPAgentKind{ ID: k.ID, Name: k.Name, DisplayName: k.DisplayName, Description: k.Description, Enabled: k.Enabled, ToolAllowlist: k.ToolAllowlist, } } func sessionToMCP(s *Session) mcp.ACPSession { return mcp.ACPSession{ ID: s.ID, WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, AgentKindID: s.AgentKindID, UserID: s.UserID, IssueID: s.IssueID, RequirementID: s.RequirementID, Branch: s.Branch, IsMainWorktree: s.IsMainWorktree, Status: string(s.Status), LastError: s.LastError, LastStopReason: s.LastStopReason, StartedAt: s.StartedAt, EndedAt: s.EndedAt, PromptTokens: s.PromptTokens, CompletionTokens: s.CompletionTokens, ThinkingTokens: s.ThinkingTokens, TotalCostUSD: s.TotalCostUSD, BudgetMaxCostUSD: s.BudgetMaxCostUSD, BudgetMaxTokens: s.BudgetMaxTokens, } }