// Command localsim 模拟桌面端本地执行 runner(联调工具,无需起真 Wails 桌面端): // 连 gateway 的 /api/v1/local/runner WS,注册为当前用户的本地执行器, // 在指定工作目录内响应 local_list_dir / local_read_file(只读,与桌面端同协议)。 // // 用法:LOCALSIM_TOKEN= go run ./cmd/localsim [-gw ws://localhost:8080] [-dir /path/to/workdir] package main import ( "encoding/json" "flag" "fmt" "log" "os" "path/filepath" "strings" "github.com/gorilla/websocket" ) type req struct { ID string `json:"id"` Tool string `json:"tool"` Args map[string]any `json:"args,omitempty"` } type resp struct { ID string `json:"id"` OK bool `json:"ok"` Content string `json:"content,omitempty"` Error string `json:"error,omitempty"` Workdir string `json:"workdir,omitempty"` } func main() { gw := flag.String("gw", "ws://localhost:8080", "gateway WS 基址") dir := flag.String("dir", ".", "沙箱工作目录") flag.Parse() token := os.Getenv("LOCALSIM_TOKEN") if token == "" { log.Fatal("缺 LOCALSIM_TOKEN(用户 JWT)") } root, err := filepath.Abs(*dir) if err != nil { log.Fatal(err) } url := strings.TrimRight(*gw, "/") + "/api/v1/local/runner?token=" + token conn, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { log.Fatalf("连接失败: %v", err) } defer conn.Close() log.Printf("已注册为本地执行器 workdir=%s", root) hello, _ := json.Marshal(resp{ID: "hello", OK: true, Workdir: root}) _ = conn.WriteMessage(websocket.TextMessage, hello) for { _, data, err := conn.ReadMessage() if err != nil { log.Fatalf("连接断开: %v", err) } var r req if json.Unmarshal(data, &r) != nil { continue } log.Printf("收到调用 tool=%s args=%v", r.Tool, r.Args) out, _ := json.Marshal(handle(root, &r)) _ = conn.WriteMessage(websocket.TextMessage, out) } } func handle(root string, r *req) *resp { rel, _ := r.Args["path"].(string) p := filepath.Join(root, filepath.Clean("/"+rel)) // 简版沙箱(联调工具;正式沙箱在桌面端) switch r.Tool { case "local_list_dir": ents, err := os.ReadDir(p) if err != nil { return &resp{ID: r.ID, OK: false, Error: err.Error()} } var names []string for _, e := range ents { n := e.Name() if e.IsDir() { n += "/" } names = append(names, n) } data, _ := json.Marshal(map[string]any{"dir": rel, "entries": names}) return &resp{ID: r.ID, OK: true, Content: string(data)} case "local_read_file": b, err := os.ReadFile(p) if err != nil { return &resp{ID: r.ID, OK: false, Error: err.Error()} } if len(b) > 64*1024 { b = b[:64*1024] } return &resp{ID: r.ID, OK: true, Content: string(b)} default: return &resp{ID: r.ID, OK: false, Error: fmt.Sprintf("不支持: %s", r.Tool)} } }