// relay.go 实现 ACP 双向 JSON-RPC 中继: // - reader: 子进程 stdout → 解析 JSON-RPC → 落 events → fanout / handler dispatch // - writer: 服务端发起 / WS 客户端透传 → 子进程 stdin // - subscribers: 多 WS tab 共享 reader 的 fanout // // id 命名空间(spec §5.2 / §7.2): // - 服务端发起:int64(atomic 递增) // - 客户端 WS 发起:string(前端自生成) // - inflight 仅存 int64 → response chan;string id 的 response 当 event fanout package acp import ( "encoding/json" "fmt" "log/slog" "sync" "sync/atomic" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/acp/handlers" ) // EventEnvelope 是推到 WS 的事件结构(与落库的 Event 字段同步,但带 ID 数值化方便 JSON)。 type EventEnvelope struct { ID int64 `json:"id"` Direction string `json:"direction"` // 'in' | 'out' Kind string `json:"kind"` // 'request'|'response'|'notification'|'error'|'session_terminated'|'slow_consumer_disconnect' Method string `json:"method,omitempty"` Payload json.RawMessage `json:"payload"` Truncated bool `json:"truncated"` } // Subscriber 是单个 WS tab 的订阅句柄。 type Subscriber struct { ID uuid.UUID UserID uuid.UUID Send chan *EventEnvelope closed atomic.Bool } // Relay 是单 session 的中继器。每个活跃 session 一个 Relay 实例。 type Relay struct { sessionID uuid.UUID // 协议层 enc *Encoder dec *Decoder // 服务端发起请求等响应(id=int64) inflight sync.Map // int64 → chan *Message nextID atomic.Int64 // WS subscribers(多 tab fanout) subsMu sync.Mutex subs map[uuid.UUID]*Subscriber // 写入路径 serverInitiated chan *Message // 服务端发起的 request/notification clientInitiated chan *Message // WS 上行(已校验 method 白名单) // 依赖 repo Repository fsHandler *handlers.FsHandler permHandler *handlers.PermissionHandler log *slog.Logger cfg RelayConfig // 终止控制 closed atomic.Bool done chan struct{} } // RelayConfig 影响事件入库截断行为。 type RelayConfig struct { EventMaxPayload int EventTruncateField int WSSendBuffer int } // NewRelay 构造一个空 Relay;启动 goroutine 走 Run(D7 实现)。 func NewRelay(sessionID uuid.UUID, dec *Decoder, enc *Encoder, repo Repository, fs *handlers.FsHandler, perm *handlers.PermissionHandler, cfg RelayConfig, log *slog.Logger) *Relay { if log == nil { log = slog.Default() } return &Relay{ sessionID: sessionID, enc: enc, dec: dec, subs: map[uuid.UUID]*Subscriber{}, serverInitiated: make(chan *Message, 32), clientInitiated: make(chan *Message, 32), repo: repo, fsHandler: fs, permHandler: perm, log: log, cfg: cfg, done: make(chan struct{}), } } // Subscribe 注册一个 WS tab 订阅。 func (r *Relay) Subscribe(userID uuid.UUID) *Subscriber { sub := &Subscriber{ ID: uuid.New(), UserID: userID, Send: make(chan *EventEnvelope, r.cfg.WSSendBuffer), } r.subsMu.Lock() r.subs[sub.ID] = sub r.subsMu.Unlock() return sub } // Unsubscribe 移除订阅;幂等。 func (r *Relay) Unsubscribe(subID uuid.UUID) { r.subsMu.Lock() defer r.subsMu.Unlock() if sub, ok := r.subs[subID]; ok { if sub.closed.CompareAndSwap(false, true) { close(sub.Send) } delete(r.subs, subID) } } // fanout 把一条 envelope 推给所有 subscribers。慢消费者按 §7.6 自动断开。 func (r *Relay) fanout(env *EventEnvelope) { r.subsMu.Lock() defer r.subsMu.Unlock() for id, sub := range r.subs { select { case sub.Send <- env: default: // buffer 满 → 慢消费者,标 closed + 通知 + close channel if sub.closed.CompareAndSwap(false, true) { disc := &EventEnvelope{Kind: "slow_consumer_disconnect"} select { case sub.Send <- disc: default: } close(sub.Send) delete(r.subs, id) r.log.Warn("acp.relay.slow_subscriber_dropped", "session_id", r.sessionID, "sub_id", id) } } } } // Close 优雅关闭:先广播 final event,再关 channel,再标 closed。 // supervisor.onExit 调用,确保所有 WS 看到 session_terminated。 func (r *Relay) Close(status string, exitCode *int32) { if !r.closed.CompareAndSwap(false, true) { return } final := &EventEnvelope{ Kind: "session_terminated", Payload: mustJSON(map[string]any{ "status": status, "exit_code": exitCode, }), } r.fanout(final) r.subsMu.Lock() for id, sub := range r.subs { if sub.closed.CompareAndSwap(false, true) { close(sub.Send) } delete(r.subs, id) } r.subsMu.Unlock() close(r.done) } // Done returns a channel closed when Relay is terminated. func (r *Relay) Done() <-chan struct{} { return r.done } func mustJSON(v any) json.RawMessage { b, err := json.Marshal(v) if err != nil { return json.RawMessage(fmt.Sprintf(`{"_marshal_err":%q}`, err.Error())) } return b }