@@ -14,6 +14,10 @@ pub struct GatewayStatus {
1414 pub pid : Option < u32 > ,
1515}
1616
17+ /// Pinned OpenClaw version. 2026.3.31 broke bundled channel plugin loading
18+ /// (Telegram etc. fail to register). Remove pin once upstream fix #58782 ships.
19+ pub const REQUIRED_OPENCLAW_VERSION : & str = "2026.3.28" ;
20+
1721pub ( crate ) fn maxauto_dir ( ) -> PathBuf {
1822 dirs:: home_dir ( )
1923 . expect ( "Could not determine home directory" )
@@ -43,6 +47,125 @@ fn node_binary() -> PathBuf {
4347 PathBuf :: from ( if cfg ! ( windows) { "node.exe" } else { "node" } )
4448}
4549
50+ /// Read the installed OpenClaw version from its package.json.
51+ fn installed_openclaw_version ( ) -> Option < String > {
52+ let pkg = maxauto_dir ( )
53+ . join ( "openclaw" )
54+ . join ( "node_modules" )
55+ . join ( "openclaw" )
56+ . join ( "package.json" ) ;
57+ let raw = std:: fs:: read_to_string ( pkg) . ok ( ) ?;
58+ let parsed: serde_json:: Value = serde_json:: from_str ( & raw ) . ok ( ) ?;
59+ parsed. get ( "version" ) ?. as_str ( ) . map ( |s| s. to_string ( ) )
60+ }
61+
62+ /// Ensure the installed OpenClaw version matches `REQUIRED_OPENCLAW_VERSION`.
63+ /// If it doesn't (or isn't installed), reinstall the correct version via npm.
64+ async fn ensure_openclaw_version ( app : & AppHandle ) -> Result < ( ) , String > {
65+ if let Some ( v) = installed_openclaw_version ( ) {
66+ if v == REQUIRED_OPENCLAW_VERSION {
67+ return Ok ( ( ) ) ;
68+ }
69+ let _ = app. emit ( "gateway-log" , & format ! (
70+ "OpenClaw version mismatch: installed {} but require {}. Reinstalling..." ,
71+ v, REQUIRED_OPENCLAW_VERSION
72+ ) ) ;
73+ } else {
74+ let _ = app. emit ( "gateway-log" , & format ! (
75+ "OpenClaw version unknown. Installing {}..." ,
76+ REQUIRED_OPENCLAW_VERSION
77+ ) ) ;
78+ }
79+
80+ let base_dir = maxauto_dir ( ) ;
81+ let openclaw_prefix = base_dir. join ( "openclaw" ) ;
82+ std:: fs:: create_dir_all ( & openclaw_prefix)
83+ . map_err ( |e| format ! ( "Failed to create openclaw dir: {}" , e) ) ?;
84+
85+ let node = node_binary ( ) ;
86+
87+ // Build PATH with local node/git bin dirs
88+ let node_bin_dir = if cfg ! ( windows) {
89+ base_dir. join ( "node" )
90+ } else {
91+ base_dir. join ( "node" ) . join ( "bin" )
92+ } ;
93+ let git_bin_dir = if cfg ! ( windows) {
94+ base_dir. join ( "git" ) . join ( "cmd" )
95+ } else {
96+ base_dir. join ( "git" ) . join ( "bin" )
97+ } ;
98+ let path_sep = if cfg ! ( windows) { ";" } else { ":" } ;
99+ let new_path = {
100+ let mut parts = vec ! [ node_bin_dir. to_string_lossy( ) . to_string( ) ] ;
101+ if git_bin_dir. exists ( ) {
102+ parts. push ( git_bin_dir. to_string_lossy ( ) . to_string ( ) ) ;
103+ }
104+ if let Ok ( existing) = std:: env:: var ( "PATH" ) {
105+ parts. push ( existing) ;
106+ }
107+ parts. join ( path_sep)
108+ } ;
109+
110+ let npm_cache = base_dir. join ( "npm-cache" ) ;
111+ std:: fs:: create_dir_all ( & npm_cache) . ok ( ) ;
112+
113+ let pkg_spec = format ! ( "openclaw@{}" , REQUIRED_OPENCLAW_VERSION ) ;
114+
115+ // Find npm-cli.js for local node
116+ let local_npm_cli = if cfg ! ( windows) {
117+ base_dir. join ( "node" ) . join ( "node_modules" ) . join ( "npm" ) . join ( "bin" ) . join ( "npm-cli.js" )
118+ } else {
119+ base_dir. join ( "node" ) . join ( "lib" ) . join ( "node_modules" ) . join ( "npm" ) . join ( "bin" ) . join ( "npm-cli.js" )
120+ } ;
121+
122+ let output = if local_npm_cli. exists ( ) {
123+ tokio:: process:: Command :: new ( & node)
124+ . env ( "PATH" , & new_path)
125+ . env ( "npm_config_cache" , npm_cache. to_str ( ) . unwrap ( ) )
126+ . env ( "GIT_CONFIG_COUNT" , "1" )
127+ . env ( "GIT_CONFIG_KEY_0" , "url.https://github.com/.insteadOf" )
128+ . env ( "GIT_CONFIG_VALUE_0" , "ssh://git@github.com/" )
129+ . arg ( local_npm_cli. to_str ( ) . unwrap ( ) )
130+ . args ( [ "install" , "--prefix" , openclaw_prefix. to_str ( ) . unwrap ( ) , & pkg_spec] )
131+ . output ( )
132+ . await
133+ . map_err ( |e| format ! ( "npm install failed: {}" , e) ) ?
134+ } else {
135+ let npm_cmd = if cfg ! ( windows) { "npm.cmd" } else { "npm" } ;
136+ tokio:: process:: Command :: new ( npm_cmd)
137+ . env ( "PATH" , & new_path)
138+ . env ( "npm_config_cache" , npm_cache. to_str ( ) . unwrap ( ) )
139+ . env ( "GIT_CONFIG_COUNT" , "1" )
140+ . env ( "GIT_CONFIG_KEY_0" , "url.https://github.com/.insteadOf" )
141+ . env ( "GIT_CONFIG_VALUE_0" , "ssh://git@github.com/" )
142+ . args ( [ "install" , "--prefix" , openclaw_prefix. to_str ( ) . unwrap ( ) , & pkg_spec] )
143+ . output ( )
144+ . await
145+ . map_err ( |e| format ! ( "npm install failed: {}" , e) ) ?
146+ } ;
147+
148+ if !output. status . success ( ) {
149+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
150+ let errors: String = stderr
151+ . lines ( )
152+ . filter ( |l| !l. trim_start ( ) . starts_with ( "npm notice" ) )
153+ . collect :: < Vec < _ > > ( )
154+ . join ( "\n " ) ;
155+ return Err ( format ! (
156+ "Failed to install OpenClaw {}: {}" ,
157+ REQUIRED_OPENCLAW_VERSION ,
158+ errors. trim( )
159+ ) ) ;
160+ }
161+
162+ let _ = app. emit ( "gateway-log" , & format ! (
163+ "OpenClaw {} installed successfully" ,
164+ REQUIRED_OPENCLAW_VERSION
165+ ) ) ;
166+ Ok ( ( ) )
167+ }
168+
46169pub ( crate ) fn generate_token ( ) -> String {
47170 use std:: collections:: hash_map:: RandomState ;
48171 use std:: hash:: { BuildHasher , Hasher } ;
@@ -322,6 +445,9 @@ pub async fn start_gateway(
322445 ) ) ;
323446 }
324447
448+ // Ensure installed version matches the pinned version before launching
449+ ensure_openclaw_version ( & app) . await ?;
450+
325451 let mut cmd = tokio:: process:: Command :: new ( & node) ;
326452 cmd. arg ( & entry)
327453 . args ( [ "gateway" , "run" , "--bind" , & bind, "--port" , & port. to_string ( ) ] )
0 commit comments