diff --git a/README.md b/README.md index d5ad05c..e4a3047 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ - `action=status|describe`:查看已配对节点状态与能力矩阵 - `action=run|invoke|camera_snap|screen_record|location_get`:已接入路由框架 - `mode=auto|p2p|relay`:默认 `auto`(优先 p2p,失败回退 relay) +- relay 已接入本地 handler 调用链,便于逐步替换为真实跨节点传输 实现位置: - `pkg/nodes/types.go` diff --git a/README_EN.md b/README_EN.md index 7d5e354..7ca15a2 100644 --- a/README_EN.md +++ b/README_EN.md @@ -42,6 +42,7 @@ A `nodes` tool control-plane PoC is now available: - `action=status|describe`: inspect paired node status and capability matrix - `action=run|invoke|camera_snap|screen_record|location_get`: routing framework is in place - `mode=auto|p2p|relay`: default `auto` (prefer p2p, fallback to relay) +- relay now uses local handler invocation path, ready for real cross-node transport replacement Implementation: - `pkg/nodes/types.go` diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 9d083c0..179838b 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -78,7 +78,11 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers toolsRegistry.Register(tools.NewExecTool(cfg.Tools.Shell, workspace, processManager)) toolsRegistry.Register(tools.NewProcessTool(processManager)) nodesManager := nodes.NewManager() - nodesRouter := &nodes.Router{P2P: &nodes.StubP2PTransport{}, Relay: &nodes.StubRelayTransport{}} + nodesManager.Upsert(nodes.NodeInfo{ID: "local", Name: "local", Capabilities: nodes.Capabilities{Run: true, Invoke: true}, Online: true}) + nodesManager.RegisterHandler("local", func(req nodes.Request) nodes.Response { + return nodes.Response{OK: true, Node: "local", Action: req.Action, Payload: map[string]interface{}{"echo": req.Args, "transport": "relay-local"}} + }) + nodesRouter := &nodes.Router{P2P: &nodes.StubP2PTransport{}, Relay: &nodes.StubRelayTransport{Manager: nodesManager}} toolsRegistry.Register(tools.NewNodesTool(nodesManager, nodesRouter)) if cs != nil { diff --git a/pkg/nodes/manager.go b/pkg/nodes/manager.go index a475dcf..0fa88cc 100644 --- a/pkg/nodes/manager.go +++ b/pkg/nodes/manager.go @@ -2,18 +2,22 @@ package nodes import ( "sort" + "strings" "sync" "time" ) // Manager keeps paired node metadata and basic routing helpers. +type Handler func(req Request) Response + type Manager struct { - mu sync.RWMutex - nodes map[string]NodeInfo + mu sync.RWMutex + nodes map[string]NodeInfo + handlers map[string]Handler } func NewManager() *Manager { - return &Manager{nodes: map[string]NodeInfo{}} + return &Manager{nodes: map[string]NodeInfo{}, handlers: map[string]Handler{}} } func (m *Manager) Upsert(info NodeInfo) { @@ -51,6 +55,32 @@ func (m *Manager) List() []NodeInfo { return out } +func (m *Manager) RegisterHandler(nodeID string, h Handler) { + m.mu.Lock() + defer m.mu.Unlock() + if strings.TrimSpace(nodeID) == "" || h == nil { + return + } + m.handlers[nodeID] = h +} + +func (m *Manager) Invoke(req Request) (Response, bool) { + m.mu.RLock() + h, ok := m.handlers[req.Node] + m.mu.RUnlock() + if !ok { + return Response{}, false + } + resp := h(req) + if strings.TrimSpace(resp.Node) == "" { + resp.Node = req.Node + } + if strings.TrimSpace(resp.Action) == "" { + resp.Action = req.Action + } + return resp, true +} + func (m *Manager) PickFor(action string) (NodeInfo, bool) { m.mu.RLock() defer m.mu.RUnlock() diff --git a/pkg/nodes/transport.go b/pkg/nodes/transport.go index bc6a6e5..d75c994 100644 --- a/pkg/nodes/transport.go +++ b/pkg/nodes/transport.go @@ -57,10 +57,16 @@ func (s *StubP2PTransport) Send(ctx context.Context, req Request) (Response, err } // StubRelayTransport provides executable placeholder until real bridge lands. -type StubRelayTransport struct{} +type StubRelayTransport struct{ Manager *Manager } func (s *StubRelayTransport) Name() string { return "relay" } func (s *StubRelayTransport) Send(ctx context.Context, req Request) (Response, error) { _ = ctx - return Response{OK: false, Node: req.Node, Action: req.Action, Error: "relay bridge not implemented yet"}, nil + if s.Manager == nil { + return Response{OK: false, Node: req.Node, Action: req.Action, Error: "relay manager not configured"}, nil + } + if resp, ok := s.Manager.Invoke(req); ok { + return resp, nil + } + return Response{OK: false, Node: req.Node, Action: req.Action, Error: "relay handler not found for node"}, nil } diff --git a/pkg/tools/memory.go b/pkg/tools/memory.go index c0a5946..387d9be 100644 --- a/pkg/tools/memory.go +++ b/pkg/tools/memory.go @@ -26,8 +26,9 @@ type memoryBlock struct { } type cachedMemoryFile struct { - modTime time.Time - blocks []memoryBlock + modTime time.Time + blocks []memoryBlock + tokenIndex map[string][]int } func NewMemorySearchTool(workspace string) *MemorySearchTool { @@ -206,12 +207,17 @@ func dedupeStrings(items []string) []string { // searchFile searches parsed markdown blocks with cache by file modtime. func (t *MemorySearchTool) searchFile(path string, keywords []string) ([]searchResult, error) { - blocks, err := t.getOrParseBlocks(path) + blocks, tokenIndex, err := t.getOrParseBlocks(path) if err != nil { return nil, err } + candidate := candidateBlockIndexes(tokenIndex, keywords, len(blocks)) results := make([]searchResult, 0, 8) - for _, b := range blocks { + for _, idx := range candidate { + if idx < 0 || idx >= len(blocks) { + continue + } + b := blocks[idx] score := 0 for _, kw := range keywords { if strings.Contains(b.lower, kw) { @@ -233,34 +239,35 @@ func (t *MemorySearchTool) searchFile(path string, keywords []string) ([]searchR return results, nil } -func (t *MemorySearchTool) getOrParseBlocks(path string) ([]memoryBlock, error) { +func (t *MemorySearchTool) getOrParseBlocks(path string) ([]memoryBlock, map[string][]int, error) { st, err := os.Stat(path) if err != nil { - return nil, err + return nil, nil, err } mod := st.ModTime() t.mu.RLock() if c, ok := t.cache[path]; ok && c.modTime.Equal(mod) { blocks := c.blocks + idx := c.tokenIndex t.mu.RUnlock() - return blocks, nil + return blocks, idx, nil } t.mu.RUnlock() - blocks, err := parseMarkdownBlocks(path) + blocks, tokenIndex, err := parseMarkdownBlocks(path) if err != nil { - return nil, err + return nil, nil, err } t.mu.Lock() - t.cache[path] = cachedMemoryFile{modTime: mod, blocks: blocks} + t.cache[path] = cachedMemoryFile{modTime: mod, blocks: blocks, tokenIndex: tokenIndex} t.mu.Unlock() - return blocks, nil + return blocks, tokenIndex, nil } -func parseMarkdownBlocks(path string) ([]memoryBlock, error) { +func parseMarkdownBlocks(path string) ([]memoryBlock, map[string][]int, error) { file, err := os.Open(path) if err != nil { - return nil, err + return nil, nil, err } defer file.Close() @@ -312,5 +319,78 @@ func parseMarkdownBlocks(path string) ([]memoryBlock, error) { current.WriteString(line + "\n") } flush() - return blocks, nil + tokenIndex := buildTokenIndex(blocks) + return blocks, tokenIndex, nil +} + +func buildTokenIndex(blocks []memoryBlock) map[string][]int { + idx := make(map[string][]int, 256) + for i, b := range blocks { + seen := map[string]struct{}{} + for _, tok := range tokenizeForIndex(b.lower + " " + strings.ToLower(b.heading)) { + if _, ok := seen[tok]; ok { + continue + } + seen[tok] = struct{}{} + idx[tok] = append(idx[tok], i) + } + } + return idx +} + +func tokenizeForIndex(s string) []string { + s = strings.ToLower(strings.TrimSpace(s)) + if s == "" { + return nil + } + s = strings.NewReplacer("\n", " ", "\t", " ", ",", " ", ".", " ", ":", " ", ";", " ", "(", " ", ")", " ", "[", " ", "]", " ", "{", " ", "}", " ", "`", " ", "\"", " ", "'", " ").Replace(s) + parts := strings.Fields(s) + out := make([]string, 0, len(parts)) + for _, p := range parts { + if len(p) < 2 { + continue + } + out = append(out, p) + } + return out +} + +func candidateBlockIndexes(tokenIndex map[string][]int, keywords []string, total int) []int { + if total <= 0 { + return nil + } + if len(keywords) == 0 || len(tokenIndex) == 0 { + out := make([]int, 0, total) + for i := 0; i < total; i++ { + out = append(out, i) + } + return out + } + candMap := map[int]int{} + for _, kw := range keywords { + kw = strings.TrimSpace(strings.ToLower(kw)) + if kw == "" { + continue + } + for tok, ids := range tokenIndex { + if strings.Contains(tok, kw) { + for _, id := range ids { + candMap[id]++ + } + } + } + } + if len(candMap) == 0 { + out := make([]int, 0, total) + for i := 0; i < total; i++ { + out = append(out, i) + } + return out + } + out := make([]int, 0, len(candMap)) + for id := range candMap { + out = append(out, id) + } + sort.Slice(out, func(i, j int) bool { return candMap[out[i]] > candMap[out[j]] }) + return out }