// 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" "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", "file_created" Content string ToolName string ToolStatus string SessionId string StopReason string // 权限请求相关 Permission *PermissionEvent `json:"permission,omitempty"` // 文件创建相关 File *FileRecord `json:"file,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 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: NewFileStore(), }, 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) } 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, []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) } return sessionId, 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.mu.Lock() c.FileStore.files = nil c.FileStore.mu.Unlock() } } // 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{}, }) // 推送 file_created 事件给前端 if c.eventCh != nil { c.eventCh <- Event{Type: "file_created", File: 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 }