feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
This commit is contained in:
commit
d5dac5a6fc
140 files changed
+26497
No files matched your search
@@ -0,0 +1,168 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.gopher.ink/mirrors2/AutoZero/diag"
|
||||
"git.gopher.ink/mirrors2/AutoZero/execx"
|
||||
)
|
||||
|
||||
// reqMeta 是发送一条请求时传输层需要的元数据:HTTP 要据此生成 Mcp-Method / Mcp-Name 头,
|
||||
// stdio 只用到 id(按 id 分发响应)。
|
||||
type reqMeta struct {
|
||||
id string // 归一化后的请求 id
|
||||
method string // JSON-RPC method
|
||||
name string // tools/call 的 params.name / resources/read 的 params.uri
|
||||
version string // 本次请求声明的协议版本(HTTP 放进 MCP-Protocol-Version 头)
|
||||
}
|
||||
|
||||
// transport 是"发一条 JSON-RPC 消息并拿到响应"的抽象。
|
||||
//
|
||||
// stdio:一条连接复用多次请求,按 id 分发;HTTP:每个请求一次 POST,响应自带。
|
||||
type transport interface {
|
||||
Call(ctx context.Context, body []byte, meta reqMeta) (rpcMsg, error)
|
||||
Notify(body []byte, meta reqMeta) error
|
||||
Close() error
|
||||
Kind() string // "stdio" | "http",仅用于日志
|
||||
}
|
||||
|
||||
// legacyServerError 表示"服务端不是现代的(无 initialize 之外的现代特征)",
|
||||
// 客户端据此回退到 handshake 式协议。HTTP 侧要靠它区分 400 的两种含义。
|
||||
type legacyServerError struct{ reason string }
|
||||
|
||||
func (e *legacyServerError) Error() string { return e.reason }
|
||||
|
||||
// authRequiredError 表示服务端要求鉴权。它**不是年代信号**——两种年代都会返回 401,
|
||||
// 所以年代探测遇到它应当直接终止,而不是再去试一次 legacy 握手(那样错误里会多出一个
|
||||
// 误导性的 "initialize:" 前缀,让人以为是握手失败)。
|
||||
type authRequiredError struct{ msg string }
|
||||
|
||||
func (e *authRequiredError) Error() string { return e.msg }
|
||||
|
||||
// ---- stdio ----
|
||||
|
||||
// stdioTransport 用子进程的 stdin/stdout 通信(JSON-RPC,换行分隔帧)。
|
||||
type stdioTransport struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
pending map[string]chan rpcMsg
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newStdioTransport(cmd *exec.Cmd, stdin io.WriteCloser, stdout io.Reader) *stdioTransport {
|
||||
t := &stdioTransport{cmd: cmd, stdin: stdin, pending: map[string]chan rpcMsg{}}
|
||||
go t.readLoop(stdout)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *stdioTransport) Kind() string { return "stdio" }
|
||||
|
||||
func (t *stdioTransport) Call(ctx context.Context, body []byte, meta reqMeta) (rpcMsg, error) {
|
||||
ch := make(chan rpcMsg, 1)
|
||||
t.mu.Lock()
|
||||
t.pending[meta.id] = ch
|
||||
t.mu.Unlock()
|
||||
|
||||
if _, err := t.stdin.Write(append(body, '\n')); err != nil {
|
||||
t.drop(meta.id)
|
||||
return rpcMsg{}, err
|
||||
}
|
||||
select {
|
||||
case m := <-ch:
|
||||
return m, nil
|
||||
case <-ctx.Done():
|
||||
t.drop(meta.id)
|
||||
return rpcMsg{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *stdioTransport) Notify(body []byte, _ reqMeta) error {
|
||||
_, err := t.stdin.Write(append(body, '\n'))
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *stdioTransport) drop(id string) {
|
||||
t.mu.Lock()
|
||||
delete(t.pending, id)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *stdioTransport) Close() error {
|
||||
t.mu.Lock()
|
||||
for id, ch := range t.pending {
|
||||
ch <- rpcMsg{Error: &rpcError{Message: "client closed"}}
|
||||
delete(t.pending, id)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
_ = t.stdin.Close()
|
||||
if t.cmd != nil {
|
||||
// 只 Kill 直接子进程会把真正的 server 留成孤儿(npx/uvx 起的 node/python
|
||||
// 就是孙子):它继续占着 stdout,readLoop goroutine 永不退出。
|
||||
_ = execx.KillTree(t.cmd)
|
||||
return t.cmd.Wait() // 构造时已设 WaitDelay,这里不会无限阻塞
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLoop 按行读 server stdout 并分发给等待者。
|
||||
//
|
||||
// **不能用 json.Decoder 直接解流**:stdout 上出现一行非 JSON(很多 server 会打启动
|
||||
// banner)就会让它报错并停止读取,之后所有请求都等不到回复;而规范要求的恰恰是
|
||||
// "无法识别的输出 ⇒ 判为 legacy ⇒ 回退 initialize",按那种写法那次回退必然白等到超时。
|
||||
// 所以这里跳过解析不了的行,只在 EOF/读错误时唤醒等待者。
|
||||
func (t *stdioTransport) readLoop(r io.Reader) {
|
||||
br := bufio.NewReader(r)
|
||||
for {
|
||||
line, err := br.ReadBytes('\n')
|
||||
if trimmed := bytes.TrimSpace(line); len(trimmed) > 0 {
|
||||
var m rpcMsg
|
||||
if json.Unmarshal(trimmed, &m) == nil {
|
||||
t.dispatch(m)
|
||||
} else {
|
||||
diag.Printf("[mcp] 忽略无法解析的 stdout 行: %.120s\n", trimmed)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.failAll(fmt.Errorf("mcp server stopped: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch 把带 id 的响应交给等待者。通知与服务端发起的请求都不处理
|
||||
// (现代协议里服务端不再发起请求;legacy 下我们声明了空 capabilities)。
|
||||
func (t *stdioTransport) dispatch(m rpcMsg) {
|
||||
if len(m.ID) == 0 {
|
||||
return
|
||||
}
|
||||
key := idKey(m.ID)
|
||||
t.mu.Lock()
|
||||
ch := t.pending[key]
|
||||
delete(t.pending, key)
|
||||
t.mu.Unlock()
|
||||
if ch != nil {
|
||||
ch <- m
|
||||
}
|
||||
}
|
||||
|
||||
func (t *stdioTransport) failAll(err error) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
for id, ch := range t.pending {
|
||||
ch <- rpcMsg{Error: &rpcError{Message: err.Error()}}
|
||||
delete(t.pending, id)
|
||||
}
|
||||
}
|
||||
|
||||
// idKey 把 id 归一化成 map 键:去掉引号与空白。有些实现会把数字 id 回成字符串。
|
||||
func idKey(raw json.RawMessage) string {
|
||||
return strings.Trim(strings.TrimSpace(string(raw)), `"`)
|
||||
}
|
||||
Reference in new issue
Block a user