mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 06:17:25 +00:00
feat(examples): 内置插件外置化 —— cmd/localuse/mcp 迁入 example + files 新增按行号精确编辑
## 背景 主仓按「只有必须使用内部 SDK 能力的插件才内置」的判定标准(见主仓 ARCHITECTURE「内置插件的判定标准」)把纯公开 SDK 插件移出编译期内置清单。 本仓为它们提供外部插件工程(example/ 下,hmapdev 可直接构建)。 ★ 终审翻案:kbtree 与 multimodal **不**在本次外置之列 —— 逐方法对照 公开 SDK 后发现它们依赖内部专有能力(kbtree 要 KnowledgeAPI 的 Subtree/CategoryCounts/SearchIn/Categories 与树视图类型;multimodal 要 LLMAPI.Chat 与多模态消息类型),公开面没有,已加回主仓内置清单。 ## 新增工程(全部对齐当前模板 + 随用随起契约) | 工程 | 工具 | auto_start | 说明 | |---|---|---|---| | cmd | cmd_run | false(按需) | 沙箱执行 shell 命令 | | localuse | screensee/screensue/camerasue/speakeruse/clipboardsee/clipboardsue/computeruse | false(按需) | 本机外设 | | mcp | mcp_add/list/remove/restart_server | false(按需) | MCP 服务器接入(stdio/SSE) | 每工程:plg.json(含 sdk 字段声明)+ skill/<name>-tools/SKILL.md + README。按 auto_start 契约(sdk/autostart.go 的 OnDemandNeedsSkill), **auto_start=false 必须配 skill**,否则工具不出现在初始清单 ⇒ 模型不知道 它存在 ⇒ 永远不会被拉起,等于没装还占地方。 ## 移植要点 - 注册:内置版 init()+RegisterFactory → 外部版导出 NewPluginFactory; RegisterPluginMeta 的显示名进 plg.json/plugin.json。 - mcp:本地 rpcRequest/rpcResponse/rpcError 与 hmapdev 生成的 z_proc_gen.go 同名冲突(构建期生成物与本目录源码同包编译), 本地类型改名 mcpRPC*;internal/safeclose 内联为 safeCloseChan (外部插件不能引 internal 包,close-of-closed-channel 纪律随之带过来)。 - module 名:cmd 的 go.mod 用 homeagent-cmd(避免与标准库 cmd 混淆); name_en 对齐插件名,hmapdev 以 toSnake(name_en) 命名产物 (此前误写 Command/Local,产物成了 command_/local_)。 ## files:新增 files_edit_line(按行号精确编辑) files_edit 要求 old 文本全文件唯一,改「第三个 case 的 break」这类 非唯一文本时只能退化为整文件重写(有损)。files_edit_line 按行号 定位,op=replace/insert/delete,支持: - 多条 edit 从后往前应用(前面行号不漂移) - expect 安全闸:行内容与预期不符则整体拒绝(防行号过期改错位置) - CRLF/末尾换行保真 skill 同步补 edit_line 行。测试 5 条(含 expect 拒绝路径)。 ## 验证 hmapdev build 四工程全过:cmd/localuse/mcp/files 产物齐; go test files 全绿。
This commit is contained in:
161
example/mcp/stdio.go
Normal file
161
example/mcp/stdio.go
Normal file
@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
// safeCloseChan 是主仓 internal/safeclose.Safe 的内联版(外部插件不能引
|
||||
// internal 包)。close of closed channel 是真机 SIGTERM 实测过的 panic:
|
||||
// 多个退出路径(错误/超时/重连)都可能走到同一根 channel 的 close,
|
||||
// 所以除本定义外不得出现裸 close(ch)——一律走这里。
|
||||
func safeCloseChan[T any](once *sync.Once, ch chan T) bool {
|
||||
if once == nil || ch == nil {
|
||||
return false
|
||||
}
|
||||
fired := false
|
||||
once.Do(func() {
|
||||
fired = true
|
||||
close(ch)
|
||||
})
|
||||
return fired
|
||||
}
|
||||
|
||||
// StdioTransport 通过子进程 stdin/stdout 进行 JSON-RPC 通信
|
||||
// pendingRPC 是待应答通道 + 它自己的 close 守卫(本仓 close 纪律)。
|
||||
type pendingRPC struct {
|
||||
ch chan *mcpRPCResponse
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (p *pendingRPC) closeOnce() { safeCloseChan(&p.once, p.ch) }
|
||||
|
||||
type StdioTransport struct {
|
||||
doneOnce sync.Once
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdout *bufio.Reader
|
||||
mu sync.Mutex
|
||||
pending map[int]*pendingRPC
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewStdioTransport(command string, args []string, env []string) (*StdioTransport, error) {
|
||||
cmd := exec.Command(command, args...)
|
||||
if len(env) > 0 {
|
||||
cmd.Env = append(os.Environ(), 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(MCP 服务器可能输出日志到 stderr)
|
||||
cmd.Stderr = nil
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("start %s: %w", command, err)
|
||||
}
|
||||
|
||||
t := &StdioTransport{
|
||||
cmd: cmd,
|
||||
stdin: stdin,
|
||||
stdout: bufio.NewReader(stdout),
|
||||
pending: make(map[int]*pendingRPC),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
go t.readLoop()
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *StdioTransport) readLoop() {
|
||||
dec := json.NewDecoder(t.stdout)
|
||||
for {
|
||||
var resp mcpRPCResponse
|
||||
if err := dec.Decode(&resp); err != nil {
|
||||
safeCloseChan(&t.doneOnce, t.done)
|
||||
// 通知所有等待的请求
|
||||
t.mu.Lock()
|
||||
for _, pr := range t.pending {
|
||||
pr.closeOnce()
|
||||
}
|
||||
t.pending = make(map[int]*pendingRPC)
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
pr, ok := t.pending[resp.ID]
|
||||
delete(t.pending, resp.ID)
|
||||
t.mu.Unlock()
|
||||
|
||||
if ok {
|
||||
pr.ch <- &resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *StdioTransport) Send(req *mcpRPCRequest) (*mcpRPCResponse, error) {
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
ch := make(chan *mcpRPCResponse, 1)
|
||||
t.mu.Lock()
|
||||
t.pending[req.ID] = &pendingRPC{ch: ch}
|
||||
t.mu.Unlock()
|
||||
|
||||
if _, err := t.stdin.Write(data); err != nil {
|
||||
t.mu.Lock()
|
||||
delete(t.pending, req.ID)
|
||||
t.mu.Unlock()
|
||||
return nil, fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if _, err := t.stdin.Write([]byte("\n")); err != nil {
|
||||
t.mu.Lock()
|
||||
delete(t.pending, req.ID)
|
||||
t.mu.Unlock()
|
||||
return nil, fmt.Errorf("write newline: %w", err)
|
||||
}
|
||||
|
||||
// 超时保护:MCP server 进程启动慢/卡死不响应时,不能让插件加载永久阻塞
|
||||
// (stdio server 冷启动如 npx 拉包可能数十秒,给 60s)
|
||||
timeout := time.After(60 * time.Second)
|
||||
select {
|
||||
case resp := <-ch:
|
||||
return resp, nil
|
||||
case <-t.done:
|
||||
t.mu.Lock()
|
||||
delete(t.pending, req.ID)
|
||||
t.mu.Unlock()
|
||||
return nil, fmt.Errorf("mcp transport closed")
|
||||
case <-timeout:
|
||||
t.mu.Lock()
|
||||
delete(t.pending, req.ID)
|
||||
t.mu.Unlock()
|
||||
return nil, fmt.Errorf("mcp request timeout (60s), method may be hung")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *StdioTransport) Close() error {
|
||||
t.stdin.Close()
|
||||
if t.cmd.Process != nil {
|
||||
t.cmd.Process.Kill()
|
||||
}
|
||||
<-t.done
|
||||
return t.cmd.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user