| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569 |
- // Package acp 实现 ACP (Agent Client Protocol) 客户端
- // 基于 ACP v1 规范,通过 stdin/stdout ND-JSON 与 Agent 子进程通讯
- package acp
- import (
- "bufio"
- "encoding/json"
- "fmt"
- "io"
- "os"
- "os/exec"
- "os/user"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- "syscall"
- "time"
- "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"`
- Data string `json:"data,omitempty"`
- }
- // FullMessage 返回包含 data 的完整错误信息
- func (e *RPCError) FullMessage() string {
- if e.Data != "" {
- return fmt.Sprintf("%s: %s", e.Message, e.Data)
- }
- return e.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"`
- Models *SessionModels `json:"models,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"`
- }
- // SessionModels 会话可用模型
- type SessionModels struct {
- CurrentModelId string `json:"currentModelId"`
- AvailableModels []SessionModel `json:"availableModels"`
- }
- // SessionModel 单个模型
- type SessionModel struct {
- ModelId string `json:"modelId"`
- Name string `json:"name"`
- Description string `json:"description"`
- }
- // 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
- ToolCallId string // 工具调用唯一ID(用于前端按 id 关联状态更新)
- 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 实现 ============
- // PermissionWaitTimeout 前端授权回传的最长等待时长。
- // 超时后默认按 deny 处理,避免读循环因前端断开而永久阻塞。
- const PermissionWaitTimeout = 5 * time.Minute
- // pendingResponse 等待中的请求:读循环收到匹配 id 的响应后通过 ch 投递
- type pendingResponse struct {
- ch chan Response
- }
- // notifySink 通知回调:读循环把 notification 派发给当前阶段的接收者。
- // 返回值无意义,仅用于让不同阶段(Prompt / LoadSession / 默认)自定义处理。
- type notifySink func(method string, params json.RawMessage)
- // Client ACP 客户端(管理单个 Agent 进程)
- //
- // 并发模型:构造时启动唯一的 readLoop goroutine,负责读取 stdout 的每一行并派发:
- // - 响应(有 id、无 method)→ 按 id 投递到 pending map 中等待的 channel;
- // - Agent→Client 反向请求(有 id、有 method)→ 交给 handleAgentRequest(在独立
- // goroutine 中处理,避免权限等待阻塞读循环);
- // - 通知(无 id、有 method)→ 交给当前 notifySink。
- //
- // 所有对 stdin 的写入通过 writeMu 串行化;Cancel 与权限响应因此不会互相死锁。
- type Client struct {
- cmd *exec.Cmd
- stdin io.WriteCloser
- scanner *bufio.Scanner
- sessionId string
- requestId int64
- writeMu sync.Mutex // 保护 stdin 写入
- // 请求分发:id -> 等待响应的 channel
- pendingMu sync.Mutex
- pending map[int64]*pendingResponse
- // 通知分发:读循环把 notification 交给当前 sink
- sinkMu sync.Mutex
- sink notifySink
- eventCh chan<- Event // 当前 prompt 的事件通道(用于推送 permission 事件),受 sinkMu 保护
- abortCh chan struct{} // 当前 prompt 的中止信号(上层停止消费时用于解除发送阻塞),受 sinkMu 保护
- emitWg sync.WaitGroup // 统计在途 emit,endTurn 借此在 close(eventCh) 前等待发送退出
- // 权限请求等待机制
- permissionCh chan string // 接收前端选择的 optionId
- // 进程存活标记:由 readLoop / waitProcess 单点置位,IsAlive 读取
- dead atomic.Bool
- doneCh chan struct{} // 进程退出/读循环结束时关闭
- // 文件存储:会话身份(用于定位 MinIO 中的文件)
- UserId string
- // 文本累积(仅用于在 turn 结束时打一条完整回复日志)。
- // 由 sink(readLoop goroutine)写入、Prompt goroutine 读取,故用 textMu 保护。
- textMu sync.Mutex
- textBuffer string
- // 最近一次上下文用量与耗时(供首屏展示,读取需加 metaMu)
- metaMu sync.Mutex
- lastContextUsage float64
- lastTurnDurationMs int64
- }
- // ClientConfig 客户端配置
- type ClientConfig struct {
- Command string // Agent 命令(如 "kiro-cli")
- Args []string // 命令参数(如 ["acp"])
- WorkingDir string // 工作目录
- Env []string // 额外环境变量(如 "HOME=/home/user")
- RunAsUser string // 降权运行的目标系统用户名,为空则不降权
- }
- // ApplyRunAsUser 让 cmd 以指定系统用户身份运行(降权)。
- // runAsUser 为空时不做任何处理(沿用当前进程用户)。
- // 该函数会:
- // 1. 解析目标用户的 uid/gid,并设置 cmd.SysProcAttr.Credential;
- // 2. 附带解析该用户的补充组(supplementary groups);
- // 3. 修正 HOME/USER/LOGNAME 环境变量为目标用户的值,避免 kiro-cli
- // 仍读取 root 的家目录导致配置/凭证路径错误。
- //
- // 注意:设置 Credential 需要当前进程具备相应权限(通常为 root)。
- func ApplyRunAsUser(cmd *exec.Cmd, runAsUser string) error {
- runAsUser = strings.TrimSpace(runAsUser)
- if runAsUser == "" {
- return nil
- }
- u, err := user.Lookup(runAsUser)
- if err != nil {
- return fmt.Errorf("查找降权用户 %q 失败: %w", runAsUser, err)
- }
- uid, err := strconv.ParseUint(u.Uid, 10, 32)
- if err != nil {
- return fmt.Errorf("解析用户 %q 的 uid(%s) 失败: %w", runAsUser, u.Uid, err)
- }
- gid, err := strconv.ParseUint(u.Gid, 10, 32)
- if err != nil {
- return fmt.Errorf("解析用户 %q 的 gid(%s) 失败: %w", runAsUser, u.Gid, err)
- }
- // 解析补充组(失败不致命,仅记录告警)
- var groups []uint32
- if gidStrs, gErr := u.GroupIds(); gErr == nil {
- for _, gs := range gidStrs {
- if g, pErr := strconv.ParseUint(gs, 10, 32); pErr == nil {
- groups = append(groups, uint32(g))
- }
- }
- } else {
- zap.L().Warn("[ACP] 读取降权用户补充组失败", zap.String("user", runAsUser), zap.Error(gErr))
- }
- if cmd.SysProcAttr == nil {
- cmd.SysProcAttr = &syscall.SysProcAttr{}
- }
- cmd.SysProcAttr.Credential = &syscall.Credential{
- Uid: uint32(uid),
- Gid: uint32(gid),
- Groups: groups,
- }
- // 修正环境变量,确保子进程使用目标用户的身份。
- // USER/LOGNAME 强制设为目标用户;HOME 尊重上层(配置)已传入的值,
- // 仅在未提供时才回退到用户家目录。
- if cmd.Env == nil {
- cmd.Env = os.Environ()
- }
- cmd.Env = overrideEnv(cmd.Env, map[string]string{
- "USER": u.Username,
- "LOGNAME": u.Username,
- })
- cmd.Env = fillEnvIfAbsent(cmd.Env, "HOME", u.HomeDir)
- effectiveHome := u.HomeDir
- for _, e := range cmd.Env {
- if strings.HasPrefix(e, "HOME=") {
- effectiveHome = strings.TrimPrefix(e, "HOME=")
- break
- }
- }
- zap.L().Info("[ACP] 子进程降权运行",
- zap.String("user", u.Username),
- zap.String("uid", u.Uid),
- zap.String("gid", u.Gid),
- zap.String("home", effectiveHome),
- )
- return nil
- }
- // fillEnvIfAbsent 仅当 env 中不存在 key 时才追加 key=value,否则原样返回。
- func fillEnvIfAbsent(env []string, key, value string) []string {
- prefix := key + "="
- for _, e := range env {
- if strings.HasPrefix(e, prefix) {
- return env
- }
- }
- return append(env, prefix+value)
- }
- // envHasKey 判断 env("KEY=VALUE" 形式的切片)中是否已存在指定 key。
- func envHasKey(env []string, key string) bool {
- prefix := key + "="
- for _, e := range env {
- if strings.HasPrefix(e, prefix) {
- return true
- }
- }
- return false
- }
- // ensureDirWritableByRunAsUser 确保日志目录可被降权用户写入。
- // 主服务常以 root 运行,创建的目录属主为 root;子进程降权到 runAsUser 后会写不进去,
- // 导致 kiro-cli 日志被静默丢弃。这里将目录(含其直接父目录)chown 到 runAsUser 的 uid/gid。
- // runAsUser 为空表示不降权(与主进程同用户),无需调整。所有失败仅记录告警,不致命。
- func ensureDirWritableByRunAsUser(dir string, runAsUser string) {
- runAsUser = strings.TrimSpace(runAsUser)
- if runAsUser == "" {
- return
- }
- u, err := user.Lookup(runAsUser)
- if err != nil {
- zap.L().Warn("[ACP] 查找降权用户失败,日志目录属主未调整", zap.String("user", runAsUser), zap.Error(err))
- return
- }
- uid, err1 := strconv.Atoi(u.Uid)
- gid, err2 := strconv.Atoi(u.Gid)
- if err1 != nil || err2 != nil {
- zap.L().Warn("[ACP] 解析降权用户 uid/gid 失败,日志目录属主未调整",
- zap.String("user", runAsUser), zap.String("uid", u.Uid), zap.String("gid", u.Gid))
- return
- }
- // 同时调整父目录(如 /tmp/kiro-log)与目标目录(如 /tmp/kiro-log/logs)。
- for _, d := range []string{filepath.Dir(dir), dir} {
- if err := os.Chown(d, uid, gid); err != nil {
- zap.L().Warn("[ACP] 调整日志目录属主失败(降权用户可能无法写入日志)",
- zap.String("dir", d), zap.String("user", runAsUser), zap.Error(err))
- }
- }
- }
- // overrideEnv 用 kv 覆盖 env 中已存在的同名变量,不存在则追加。
- func overrideEnv(env []string, kv map[string]string) []string {
- out := make([]string, 0, len(env)+len(kv))
- seen := make(map[string]bool, len(kv))
- for _, e := range env {
- key := e
- if idx := strings.IndexByte(e, '='); idx >= 0 {
- key = e[:idx]
- }
- if v, ok := kv[key]; ok {
- out = append(out, key+"="+v)
- seen[key] = true
- } else {
- out = append(out, e)
- }
- }
- for k, v := range kv {
- if !seen[k] {
- out = append(out, k+"="+v)
- }
- }
- return out
- }
- // 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...)
- // A 方案:为每次启动的子进程分配带启动时间戳的独立日志文件,避免覆盖历史日志。
- // 仅在开启了 kiro-cli debug 日志(env 含 KIRO_LOG_LEVEL)、且调用方未显式指定
- // KIRO_CHAT_LOG_FILE 时生效。KIRO_CHAT_LOG_FILE 是 kiro-cli acp 官方支持的日志路径变量。
- if envHasKey(cmd.Env, "KIRO_LOG_LEVEL") && !envHasKey(cmd.Env, "KIRO_CHAT_LOG_FILE") {
- logDir := "/tmp/kiro-log/logs"
- if err := os.MkdirAll(logDir, 0o777); err != nil {
- zap.L().Warn("[ACP] 创建日志目录失败,沿用 kiro-cli 默认日志路径",
- zap.String("dir", logDir), zap.Error(err))
- } else {
- // 主服务常以 root 运行,创建出的目录默认属主为 root;而子进程会被
- // ApplyRunAsUser 降权到 cfg.RunAsUser,若目录不可写日志将被静默丢弃。
- // 因此这里把目录属主/权限调整到降权用户可写。
- ensureDirWritableByRunAsUser(logDir, cfg.RunAsUser)
- logPath := filepath.Join(logDir, fmt.Sprintf("kiro-chat-%s.log", time.Now().Format("20060102T150405.000")))
- cmd.Env = append(cmd.Env, "KIRO_CHAT_LOG_FILE="+logPath)
- // 顺带关闭 ANSI 颜色码,便于后续直接阅读/grep 日志。
- if !envHasKey(cmd.Env, "KIRO_LOG_NO_COLOR") {
- cmd.Env = append(cmd.Env, "KIRO_LOG_NO_COLOR=1")
- }
- zap.L().Info("[ACP] 本次子进程日志文件", zap.String("logFile", logPath))
- }
- }
- // 若配置了降权用户,则以该用户身份拉起子进程
- if err := ApplyRunAsUser(cmd, cfg.RunAsUser); err != nil {
- return nil, err
- }
- 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
- c := &Client{
- cmd: cmd,
- stdin: stdin,
- scanner: scanner,
- pending: make(map[int64]*pendingResponse),
- permissionCh: make(chan string, 1),
- doneCh: make(chan struct{}),
- }
- // 监听子进程退出,单点置位 dead 标记(供 IsAlive 可靠判活)
- go c.waitProcess()
- // 启动唯一的读循环,统一分发响应/请求/通知
- go c.readLoop()
- return c, nil
- }
- // waitProcess 阻塞等待子进程退出,退出后标记 dead。
- // 这是回收子进程、置位存活标记的唯一入口,因此 IsAlive 不再依赖
- // cmd.ProcessState(只有调用 Wait 后才会被填充)的偶然状态。
- func (c *Client) waitProcess() {
- _ = c.cmd.Wait()
- c.markDead()
- }
- // markDead 标记进程已退出,并关闭 doneCh(幂等)。
- func (c *Client) markDead() {
- if c.dead.CompareAndSwap(false, true) {
- close(c.doneCh)
- }
- }
- // ============ 读循环与分发 ============
- // readLoop 是唯一读取 stdout 的 goroutine。它把每行消息按类型派发:
- // - 响应(有 id、无 method)→ 投递到 pending map 中等待的 channel;
- // - 反向请求(有 id、有 method)→ 在独立 goroutine 中处理(权限等待不阻塞读循环);
- // - 通知(无 id、有 method)→ 交给当前 notifySink。
- //
- // scanner 结束(EOF / 进程退出)时标记 dead,并唤醒所有等待中的请求。
- func (c *Client) readLoop() {
- defer func() {
- c.markDead()
- c.failAllPending()
- }()
- for c.scanner.Scan() {
- line := c.scanner.Text()
- if line == "" {
- continue
- }
- zap.L().Debug("[ACP] <<< 收到", zap.String("raw", line[:min(len(line), 500)]))
- 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
- }
- hasId := len(msg.ID) > 0 && string(msg.ID) != "null"
- switch {
- case hasId && msg.Method == "":
- // 响应:按 id 派发给等待者
- var id int64
- if err := json.Unmarshal(msg.ID, &id); err != nil {
- zap.L().Warn("[ACP] 响应 id 解析失败", zap.String("id", string(msg.ID)))
- continue
- }
- c.deliverResponse(id, Response{
- JSONRPC: msg.JSONRPC,
- ID: &id,
- Result: msg.Result,
- Error: msg.Error,
- })
- case hasId && msg.Method != "":
- // Agent→Client 反向请求:独立 goroutine 处理,避免权限等待阻塞读循环
- id := append(json.RawMessage(nil), msg.ID...)
- params := append(json.RawMessage(nil), msg.Params...)
- method := msg.Method
- go c.handleAgentRequest(id, method, params)
- case !hasId && msg.Method != "":
- // 通知:交给当前 sink
- c.dispatchNotification(msg.Method, msg.Params)
- default:
- zap.L().Debug("[ACP] 未处理的消息", zap.String("line", line[:min(len(line), 200)]))
- }
- }
- }
- // registerPending 注册一个等待响应的请求,返回接收 channel。
- func (c *Client) registerPending(id int64) chan Response {
- ch := make(chan Response, 1)
- c.pendingMu.Lock()
- c.pending[id] = &pendingResponse{ch: ch}
- c.pendingMu.Unlock()
- return ch
- }
- // unregisterPending 移除等待项(请求完成或超时后调用)。
- func (c *Client) unregisterPending(id int64) {
- c.pendingMu.Lock()
- delete(c.pending, id)
- c.pendingMu.Unlock()
- }
- // deliverResponse 把响应投递给等待者;无人等待则忽略。
- func (c *Client) deliverResponse(id int64, resp Response) {
- c.pendingMu.Lock()
- p, ok := c.pending[id]
- if ok {
- delete(c.pending, id)
- }
- c.pendingMu.Unlock()
- if ok {
- p.ch <- resp
- } else {
- zap.L().Debug("[ACP] 收到无等待者的响应", zap.Int64("id", id))
- }
- }
- // failAllPending 进程退出时唤醒所有等待中的请求,返回错误响应。
- func (c *Client) failAllPending() {
- c.pendingMu.Lock()
- pend := c.pending
- c.pending = make(map[int64]*pendingResponse)
- c.pendingMu.Unlock()
- for id, p := range pend {
- p.ch <- Response{ID: &id, Error: &RPCError{Code: -1, Message: "Agent 进程已断开"}}
- }
- }
- // setSink 设置当前通知接收者,返回旧 sink(便于恢复)。
- func (c *Client) setSink(s notifySink) notifySink {
- c.sinkMu.Lock()
- old := c.sink
- c.sink = s
- c.sinkMu.Unlock()
- return old
- }
- // beginTurn 在一次 prompt 开始时设置事件通道与中止信号(受 sinkMu 保护),
- // 返回本轮的 abort channel 供上层(如断连时)主动中止。
- func (c *Client) beginTurn(ch chan<- Event) chan struct{} {
- abort := make(chan struct{})
- c.sinkMu.Lock()
- c.eventCh = ch
- c.abortCh = abort
- c.sinkMu.Unlock()
- return abort
- }
- // endTurn 在一次 prompt 结束时清理,并保证调用方随后可安全 close(eventCh):
- // 1. 关闭 abort,唤醒可能阻塞在发送上的 emit;
- // 2. 等待所有在途 emit(emitWg)退出;
- // 3. 在锁内清空 eventCh/abortCh。
- //
- // 注意:emit 不持 sinkMu 阻塞发送,故这里关闭 abort 不会与 emit 形成锁环。
- func (c *Client) endTurn() {
- c.sinkMu.Lock()
- abort := c.abortCh
- c.sinkMu.Unlock()
- if abort != nil {
- safeClose(abort)
- }
- c.emitWg.Wait()
- c.sinkMu.Lock()
- c.eventCh = nil
- c.abortCh = nil
- c.sinkMu.Unlock()
- }
- // abortTurn 主动中止当前 turn 的事件发送(上层连接断开时调用)。
- // 仅关闭 abort 信号,唤醒阻塞的 emit;turn 的最终结束仍由 Prompt 收到响应/取消驱动。
- func (c *Client) abortTurn() {
- c.sinkMu.Lock()
- abort := c.abortCh
- c.sinkMu.Unlock()
- if abort != nil {
- safeClose(abort)
- }
- }
- // AbortEvents 供上层在连接断开时调用:唤醒所有阻塞在事件发送上的 goroutine,
- // 使当前 turn 的事件流尽快收敛(配合 Cancel 通知 Agent 停止本轮)。
- func (c *Client) AbortEvents() {
- c.abortTurn()
- }
- // safeClose 幂等关闭一个 struct{} channel。
- func safeClose(ch chan struct{}) {
- defer func() { _ = recover() }()
- close(ch)
- }
- // emit 向当前 turn 的事件通道发送一个事件,返回是否成功送达。
- // 发送不持有 sinkMu(避免与 endTurn 关闭 abort 形成锁环),改用 emitWg 标记在途状态,
- // 使 endTurn 能在 close(eventCh) 前等待所有发送退出,杜绝向已关闭通道发送。
- // 发送同时监听 abort(上层停止消费)与 doneCh(进程退出),任一触发即放弃发送。
- func (c *Client) emit(ev Event) bool {
- c.emitWg.Add(1)
- defer c.emitWg.Done()
- c.sinkMu.Lock()
- ch := c.eventCh
- abort := c.abortCh
- c.sinkMu.Unlock()
- if ch == nil {
- return false
- }
- select {
- case ch <- ev:
- return true
- case <-abort:
- return false
- case <-c.doneCh:
- return false
- }
- }
- // pushPermissionEvent 把权限事件推给当前 eventCh;返回是否成功(无活跃通道时返回 false)。
- func (c *Client) pushPermissionEvent(ev Event) bool {
- return c.emit(ev)
- }
- // dispatchNotification 把通知交给当前 sink;无 sink 时走默认处理(仅缓存 metadata)。
- func (c *Client) dispatchNotification(method string, params json.RawMessage) {
- c.sinkMu.Lock()
- s := c.sink
- c.sinkMu.Unlock()
- if s != nil {
- s(method, params)
- return
- }
- c.defaultNotify(method, params)
- }
- // defaultNotify 无活跃 sink 时的兜底:仅捕获 metadata 用量,其余忽略。
- func (c *Client) defaultNotify(method string, params json.RawMessage) {
- if method == "_kiro.dev/metadata" {
- var data struct {
- ContextUsagePercentage float64 `json:"contextUsagePercentage"`
- TurnDurationMs int64 `json:"turnDurationMs,omitempty"`
- }
- if err := json.Unmarshal(params, &data); err == nil {
- c.setMeta(data.ContextUsagePercentage, data.TurnDurationMs)
- }
- }
- }
- // ResolvePermission 前端回传权限选择结果
- func (c *Client) ResolvePermission(optionId string) {
- select {
- case c.permissionCh <- optionId:
- default:
- // 没有等待中的权限请求,忽略
- }
- }
- // appendText 累积本轮 AI 文本(sink/readLoop goroutine 调用)。
- func (c *Client) appendText(s string) {
- c.textMu.Lock()
- c.textBuffer += s
- c.textMu.Unlock()
- }
- // takeText 取出并清空累积文本(Prompt goroutine 在 turn 结束时调用)。
- func (c *Client) takeText() string {
- c.textMu.Lock()
- s := c.textBuffer
- c.textBuffer = ""
- c.textMu.Unlock()
- return s
- }
- // resetText 清空累积文本(turn 开始前调用,避免跨 turn 串味)。
- func (c *Client) resetText() {
- c.textMu.Lock()
- c.textBuffer = ""
- c.textMu.Unlock()
- }
- // setMeta 更新最近的上下文用量与耗时(值 >0 时才覆盖)
- func (c *Client) setMeta(usage float64, turnMs int64) {
- c.metaMu.Lock()
- defer c.metaMu.Unlock()
- if usage > 0 {
- c.lastContextUsage = usage
- }
- if turnMs > 0 {
- c.lastTurnDurationMs = turnMs
- }
- }
- // GetMeta 返回最近的上下文用量(百分比)与上一轮耗时(毫秒)
- func (c *Client) GetMeta() (float64, int64) {
- c.metaMu.Lock()
- defer c.metaMu.Unlock()
- return c.lastContextUsage, c.lastTurnDurationMs
- }
- // 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、实际生效的 agentId 和 modelId
- func (c *Client) NewSession(cwd string) (sessionId string, welcomeMsg string, agentId string, modelId string, err 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 和实际生效的 agentId
- if res.Modes != nil {
- agentId = res.Modes.CurrentModeId
- for _, mode := range res.Modes.AvailableModes {
- if mode.Id == res.Modes.CurrentModeId && mode.Meta != nil {
- welcomeMsg = mode.Meta.WelcomeMessage
- break
- }
- }
- }
- // 提取实际生效的 modelId
- if res.Models != nil {
- modelId = res.Models.CurrentModelId
- }
- // session/new 之后 Kiro 紧跟发送的 commands/available、metadata 等通知,
- // 由读循环交给默认 sink 处理:其中 metadata 的初始上下文用量会被 setMeta 缓存。
- // 不再需要单独的 drainInitialMeta 阻塞读取。
- return res.SessionId, welcomeMsg, agentId, modelId, 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, string, string, []ReplayMessage, error) {
- params := map[string]any{
- "sessionId": sessionId,
- "cwd": cwd,
- "mcpServers": []any{},
- }
- // 回放消息在 session/load 响应之前以通知形式到达。用一个临时 sink 收集它们,
- // 由读循环在等待响应期间调用(sendAndWaitWithSink 会在返回前恢复原 sink)。
- var replayMessages []ReplayMessage
- currentMsg := &ReplayMessage{}
- sink := func(method string, p json.RawMessage) {
- if method != "session/update" && method != "_kiro.dev/session/update" {
- // 非回放通知(metadata 等)走默认处理,避免丢失用量
- c.defaultNotify(method, p)
- return
- }
- var notif SessionUpdate
- if err := json.Unmarshal(p, ¬if); err != nil {
- return
- }
- var base UpdateBase
- if err := json.Unmarshal(notif.Update, &base); err != nil {
- return
- }
- 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":
- var chunk AgentMessageChunk
- json.Unmarshal(notif.Update, &chunk)
- if currentMsg.Role != "assistant" || (chunk.MessageId != "" && chunk.MessageId != currentMsg.MessageId) {
- 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", "tool_call", "tool_call_update", "tool_call_chunk":
- // 思考内容与工具调用回放忽略
- default:
- // 其他类型忽略
- }
- }
- result, err := c.sendAndWaitWithSink("session/load", params, sink)
- if err != nil {
- return "", "", "", "", nil, fmt.Errorf("session/load 错误: %w", err)
- }
- c.sessionId = sessionId
- // 追加最后一条未关闭的消息
- if currentMsg.Content != "" {
- replayMessages = append(replayMessages, *currentMsg)
- }
- // 解析响应中的 welcomeMessage、实际生效的 agentId 和 modelId
- var welcomeMsg string
- var agentId string
- var modelId string
- if len(result) > 0 {
- var res NewSessionResult
- if err := json.Unmarshal(result, &res); err == nil {
- if res.Modes != nil {
- agentId = res.Modes.CurrentModeId
- for _, mode := range res.Modes.AvailableModes {
- if mode.Id == res.Modes.CurrentModeId && mode.Meta != nil {
- welcomeMsg = mode.Meta.WelcomeMessage
- break
- }
- }
- }
- if res.Models != nil {
- modelId = res.Models.CurrentModelId
- }
- }
- }
- // session/load 之后的初始用量 metadata 通知由默认 sink 捕获,无需单独处理。
- return sessionId, welcomeMsg, agentId, modelId, replayMessages, nil
- }
- // Prompt 发送消息并流式读取事件
- // eventCh 会在 turn 结束或出错时关闭
- func (c *Client) Prompt(sessionId string, message string, eventCh chan<- Event) {
- // 开始一个 turn:登记事件通道与 abort 信号。退出时 endTurn 会关闭 abort、
- // 等待所有在途 emit 结束,再清空通道引用,之后才 close(eventCh)——
- // 保证权限/流式 goroutine 不会向已关闭的通道发送。
- c.beginTurn(eventCh)
- defer func() {
- c.endTurn()
- close(eventCh)
- }()
- params := PromptParams{
- SessionId: sessionId,
- Prompt: []ContentBlock{
- {Type: "text", Text: message},
- },
- }
- // 注册等待 prompt 响应
- promptId := atomic.AddInt64(&c.requestId, 1)
- respCh := c.registerPending(promptId)
- // 清空上一轮可能残留的累积文本,避免跨 turn 串味
- c.resetText()
- // 安装 sink:turn 期间的 session/update 与扩展通知都转成 Event 推给 eventCh。
- // 反向请求(requestPermission)由读循环单独派发,不经过 sink。
- old := c.setSink(func(method string, p json.RawMessage) {
- switch method {
- case "session/update", "_kiro.dev/session/update":
- c.handleSessionUpdate(p)
- default:
- c.handleExtensionNotification(method, p)
- }
- })
- defer c.setSink(old)
- if err := c.writeRequest(promptId, "session/prompt", params); err != nil {
- c.unregisterPending(promptId)
- c.emit(Event{Type: "error", Content: fmt.Sprintf("发送 prompt 失败: %v", err)})
- return
- }
- zap.L().Debug("[ACP] prompt 已发送", zap.Int64("promptId", promptId), zap.String("sessionId", sessionId))
- // 等待 prompt 响应(turn 结束)或进程退出
- select {
- case resp := <-respCh:
- if full := c.takeText(); full != "" {
- zap.L().Info("[ACP] AI 回复完整内容", zap.String("text", full))
- }
- if resp.Error != nil {
- c.emit(Event{Type: "error", Content: resp.Error.FullMessage()})
- return
- }
- var result PromptResult
- if err := json.Unmarshal(resp.Result, &result); err == nil {
- c.emit(Event{Type: "done", SessionId: sessionId, StopReason: result.StopReason})
- }
- case <-c.doneCh:
- c.unregisterPending(promptId)
- c.emit(Event{Type: "error", Content: "Agent 进程已断开"})
- }
- }
- // Cancel 取消当前 prompt turn(ACP 协议:Notification, method="session/cancel", params={sessionId})
- func (c *Client) Cancel(sessionId string) {
- c.sendNotification("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()
- // 终止进程(回收由 waitProcess 中的 cmd.Wait 负责,此处不再重复 Wait)
- if c.cmd.Process != nil {
- c.cmd.Process.Kill()
- }
- // 解除可能阻塞在 permissionCh 上的 goroutine
- select {
- case c.permissionCh <- "deny":
- default:
- }
- }
- // IsAlive 检查 Agent 进程是否存活。
- // 依赖 waitProcess/readLoop 单点设置的 dead 标记,而非 cmd.ProcessState
- // (后者只有在 Wait 之后才被填充,子进程自行崩溃时会误判为存活)。
- func (c *Client) IsAlive() bool {
- return !c.dead.Load()
- }
- // ============ 内部方法 ============
- // sendRequest 发送请求(不等待响应),返回 request ID。
- // 注意:调用方若需要等待响应,应使用 sendAndWait;直接使用本方法的场景
- // (如 Prompt)需自行通过 registerPending/读循环获取响应。
- func (c *Client) sendRequest(method string, params any) int64 {
- id := atomic.AddInt64(&c.requestId, 1)
- if err := c.writeRequest(id, method, params); err != nil {
- zap.L().Error("[ACP] 发送请求失败", zap.String("method", method), zap.Error(err))
- }
- return id
- }
- // writeRequest 序列化并写入一个带 id 的请求,写入通过 writeMu 串行化。
- func (c *Client) writeRequest(id int64, method string, params any) error {
- req := Request{
- JSONRPC: "2.0",
- ID: id,
- Method: method,
- Params: params,
- }
- data, err := json.Marshal(req)
- if err != nil {
- return fmt.Errorf("序列化请求失败: %w", err)
- }
- zap.L().Debug("[ACP] >>> 发送", zap.String("raw", string(data)))
- return c.writeLine(data)
- }
- // writeLine 追加换行并串行写入 stdin。
- func (c *Client) writeLine(data []byte) error {
- c.writeMu.Lock()
- defer c.writeMu.Unlock()
- data = append(data, '\n')
- _, err := c.stdin.Write(data)
- return err
- }
- // sendNotification 发送 JSON-RPC Notification(不带 id,不期望响应)
- func (c *Client) sendNotification(method string, params any) {
- notif := struct {
- JSONRPC string `json:"jsonrpc"`
- Method string `json:"method"`
- Params any `json:"params,omitempty"`
- }{
- JSONRPC: "2.0",
- Method: method,
- Params: params,
- }
- data, err := json.Marshal(notif)
- if err != nil {
- zap.L().Error("[ACP] 序列化通知失败", zap.Error(err))
- return
- }
- zap.L().Debug("[ACP] >>> 发送(notification)", zap.String("raw", string(data)))
- if err := c.writeLine(data); err != nil {
- zap.L().Error("[ACP] 写入 stdin 失败(通知)", zap.String("method", method), zap.Error(err))
- }
- }
- // sendAndWait 发送请求并等待对应 ID 的响应(通过读循环分发)。
- // 若传入非 nil sink,在等待期间临时接管通知(用于捕获 init/load 阶段的通知),
- // 返回前恢复原 sink。
- func (c *Client) sendAndWait(method string, params any) (json.RawMessage, error) {
- return c.sendAndWaitWithSink(method, params, nil)
- }
- func (c *Client) sendAndWaitWithSink(method string, params any, sink notifySink) (json.RawMessage, error) {
- id := atomic.AddInt64(&c.requestId, 1)
- ch := c.registerPending(id)
- if sink != nil {
- old := c.setSink(sink)
- defer c.setSink(old)
- }
- if err := c.writeRequest(id, method, params); err != nil {
- c.unregisterPending(id)
- return nil, err
- }
- select {
- case resp := <-ch:
- if resp.Error != nil {
- return nil, fmt.Errorf("ACP 错误 [%d]: %s", resp.Error.Code, resp.Error.FullMessage())
- }
- return resp.Result, nil
- case <-c.doneCh:
- c.unregisterPending(id)
- return nil, fmt.Errorf("未收到 %s 的响应(进程已退出)", method)
- }
- }
- // handleSessionUpdate 解析并分发 session/update 通知
- func (c *Client) handleSessionUpdate(params json.RawMessage) {
- 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.appendText(chunk.Content.Text)
- c.emit(Event{Type: "text", Content: chunk.Content.Text})
- }
- case "agent_thought_chunk":
- var chunk AgentMessageChunk
- if err := json.Unmarshal(notif.Update, &chunk); err == nil {
- c.appendText(chunk.Content.Text)
- c.emit(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))
- c.emit(Event{Type: "tool_call", ToolCallId: tc.ToolCallId, 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))
- c.emit(Event{Type: "tool_update", ToolCallId: tcu.ToolCallId, 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) {
- 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)
- }
- c.emit(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 {
- zap.L().Debug("[ACP] metadata",
- zap.Float64("contextUsage%", data.ContextUsagePercentage),
- zap.Int64("turnMs", data.TurnDurationMs),
- )
- // 缓存最近用量/耗时,供首屏读取
- c.setMeta(data.ContextUsagePercentage, data.TurnDurationMs)
- // 上下文用量每条通知都会带(turnDurationMs 仅 turn 结束那条才有),
- // 因此总是转发用量,耗时有值时才附带
- content := map[string]any{
- "contextUsagePercent": data.ContextUsagePercentage,
- }
- if data.TurnDurationMs > 0 {
- content["turnDurationMs"] = data.TurnDurationMs
- }
- c.emit(Event{
- Type: "metadata",
- Content: toJSONStr(content),
- })
- }
- 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)
- }
- // applyLineWindow 按 ACP readTextFile 语义对文本做行窗口截取。
- // - line 为 1 基起始行号(nil 或 <=0 视为从第 1 行开始);
- // - limit 为返回的最大行数(nil 或 <=0 视为不限制,返回到文件末尾)。
- //
- // 保留行间换行;起始行越界时返回空串。用于避免把整份大文件回传给 Agent。
- func applyLineWindow(content string, line, limit *int) string {
- start := 1
- if line != nil && *line > 0 {
- start = *line
- }
- // 无窗口需求:整文件返回
- if start <= 1 && (limit == nil || *limit <= 0) {
- return content
- }
- lines := strings.Split(content, "\n")
- if start > len(lines) {
- return ""
- }
- from := start - 1 // 转 0 基
- to := len(lines)
- if limit != nil && *limit > 0 {
- if end := from + *limit; end < to {
- to = end
- }
- }
- return strings.Join(lines[from:to], "\n")
- }
- // handleAgentRequest 处理 Agent→Client 的反向请求。
- // 该方法由读循环在独立 goroutine 中调用,因此权限等待不会阻塞读循环;
- // stdin 写入通过 writeLine(writeMu)串行化,不与 Cancel 争锁。
- func (c *Client) handleAgentRequest(rawId json.RawMessage, method string, rawParams json.RawMessage) {
- 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)读取:先按路径解析出 object key
- key, keyErr := filestore.FindKeyByPath("upload", c.UserId, c.sessionId, params.Path)
- if keyErr == nil && key != "" {
- content, readErr = filestore.ReadFile(key)
- zap.L().Debug("[ACP] readTextFile 从 MinIO 读取", zap.String("path", params.Path), zap.String("key", key))
- } 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 {
- windowed := applyLineWindow(content, params.Line, params.Limit)
- result, _ := json.Marshal(map[string]string{"content": windowed})
- 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 := filestore.UploadFile(c.UserId, c.sessionId, filepath.Base(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
- }
- // 当前策略:所有权限请求都推给前端授权。
- 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))
- pushed := c.pushPermissionEvent(Event{
- Type: "permission",
- Permission: &PermissionEvent{
- RequestId: requestId,
- Title: title,
- Options: options,
- },
- })
- if pushed {
- // 丢弃可能残留的旧授权结果,避免消费到上一次请求/Close 写入的值
- select {
- case <-c.permissionCh:
- default:
- }
- // 有界等待前端回复:超时 / 进程退出 时按 deny 处理,避免永久阻塞。
- // 本 goroutine 阻塞不影响读循环(读循环在另一 goroutine)。
- selectedOptionId := c.selectDenyOption(params.Options)
- timer := time.NewTimer(PermissionWaitTimeout)
- select {
- case selectedOptionId = <-c.permissionCh:
- case <-timer.C:
- zap.L().Warn("[ACP] 权限等待超时,默认拒绝", zap.String("title", title))
- case <-c.doneCh:
- zap.L().Warn("[ACP] 权限等待期间进程已退出", zap.String("title", title))
- }
- timer.Stop()
- respData, _ = json.Marshal(map[string]any{
- "jsonrpc": "2.0",
- "id": rawId,
- "result": map[string]any{
- "outcome": map[string]any{
- "outcome": "selected",
- "optionId": selectedOptionId,
- },
- },
- })
- } else {
- // 无事件通道(非 prompt 阶段):自动允许
- 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)])))
- if err := c.writeLine(respData); err != nil {
- zap.L().Error("[ACP] 写入反向响应失败", zap.String("method", method), zap.Error(err))
- }
- } else {
- zap.L().Warn("[ACP] 反向请求无响应数据", zap.String("method", method))
- }
- }
- // selectDenyOption 从可选项中挑选一个 deny 语义的 optionId 作为超时/退出时的默认值。
- // 找不到显式 deny 选项时回退到字面量 "deny"。
- func (c *Client) selectDenyOption(options []struct {
- OptionId string `json:"optionId"`
- Name string `json:"name"`
- Kind string `json:"kind"`
- }) string {
- for _, opt := range options {
- if strings.Contains(strings.ToLower(opt.Kind), "deny") || strings.Contains(strings.ToLower(opt.OptionId), "deny") {
- return opt.OptionId
- }
- }
- return "deny"
- }
|