| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997 |
- // Package acp 实现 ACP (Agent Client Protocol) 客户端
- // 基于 ACP v1 规范,通过 stdin/stdout ND-JSON 与 Agent 子进程通讯
- package acp
- import (
- "bufio"
- "encoding/json"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "sync"
- "sync/atomic"
- "fwh-toolbox-server/utils/filestore"
- "go.uber.org/zap"
- )
- // ============ JSON-RPC 2.0 基础类型 ============
- // Request JSON-RPC 请求
- type Request struct {
- JSONRPC string `json:"jsonrpc"`
- ID int64 `json:"id"`
- Method string `json:"method"`
- Params any `json:"params,omitempty"`
- }
- // Response JSON-RPC 响应
- type Response struct {
- JSONRPC string `json:"jsonrpc"`
- ID *int64 `json:"id,omitempty"`
- Method string `json:"method,omitempty"`
- Result json.RawMessage `json:"result,omitempty"`
- Error *RPCError `json:"error,omitempty"`
- Params json.RawMessage `json:"params,omitempty"`
- }
- // RPCError JSON-RPC 错误
- type RPCError struct {
- Code int `json:"code"`
- Message string `json:"message"`
- }
- // ============ ACP 协议类型 ============
- // InitializeParams 初始化请求参数
- type InitializeParams struct {
- ProtocolVersion int `json:"protocolVersion"`
- ClientInfo ClientInfo `json:"clientInfo"`
- ClientCapabilities ClientCapabilities `json:"clientCapabilities"`
- }
- // ClientInfo 客户端信息
- type ClientInfo struct {
- Name string `json:"name"`
- Version string `json:"version"`
- }
- // ClientCapabilities 客户端能力声明
- type ClientCapabilities struct {
- FS *FSCapabilities `json:"fs,omitempty"`
- Terminal bool `json:"terminal,omitempty"`
- }
- // FSCapabilities 文件系统能力
- type FSCapabilities struct {
- ReadTextFile bool `json:"readTextFile"`
- WriteTextFile bool `json:"writeTextFile"`
- }
- // NewSessionParams 创建会话参数
- type NewSessionParams struct {
- CWD string `json:"cwd"`
- MCPServers []any `json:"mcpServers"`
- }
- // NewSessionResult 创建会话响应
- type NewSessionResult struct {
- SessionId string `json:"sessionId"`
- Modes *SessionModes `json:"modes,omitempty"`
- }
- // SessionModes 会话模式
- type SessionModes struct {
- CurrentModeId string `json:"currentModeId"`
- AvailableModes []SessionMode `json:"availableModes"`
- }
- // SessionMode 单个模式
- type SessionMode struct {
- Id string `json:"id"`
- Name string `json:"name"`
- Description string `json:"description"`
- Meta *SessionMeta `json:"_meta,omitempty"`
- }
- // SessionMeta 模式元数据
- type SessionMeta struct {
- WelcomeMessage string `json:"welcomeMessage"`
- }
- // PromptParams 发送消息参数
- type PromptParams struct {
- SessionId string `json:"sessionId"`
- Prompt []ContentBlock `json:"prompt"`
- }
- // ContentBlock 内容块
- type ContentBlock struct {
- Type string `json:"type"`
- Text string `json:"text"`
- }
- // PromptResult prompt 响应
- type PromptResult struct {
- StopReason string `json:"stopReason"`
- }
- // CancelParams 取消参数
- type CancelParams struct {
- SessionId string `json:"sessionId"`
- }
- // ============ Session Update 事件类型 ============
- // SessionUpdate 会话更新通知
- type SessionUpdate struct {
- SessionId string `json:"sessionId"`
- Update json.RawMessage `json:"update"`
- }
- // UpdateBase 更新基础类型
- type UpdateBase struct {
- SessionUpdate string `json:"sessionUpdate"`
- }
- // AgentMessageChunk Agent 文本流式输出
- type AgentMessageChunk struct {
- SessionUpdate string `json:"sessionUpdate"` // "agent_message_chunk"
- MessageId string `json:"messageId,omitempty"`
- Content ContentBlock `json:"content"`
- }
- // ToolCall 工具调用通知
- type ToolCall struct {
- SessionUpdate string `json:"sessionUpdate"` // "tool_call"
- ToolCallId string `json:"toolCallId"`
- Title string `json:"title"`
- Status string `json:"status"` // pending, running, completed, failed
- }
- // ToolCallUpdate 工具调用状态更新
- type ToolCallUpdate struct {
- SessionUpdate string `json:"sessionUpdate"` // "tool_call_update"
- ToolCallId string `json:"toolCallId"`
- Title string `json:"title,omitempty"`
- Status string `json:"status"`
- }
- // ============ 客户端事件(对外输出) ============
- // Event 输出给上层的事件
- type Event struct {
- Type string // "text", "tool_call", "tool_update", "done", "error", "permission"
- Content string
- ToolName string
- ToolStatus string
- SessionId string
- StopReason string
- // 权限请求相关
- Permission *PermissionEvent `json:"permission,omitempty"`
- }
- // PermissionEvent 权限请求事件(推给前端)
- type PermissionEvent struct {
- RequestId string `json:"requestId"` // 用于前端回传
- Title string `json:"title"` // 权限标题
- Options []PermissionOption `json:"options"` // 可选选项
- }
- // PermissionOption 权限选项
- type PermissionOption struct {
- OptionId string `json:"optionId"`
- Name string `json:"name"`
- Kind string `json:"kind"` // allow_once, allow_always, deny
- }
- // ============ ACP Client 实现 ============
- // Client ACP 客户端(管理单个 Agent 进程)
- type Client struct {
- cmd *exec.Cmd
- stdin io.WriteCloser
- scanner *bufio.Scanner
- sessionId string
- requestId int64
- mu sync.Mutex // 保护 stdin 写入
- readMu sync.Mutex // 保护 stdout 读取
- // 权限请求等待机制
- permissionCh chan string // 接收前端选择的 optionId
- eventCh chan<- Event // 当前 prompt 的事件通道(用于推送 permission 事件)
- // 文件存储
- FileStore *filestore.FileStore
- UserId string
- // 文本累积(用于合并日志输出)
- textBuffer string
- }
- // ClientConfig 客户端配置
- type ClientConfig struct {
- Command string // Agent 命令(如 "kiro-cli")
- Args []string // 命令参数(如 ["acp"])
- WorkingDir string // 工作目录
- Env []string // 额外环境变量(如 "HOME=/home/user")
- }
- // NewClient 创建并启动 ACP Client
- func NewClient(cfg ClientConfig) (*Client, error) {
- cmd := exec.Command(cfg.Command, cfg.Args...)
- // 确保工作目录是绝对路径
- absDir, err := filepath.Abs(cfg.WorkingDir)
- if err != nil {
- absDir = cfg.WorkingDir
- }
- cmd.Dir = absDir
- // 设置环境变量
- cmd.Env = append(os.Environ(), cfg.Env...)
- stdin, err := cmd.StdinPipe()
- if err != nil {
- return nil, fmt.Errorf("创建 stdin pipe 失败: %w", err)
- }
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return nil, fmt.Errorf("创建 stdout pipe 失败: %w", err)
- }
- // 捕获 stderr
- stderrPipe, _ := cmd.StderrPipe()
- go func() {
- if stderrPipe != nil {
- s := bufio.NewScanner(stderrPipe)
- for s.Scan() {
- zap.L().Warn("[ACP] agent stderr", zap.String("line", s.Text()))
- }
- }
- }()
- if err := cmd.Start(); err != nil {
- return nil, fmt.Errorf("启动 Agent 进程失败: %w", err)
- }
- scanner := bufio.NewScanner(stdout)
- scanner.Buffer(make([]byte, 0, 2*1024*1024), 2*1024*1024) // 2MB buffer
- return &Client{
- cmd: cmd,
- stdin: stdin,
- scanner: scanner,
- permissionCh: make(chan string, 1),
- FileStore: filestore.NewFileStore("upload"),
- }, nil
- }
- // ResolvePermission 前端回传权限选择结果
- func (c *Client) ResolvePermission(optionId string) {
- select {
- case c.permissionCh <- optionId:
- default:
- // 没有等待中的权限请求,忽略
- }
- }
- // Initialize 执行初始化握手
- func (c *Client) Initialize() error {
- params := InitializeParams{
- ProtocolVersion: 1,
- ClientInfo: ClientInfo{
- Name: "fwh-toolbox",
- Version: "1.0.0",
- },
- ClientCapabilities: ClientCapabilities{
- FS: &FSCapabilities{
- ReadTextFile: true,
- WriteTextFile: true,
- },
- Terminal: false, // 不声明终端能力,强制 Agent 使用 writeTextFile
- },
- }
- _, err := c.sendAndWait("initialize", params)
- return err
- }
- // NewSession 创建新会话,返回 sessionId 和 welcomeMessage
- func (c *Client) NewSession(cwd string) (string, string, error) {
- params := NewSessionParams{
- CWD: cwd,
- MCPServers: []any{},
- }
- result, err := c.sendAndWait("session/new", params)
- if err != nil {
- return "", "", err
- }
- var res NewSessionResult
- if err := json.Unmarshal(result, &res); err != nil {
- return "", "", fmt.Errorf("解析 session/new 响应失败: %w", err)
- }
- zap.L().Info("session/new", zap.String("result", string(result)))
- c.sessionId = res.SessionId
- // 提取当前 mode 的 welcomeMessage
- var welcomeMsg string
- if res.Modes != nil {
- for _, mode := range res.Modes.AvailableModes {
- if mode.Id == res.Modes.CurrentModeId && mode.Meta != nil {
- welcomeMsg = mode.Meta.WelcomeMessage
- break
- }
- }
- }
- return res.SessionId, welcomeMsg, nil
- }
- // ReplayMessage 回放的单条消息
- type ReplayMessage struct {
- Role string `json:"role"` // "user" 或 "assistant"
- Content string `json:"content"`
- MessageId string `json:"messageId,omitempty"`
- }
- // LoadSession 加载已有会话(恢复历史对话上下文),返回回放的消息列表
- func (c *Client) LoadSession(sessionId string, cwd string) (string, string, []ReplayMessage, error) {
- params := map[string]any{
- "sessionId": sessionId,
- "cwd": cwd,
- "mcpServers": []any{},
- }
- // 发送 session/load 请求
- id := c.sendRequest("session/load", params)
- // 收集回放消息
- var replayMessages []ReplayMessage
- currentMsg := &ReplayMessage{}
- // 读取回放事件直到收到 session/load 的 response
- for c.scanner.Scan() {
- line := c.scanner.Text()
- if line == "" {
- continue
- }
- var msg struct {
- JSONRPC string `json:"jsonrpc"`
- ID json.RawMessage `json:"id,omitempty"`
- Method string `json:"method,omitempty"`
- Result json.RawMessage `json:"result,omitempty"`
- Error *RPCError `json:"error,omitempty"`
- Params json.RawMessage `json:"params,omitempty"`
- }
- if err := json.Unmarshal([]byte(line), &msg); err != nil {
- continue
- }
- // 检查是否是 session/load 的响应
- hasId := len(msg.ID) > 0 && string(msg.ID) != "null"
- if hasId && msg.Method == "" {
- var numId int64
- json.Unmarshal(msg.ID, &numId)
- if numId == id {
- // session/load 完成
- if msg.Error != nil {
- return "", "", nil, fmt.Errorf("session/load 错误: %s", msg.Error.Message)
- }
- c.sessionId = sessionId
- // 追加最后一条未关闭的消息
- if currentMsg.Content != "" {
- replayMessages = append(replayMessages, *currentMsg)
- }
- // 解析响应中的 welcomeMessage
- var welcomeMsg string
- if len(msg.Result) > 0 {
- var res NewSessionResult
- if err := json.Unmarshal(msg.Result, &res); err == nil && res.Modes != nil {
- for _, mode := range res.Modes.AvailableModes {
- if mode.Id == res.Modes.CurrentModeId && mode.Meta != nil {
- welcomeMsg = mode.Meta.WelcomeMessage
- break
- }
- }
- }
- }
- return sessionId, welcomeMsg, replayMessages, nil
- }
- }
- // 处理回放的 session/update 通知
- if msg.Method == "session/update" || msg.Method == "_kiro.dev/session/update" {
- var notif SessionUpdate
- if err := json.Unmarshal(msg.Params, ¬if); err != nil {
- continue
- }
- var base UpdateBase
- if err := json.Unmarshal(notif.Update, &base); err != nil {
- continue
- }
- switch base.SessionUpdate {
- case "user_message_chunk":
- // 新的用户消息 → 保存上一条并开始新消息
- if currentMsg.Content != "" {
- replayMessages = append(replayMessages, *currentMsg)
- }
- var chunk AgentMessageChunk
- json.Unmarshal(notif.Update, &chunk)
- currentMsg = &ReplayMessage{Role: "user", Content: chunk.Content.Text, MessageId: chunk.MessageId}
- case "agent_message_chunk":
- // AI 消息
- var chunk AgentMessageChunk
- json.Unmarshal(notif.Update, &chunk)
- if currentMsg.Role != "assistant" || (chunk.MessageId != "" && chunk.MessageId != currentMsg.MessageId) {
- // 新的 AI 消息 → 保存上一条
- if currentMsg.Content != "" {
- replayMessages = append(replayMessages, *currentMsg)
- }
- currentMsg = &ReplayMessage{Role: "assistant", Content: chunk.Content.Text, MessageId: chunk.MessageId}
- } else {
- // 同一条消息的续写
- currentMsg.Content += chunk.Content.Text
- }
- case "agent_thought_chunk":
- // 思考内容忽略(不显示给用户)
- case "tool_call", "tool_call_update", "tool_call_chunk":
- // 工具调用回放忽略
- default:
- // 其他类型忽略
- }
- continue
- }
- // 忽略其他通知(kiro 扩展等)
- }
- return "", "", nil, fmt.Errorf("session/load 未收到响应(进程可能已退出)")
- }
- // Prompt 发送消息并流式读取事件
- // eventCh 会在 turn 结束或出错时关闭
- func (c *Client) Prompt(sessionId string, message string, eventCh chan<- Event) {
- defer close(eventCh)
- // 设置当前事件通道(供 handleAgentRequest 推送 permission 事件)
- c.eventCh = eventCh
- params := PromptParams{
- SessionId: sessionId,
- Prompt: []ContentBlock{
- {Type: "text", Text: message},
- },
- }
- // 发送 prompt 请求
- promptId := c.sendRequest("session/prompt", params)
- zap.L().Debug("[ACP] prompt 已发送", zap.Int64("promptId", promptId), zap.String("sessionId", sessionId))
- // 持续读取直到收到 prompt 的 response
- for c.scanner.Scan() {
- line := c.scanner.Text()
- if line == "" {
- continue
- }
- // 用宽松结构解析,兼容所有消息格式
- var msg struct {
- JSONRPC string `json:"jsonrpc"`
- ID json.RawMessage `json:"id,omitempty"`
- Method string `json:"method,omitempty"`
- Result json.RawMessage `json:"result,omitempty"`
- Error *RPCError `json:"error,omitempty"`
- Params json.RawMessage `json:"params,omitempty"`
- }
- if err := json.Unmarshal([]byte(line), &msg); err != nil {
- zap.L().Warn("[ACP] JSON解析失败,跳过", zap.String("line", line[:min(len(line), 100)]))
- continue
- }
- // 解析 ID(兼容 int64 和 string)
- hasId := len(msg.ID) > 0 && string(msg.ID) != "null"
- var numericId int64
- isPromptResp := false
- if hasId {
- json.Unmarshal(msg.ID, &numericId)
- isPromptResp = numericId == promptId && msg.Method == ""
- }
- // 收到 prompt 的响应 → turn 结束
- if isPromptResp {
- zap.L().Debug("[ACP] 收到 prompt 响应(turn结束)", zap.Int64("id", numericId))
- // 打印本轮完整回复
- if c.textBuffer != "" {
- zap.L().Info("[ACP] AI 回复完整内容", zap.String("text", c.textBuffer))
- c.textBuffer = ""
- }
- var result PromptResult
- if msg.Error != nil {
- eventCh <- Event{Type: "error", Content: msg.Error.Message}
- } else if err := json.Unmarshal(msg.Result, &result); err == nil {
- eventCh <- Event{Type: "done", SessionId: sessionId, StopReason: result.StopReason}
- }
- return
- }
- // 处理 notification (session/update 或 _kiro.dev/session/update)
- if !hasId && (msg.Method == "session/update" || msg.Method == "_kiro.dev/session/update") {
- c.handleSessionUpdate(msg.Params, eventCh)
- continue
- }
- // 处理 Agent→Client 反向请求(有 ID + Method = 请求)
- if hasId && msg.Method != "" {
- zap.L().Debug("[ACP] 收到 Agent 反向请求", zap.String("method", msg.Method), zap.String("id", string(msg.ID)))
- c.handleAgentRequest(msg.ID, msg.Method, msg.Params)
- continue
- }
- // 未匹配的消息(kiro扩展通知等)
- if msg.Method != "" {
- c.handleExtensionNotification(msg.Method, msg.Params, eventCh)
- } else {
- zap.L().Debug("[ACP] 未处理的消息", zap.String("line", line[:min(len(line), 200)]))
- }
- }
- // Scanner 结束说明进程退出
- eventCh <- Event{Type: "error", Content: "Agent 进程已断开"}
- }
- // Cancel 取消当前操作
- func (c *Client) Cancel(sessionId string) {
- c.sendRequest("session/cancel", CancelParams{SessionId: sessionId})
- }
- // GetSessionId 获取当前会话 ID
- func (c *Client) GetSessionId() string {
- return c.sessionId
- }
- // Close 关闭 ACP Client,终止 Agent 进程
- func (c *Client) Close() {
- // 关闭 stdin 通知 Agent 退出
- c.stdin.Close()
- // 终止进程
- if c.cmd.Process != nil {
- c.cmd.Process.Kill()
- // Wait 回收子进程资源,防止僵尸进程
- go c.cmd.Wait()
- }
- // 解除可能阻塞在 permissionCh 上的 goroutine
- select {
- case c.permissionCh <- "deny":
- default:
- }
- // 清理 FileStore
- if c.FileStore != nil {
- c.FileStore.Clear()
- }
- }
- // IsAlive 检查 Agent 进程是否存活
- func (c *Client) IsAlive() bool {
- return c.cmd.ProcessState == nil
- }
- // ============ 内部方法 ============
- // sendRequest 发送请求(不等待响应),返回 request ID
- func (c *Client) sendRequest(method string, params any) int64 {
- c.mu.Lock()
- defer c.mu.Unlock()
- id := atomic.AddInt64(&c.requestId, 1)
- req := Request{
- JSONRPC: "2.0",
- ID: id,
- Method: method,
- Params: params,
- }
- data, err := json.Marshal(req)
- if err != nil {
- zap.L().Error("[ACP] 序列化请求失败", zap.Error(err))
- return id
- }
- data = append(data, '\n')
- if _, err := c.stdin.Write(data); err != nil {
- zap.L().Error("[ACP] 写入 stdin 失败", zap.String("method", method), zap.Error(err))
- }
- return id
- }
- // sendAndWait 发送请求并等待对应 ID 的响应
- func (c *Client) sendAndWait(method string, params any) (json.RawMessage, error) {
- id := c.sendRequest(method, params)
- // 读取响应(跳过 notification 和其他消息)
- for c.scanner.Scan() {
- line := c.scanner.Text()
- if line == "" {
- continue
- }
- var resp Response
- if err := json.Unmarshal([]byte(line), &resp); err != nil {
- continue
- }
- // 匹配 ID 的响应
- if resp.ID != nil && *resp.ID == id {
- if resp.Error != nil {
- return nil, fmt.Errorf("ACP 错误 [%d]: %s", resp.Error.Code, resp.Error.Message)
- }
- return resp.Result, nil
- }
- // 跳过 notification(session/new 之前可能收到 kiro 扩展通知)
- }
- return nil, fmt.Errorf("未收到 %s 的响应(进程可能已退出)", method)
- }
- // handleSessionUpdate 解析并分发 session/update 通知
- func (c *Client) handleSessionUpdate(params json.RawMessage, eventCh chan<- Event) {
- var notif SessionUpdate
- if err := json.Unmarshal(params, ¬if); err != nil {
- return
- }
- // 解析 update 的类型
- var base UpdateBase
- if err := json.Unmarshal(notif.Update, &base); err != nil {
- return
- }
- // zap.L().Debug("[ACP] session/update", zap.String("type", base.SessionUpdate))
- switch base.SessionUpdate {
- case "agent_message_chunk":
- var chunk AgentMessageChunk
- if err := json.Unmarshal(notif.Update, &chunk); err == nil {
- c.textBuffer += chunk.Content.Text
- eventCh <- Event{Type: "text", Content: chunk.Content.Text}
- }
- case "agent_thought_chunk":
- var chunk AgentMessageChunk
- if err := json.Unmarshal(notif.Update, &chunk); err == nil {
- c.textBuffer += chunk.Content.Text
- eventCh <- Event{Type: "text", Content: chunk.Content.Text}
- }
- case "tool_call":
- var tc ToolCall
- if err := json.Unmarshal(notif.Update, &tc); err == nil {
- zap.L().Debug("[ACP] → tool_call", zap.String("title", tc.Title), zap.String("status", tc.Status), zap.String("id", tc.ToolCallId))
- eventCh <- Event{Type: "tool_call", ToolName: tc.Title, ToolStatus: tc.Status}
- }
- case "tool_call_update":
- var tcu ToolCallUpdate
- if err := json.Unmarshal(notif.Update, &tcu); err == nil {
- zap.L().Debug("[ACP] → tool_call_update", zap.String("title", tcu.Title), zap.String("status", tcu.Status), zap.String("id", tcu.ToolCallId))
- eventCh <- Event{Type: "tool_update", ToolName: tcu.Title, ToolStatus: tcu.Status}
- }
- case "tool_call_chunk":
- // 工具调用的流式参数输出(Kiro 扩展)
- zap.L().Debug("[ACP] → tool_call_chunk (ignored)")
- case "plan":
- zap.L().Debug("[ACP] → plan update")
- default:
- zap.L().Debug("[ACP] → 未知 session/update 类型", zap.String("type", base.SessionUpdate))
- }
- }
- // handleExtensionNotification 处理 Kiro 扩展通知
- func (c *Client) handleExtensionNotification(method string, params json.RawMessage, eventCh chan<- Event) {
- switch method {
- case "_kiro.dev/commands/available":
- // 可用命令和工具列表
- var data struct {
- SessionId string `json:"sessionId"`
- Commands []struct {
- Name string `json:"name"`
- Description string `json:"description"`
- } `json:"commands"`
- Tools []struct {
- Name string `json:"name"`
- Description string `json:"description"`
- Source string `json:"source"`
- } `json:"tools"`
- }
- if err := json.Unmarshal(params, &data); err == nil {
- zap.L().Debug("[ACP] 可用命令/工具更新",
- zap.Int("commands", len(data.Commands)),
- zap.Int("tools", len(data.Tools)),
- )
- // 推送工具列表给前端(可选)
- if len(data.Tools) > 0 {
- toolNames := make([]string, 0, len(data.Tools))
- for _, t := range data.Tools {
- toolNames = append(toolNames, t.Name)
- }
- eventCh <- Event{Type: "tools_available", Content: toJSONStr(toolNames)}
- }
- }
- case "_kiro.dev/metadata":
- // 上下文使用量和耗时
- var data struct {
- SessionId string `json:"sessionId"`
- ContextUsagePercentage float64 `json:"contextUsagePercentage"`
- TurnDurationMs int64 `json:"turnDurationMs,omitempty"`
- }
- if err := json.Unmarshal(params, &data); err == nil {
- if data.TurnDurationMs > 0 {
- zap.L().Debug("[ACP] metadata",
- zap.Float64("contextUsage%", data.ContextUsagePercentage),
- zap.Int64("turnMs", data.TurnDurationMs),
- )
- eventCh <- Event{
- Type: "metadata",
- Content: toJSONStr(map[string]any{
- "contextUsagePercent": data.ContextUsagePercentage,
- "turnDurationMs": data.TurnDurationMs,
- }),
- }
- }
- }
- case "_kiro.dev/subagent/list_update":
- // 子代理列表更新
- zap.L().Debug("[ACP] 子代理列表更新")
- case "_kiro.dev/mcp/server_initialized":
- // MCP Server 初始化完成
- var data struct {
- ServerName string `json:"serverName"`
- }
- json.Unmarshal(params, &data)
- zap.L().Info("[ACP] MCP Server 初始化完成", zap.String("server", data.ServerName))
- case "_kiro.dev/compaction/status":
- // 上下文压缩状态
- zap.L().Debug("[ACP] 上下文压缩中")
- case "_kiro.dev/clear/status":
- // 会话清除状态
- zap.L().Debug("[ACP] 会话清除中")
- default:
- zap.L().Debug("[ACP] 未处理的扩展通知", zap.String("method", method))
- }
- }
- // toJSONStr 序列化为 JSON 字符串
- func toJSONStr(v any) string {
- data, _ := json.Marshal(v)
- return string(data)
- }
- // handleAgentRequest 处理 Agent→Client 的反向请求
- func (c *Client) handleAgentRequest(rawId json.RawMessage, method string, rawParams json.RawMessage) {
- c.mu.Lock()
- defer c.mu.Unlock()
- zap.L().Debug("[ACP] 处理反向请求", zap.String("method", method), zap.String("id", string(rawId)), zap.String("params", string(rawParams[:min(len(rawParams), 200)])))
- var respData []byte
- switch method {
- case "readTextFile":
- // 读取文件:先从 FileStore 查找(MinIO),再从本地文件系统
- var params struct {
- Path string `json:"path"`
- Line *int `json:"line,omitempty"`
- Limit *int `json:"limit,omitempty"`
- }
- if err := json.Unmarshal(rawParams, ¶ms); err == nil && params.Path != "" {
- var content string
- var readErr error
- // 优先从 FileStore(MinIO)读取
- if c.FileStore.HasFile(params.Path) {
- content, readErr = c.FileStore.ReadFile(params.Path)
- zap.L().Debug("[ACP] readTextFile 从 MinIO 读取", zap.String("path", params.Path))
- } else {
- // 回退到本地文件系统
- data, err := os.ReadFile(params.Path)
- if err == nil {
- content = string(data)
- zap.L().Debug("[ACP] readTextFile 从本地读取", zap.String("path", params.Path))
- } else {
- readErr = err
- }
- }
- if readErr == nil {
- result, _ := json.Marshal(map[string]string{"content": content})
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "result": json.RawMessage(result),
- })
- }
- }
- if respData == nil {
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "error": map[string]any{"code": -1, "message": "file not found"},
- })
- }
- case "writeTextFile":
- // 写入文件到 MinIO(不写本地)
- var params struct {
- Path string `json:"path"`
- Content string `json:"content"`
- }
- if err := json.Unmarshal(rawParams, ¶ms); err == nil && params.Path != "" {
- record, err := c.FileStore.WriteFile(c.UserId, c.sessionId, params.Path, params.Content)
- if err == nil {
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "result": map[string]any{},
- })
- _ = record // 文件下载通过前端标记检测实现,不再推送事件
- } else {
- zap.L().Error("[ACP] writeTextFile MinIO上传失败", zap.Error(err))
- }
- }
- if respData == nil {
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "error": map[string]any{"code": -1, "message": "write failed"},
- })
- }
- case "requestPermission", "session/request_permission":
- // 解析权限请求参数
- var params struct {
- ToolCall *struct {
- Title string `json:"title"`
- } `json:"toolCall"`
- Options []struct {
- OptionId string `json:"optionId"`
- Name string `json:"name"`
- Kind string `json:"kind"`
- } `json:"options"`
- }
- json.Unmarshal(rawParams, ¶ms)
- title := ""
- if params.ToolCall != nil {
- title = params.ToolCall.Title
- }
- // 判断是否涉及写操作(需要手动授权)
- isWriteOp := true
- // isWriteOp := false
- // if title != "" {
- // for _, keyword := range []string{"write", "Write", "create", "Create", "delete", "Delete", "修改", "写入", "创建", "删除"} {
- // if len(title) >= len(keyword) && containsStr(title, keyword) {
- // isWriteOp = true
- // break
- // }
- // }
- // }
- if isWriteOp && c.eventCh != nil {
- // 写操作:推送权限请求给前端,等待前端回复
- options := make([]PermissionOption, 0, len(params.Options))
- for _, opt := range params.Options {
- options = append(options, PermissionOption{
- OptionId: opt.OptionId,
- Name: opt.Name,
- Kind: opt.Kind,
- })
- }
- requestId := fmt.Sprintf("perm_%s", string(rawId))
- c.eventCh <- Event{
- Type: "permission",
- Permission: &PermissionEvent{
- RequestId: requestId,
- Title: title,
- Options: options,
- },
- }
- // 释放 mu 锁后等待前端回复(否则会死锁)
- c.mu.Unlock()
- selectedOptionId := <-c.permissionCh
- c.mu.Lock()
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "result": map[string]any{
- "outcome": map[string]any{
- "outcome": "selected",
- "optionId": selectedOptionId,
- },
- },
- })
- } else {
- // 非写操作:自动允许
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "result": map[string]any{
- "outcome": map[string]any{
- "outcome": "selected",
- "optionId": "allow_once",
- },
- },
- })
- }
- default:
- // 未知请求返回错误
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "error": map[string]any{"code": -32601, "message": "method not supported"},
- })
- }
- if respData != nil {
- zap.L().Debug("[ACP] 发送反向响应", zap.String("method", method), zap.String("resp", string(respData[:min(len(respData), 200)])))
- respData = append(respData, '\n')
- c.stdin.Write(respData)
- } else {
- zap.L().Warn("[ACP] 反向请求无响应数据", zap.String("method", method))
- }
- }
- // containsStr 简单字符串包含检测
- func containsStr(s, substr string) bool {
- return len(s) >= len(substr) && (s == substr || len(s) > 0 && stringContains(s, substr))
- }
- func stringContains(s, substr string) bool {
- for i := 0; i <= len(s)-len(substr); i++ {
- if s[i:i+len(substr)] == substr {
- return true
- }
- }
- return false
- }
- func min(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
|