Files
2026-06-22 08:55:57 +08:00

229 lines
7.4 KiB
Go

// 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,
}
}