@@ -79,9 +79,19 @@ function sanitizeWindowsFilename(value: string): string {
7979 return value . replace ( / [ < > : " / \\ | ? * ] / g, "_" ) . replace ( / \p{ Cc} / gu, "_" ) ;
8080}
8181
82- function resolveStartupEntryPath ( env : GatewayServiceEnv ) : string {
82+ function resolveStartupEntryPath ( env : GatewayServiceEnv , extension ?: "cmd" | "vbs" ) : string {
8383 const taskName = resolveTaskName ( env ) ;
84- return path . join ( resolveWindowsStartupDir ( env ) , `${ sanitizeWindowsFilename ( taskName ) } .cmd` ) ;
84+ const entryExtension = extension ?? ( shouldUseHiddenWindowsTaskLauncher ( env ) ? "vbs" : "cmd" ) ;
85+ return path . join (
86+ resolveWindowsStartupDir ( env ) ,
87+ `${ sanitizeWindowsFilename ( taskName ) } .${ entryExtension } ` ,
88+ ) ;
89+ }
90+
91+ function resolveStartupEntryPaths ( env : GatewayServiceEnv ) : string [ ] {
92+ const primaryPath = resolveStartupEntryPath ( env ) ;
93+ const legacyCmdPath = resolveStartupEntryPath ( env , "cmd" ) ;
94+ return Array . from ( new Set ( [ primaryPath , legacyCmdPath ] ) ) ;
8595}
8696
8797// `/TR` is parsed by schtasks itself, while the generated `gateway.cmd` line is parsed by cmd.exe.
@@ -108,6 +118,19 @@ function resolveTaskUser(env: GatewayServiceEnv): string | null {
108118 return username ;
109119}
110120
121+ function shouldUseHiddenWindowsTaskLauncher ( env : GatewayServiceEnv ) : boolean {
122+ const value = normalizeLowercaseStringOrEmpty ( env . OPENCLAW_WINDOWS_TASK_HIDDEN_LAUNCHER ) ;
123+ return value === "1" || value === "true" || value === "yes" ;
124+ }
125+
126+ function resolveTaskLauncherScriptPath ( env : GatewayServiceEnv , scriptPath : string ) : string {
127+ if ( ! shouldUseHiddenWindowsTaskLauncher ( env ) ) {
128+ return scriptPath ;
129+ }
130+ const parsed = path . parse ( scriptPath ) ;
131+ return path . join ( parsed . dir , `${ parsed . name } .vbs` ) ;
132+ }
133+
111134export async function readScheduledTaskCommand (
112135 env : GatewayServiceEnv ,
113136) : Promise < GatewayServiceCommandConfig | null > {
@@ -292,6 +315,27 @@ function buildStartupLauncherScript(params: { description?: string; scriptPath:
292315 return `${ lines . join ( "\r\n" ) } \r\n` ;
293316}
294317
318+ function quoteVbsString ( value : string ) : string {
319+ return `"${ value . replace ( / " / g, '""' ) } "` ;
320+ }
321+
322+ function quoteVbsRunCommand ( scriptPath : string ) : string {
323+ return quoteVbsString ( `"${ scriptPath } "` ) ;
324+ }
325+
326+ function buildHiddenLauncherScript ( params : { description ?: string ; scriptPath : string } ) : string {
327+ const lines = [ ] ;
328+ const trimmedDescription = params . description ?. trim ( ) ;
329+ if ( trimmedDescription ) {
330+ assertNoCmdLineBreak ( trimmedDescription , "Hidden launcher description" ) ;
331+ lines . push ( `' ${ trimmedDescription } ` ) ;
332+ }
333+ lines . push (
334+ `CreateObject("WScript.Shell").Run ${ quoteVbsRunCommand ( params . scriptPath ) } , 0, False` ,
335+ ) ;
336+ return `${ lines . join ( "\r\n" ) } \r\n` ;
337+ }
338+
295339async function assertSchtasksAvailable ( ) {
296340 const res = await execSchtasks ( [ "/Query" ] ) ;
297341 if ( res . code === 0 ) {
@@ -302,12 +346,13 @@ async function assertSchtasksAvailable() {
302346}
303347
304348async function isStartupEntryInstalled ( env : GatewayServiceEnv ) : Promise < boolean > {
305- try {
306- await fs . access ( resolveStartupEntryPath ( env ) ) ;
307- return true ;
308- } catch {
309- return false ;
349+ for ( const startupEntryPath of resolveStartupEntryPaths ( env ) ) {
350+ try {
351+ await fs . access ( startupEntryPath ) ;
352+ return true ;
353+ } catch { }
310354 }
355+ return false ;
311356}
312357
313358async function isRegisteredScheduledTask ( env : GatewayServiceEnv ) : Promise < boolean > {
@@ -605,10 +650,12 @@ async function writeScheduledTaskScript({
605650 description,
606651} : Omit < GatewayServiceInstallArgs , "stdout" > ) : Promise < {
607652 scriptPath : string ;
653+ taskLaunchPath : string ;
608654 taskDescription : string ;
609655} > {
610656 await assertSchtasksAvailable ( ) . catch ( ( ) => undefined ) ;
611657 const scriptPath = resolveTaskScriptPath ( env ) ;
658+ const taskLaunchPath = resolveTaskLauncherScriptPath ( env , scriptPath ) ;
612659 await fs . mkdir ( path . dirname ( scriptPath ) , { recursive : true } ) ;
613660 const taskDescription = resolveGatewayServiceDescription ( { env, environment, description } ) ;
614661 const script = buildTaskScript ( {
@@ -618,7 +665,14 @@ async function writeScheduledTaskScript({
618665 environment,
619666 } ) ;
620667 await fs . writeFile ( scriptPath , script , "utf8" ) ;
621- return { scriptPath, taskDescription } ;
668+ if ( taskLaunchPath !== scriptPath ) {
669+ const launcher = buildHiddenLauncherScript ( {
670+ description : taskDescription ,
671+ scriptPath,
672+ } ) ;
673+ await fs . writeFile ( taskLaunchPath , launcher , "utf8" ) ;
674+ }
675+ return { scriptPath, taskLaunchPath, taskDescription } ;
622676}
623677
624678export async function stageScheduledTask ( {
@@ -636,7 +690,7 @@ async function updateExistingScheduledTask(params: {
636690 env : GatewayServiceEnv ;
637691 stdout : NodeJS . WritableStream ;
638692 taskName : string ;
639- quotedScript : string ;
693+ quotedLaunchPath : string ;
640694 scriptPath : string ;
641695} ) : Promise < boolean > {
642696 if ( ! ( await isRegisteredScheduledTask ( params . env ) ) ) {
@@ -647,7 +701,7 @@ async function updateExistingScheduledTask(params: {
647701 "/TN" ,
648702 params . taskName ,
649703 "/TR" ,
650- params . quotedScript ,
704+ params . quotedLaunchPath ,
651705 ] ) ;
652706 if ( change . code !== 0 ) {
653707 return false ;
@@ -820,14 +874,15 @@ async function activateScheduledTask(params: {
820874 env : GatewayServiceEnv ;
821875 stdout : NodeJS . WritableStream ;
822876 scriptPath : string ;
877+ taskLaunchPath : string ;
823878 description ?: string ;
824879} ) {
825880 const taskDescription = params . description ?? "OpenClaw Gateway" ;
826881
827882 const taskName = resolveTaskName ( params . env ) ;
828- const quotedScript = quoteSchtasksArg ( params . scriptPath ) ;
883+ const quotedLaunchPath = quoteSchtasksArg ( params . taskLaunchPath ) ;
829884
830- if ( await updateExistingScheduledTask ( { ...params , taskName, quotedScript } ) ) {
885+ if ( await updateExistingScheduledTask ( { ...params , taskName, quotedLaunchPath } ) ) {
831886 return ;
832887 }
833888
@@ -841,11 +896,12 @@ async function activateScheduledTask(params: {
841896 "/TN" ,
842897 taskName ,
843898 "/TR" ,
844- quotedScript ,
899+ quotedLaunchPath ,
845900 ] ;
846901 const taskUser = resolveTaskUser ( params . env ) ;
902+ const taskUserArgs = taskUser ? [ "/RU" , taskUser , "/NP" , "/IT" ] : [ ] ;
847903 let create = await execSchtasks (
848- taskUser ? [ ...baseArgs , "/RU" , taskUser , "/NP" , "/IT" ] : baseArgs ,
904+ taskUserArgs . length > 0 ? [ ...baseArgs , ... taskUserArgs ] : baseArgs ,
849905 ) ;
850906 if ( create . code !== 0 && taskUser ) {
851907 create = await execSchtasks ( baseArgs ) ;
@@ -855,10 +911,15 @@ async function activateScheduledTask(params: {
855911 if ( shouldFallbackToStartupEntry ( { code : create . code , detail } ) ) {
856912 const startupEntryPath = resolveStartupEntryPath ( params . env ) ;
857913 await fs . mkdir ( path . dirname ( startupEntryPath ) , { recursive : true } ) ;
858- const launcher = buildStartupLauncherScript ( {
859- description : taskDescription ,
860- scriptPath : params . scriptPath ,
861- } ) ;
914+ const launcher = shouldUseHiddenWindowsTaskLauncher ( params . env )
915+ ? buildHiddenLauncherScript ( {
916+ description : taskDescription ,
917+ scriptPath : params . scriptPath ,
918+ } )
919+ : buildStartupLauncherScript ( {
920+ description : taskDescription ,
921+ scriptPath : params . scriptPath ,
922+ } ) ;
862923 await fs . writeFile ( startupEntryPath , launcher , "utf8" ) ;
863924 await launchFallbackTaskScript ( params . env ) ;
864925 writeFormattedLines (
@@ -898,6 +959,7 @@ export async function installScheduledTask(
898959 env : args . env ,
899960 stdout : args . stdout ,
900961 scriptPath : staged . scriptPath ,
962+ taskLaunchPath : staged . taskLaunchPath ,
901963 description : staged . taskDescription ,
902964 } ) ;
903965 return { scriptPath : staged . scriptPath } ;
@@ -914,13 +976,21 @@ export async function uninstallScheduledTask({
914976 await execSchtasks ( [ "/Delete" , "/F" , "/TN" , taskName ] ) ;
915977 }
916978
917- const startupEntryPath = resolveStartupEntryPath ( env ) ;
918- try {
919- await fs . unlink ( startupEntryPath ) ;
920- stdout . write ( `${ formatLine ( "Removed Windows login item" , startupEntryPath ) } \n` ) ;
921- } catch { }
979+ for ( const startupEntryPath of resolveStartupEntryPaths ( env ) ) {
980+ try {
981+ await fs . unlink ( startupEntryPath ) ;
982+ stdout . write ( `${ formatLine ( "Removed Windows login item" , startupEntryPath ) } \n` ) ;
983+ } catch { }
984+ }
922985
923986 const scriptPath = resolveTaskScriptPath ( env ) ;
987+ const launcherPath = resolveTaskLauncherScriptPath ( env , scriptPath ) ;
988+ if ( launcherPath !== scriptPath ) {
989+ try {
990+ await fs . unlink ( launcherPath ) ;
991+ stdout . write ( `${ formatLine ( "Removed task launcher" , launcherPath ) } \n` ) ;
992+ } catch { }
993+ }
924994 try {
925995 await fs . unlink ( scriptPath ) ;
926996 stdout . write ( `${ formatLine ( "Removed task script" , scriptPath ) } \n` ) ;
0 commit comments