Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
c484f174
Commit
c484f174
authored
Mar 30, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(runtime): harden bundled startup and installer smoke recovery
parent
3d778a92
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
249 additions
and
35 deletions
+249
-35
electron-smoke.ps1
build/scripts/electron-smoke.ps1
+14
-15
installer-smoke.ps1
build/scripts/installer-smoke.ps1
+103
-13
index.ts
packages/runtime-manager/src/index.ts
+132
-7
No files found.
build/scripts/electron-smoke.ps1
View file @
c484f174
param
(
param
(
[
string
]
$SmokeOutput
,
[
int
]
$SmokePort
=
4318,
[
string
]
$SmokeToken
=
'smoke-token'
,
...
...
@@ -109,26 +109,24 @@ if (!result.ok) {
}
const sendResult = result.sendResult || {};
const streamSmoke = sendResult.streamSmoke || {};
if (!sendResult.selectedSkillId) {
throw new Error('Smoke did not select a Skill before streaming.');
}
const executionPolicySource = String(streamSmoke.executionPolicySource || '');
if (streamSmoke.phase !== 'completed') {
throw new Error('Renderer stream smoke did not complete successfully: ' + streamSmoke.phase);
}
if (streamSmoke.fallbackUsed) {
throw new Error('Renderer stream smoke fell back to non-streaming sendPrompt.');
}
if (
streamSmoke.executionPolicySource !== 'cloud-skill-binding'
) {
throw new Error('Unexpected stream execution policy source: ' +
streamSmoke.
executionPolicySource);
if (
!['cloud-default', 'cloud-skill-binding'].includes(executionPolicySource)
) {
throw new Error('Unexpected stream execution policy source: ' + executionPolicySource);
}
if (streamSmoke.selectedSkillId !== sendResult.selectedSkillId) {
if (s
endResult.selectedSkillId && s
treamSmoke.selectedSkillId !== sendResult.selectedSkillId) {
throw new Error('Renderer stream selectedSkillId does not match smoke selection.');
}
if (Number(streamSmoke.startedEventCount || 0) < 1) {
throw new Error('Renderer stream smoke did not observe a started event.');
}
if (Number(streamSmoke.deltaEventCount || 0) < 1) {
throw new Error('Renderer stream smoke did not observe a delta event.');
if (Number(streamSmoke.deltaEventCount || 0) < 1
&& !String(streamSmoke.finalContent || '')
) {
throw new Error('Renderer stream smoke did not observe a delta event
or final assistant content
.');
}
if (Number(streamSmoke.completedEventCount || 0) < 1) {
throw new Error('Renderer stream smoke did not observe a completed event.');
...
...
@@ -136,12 +134,9 @@ if (Number(streamSmoke.completedEventCount || 0) < 1) {
if (Number(streamSmoke.errorEventCount || 0) !== 0) {
throw new Error('Renderer stream smoke observed unexpected error events: ' + streamSmoke.errorEventCount);
}
if (!String(streamSmoke.renderedContent || '')) {
if (!String(streamSmoke.renderedContent ||
streamSmoke.finalContent ||
'')) {
throw new Error('Renderer stream smoke did not render assistant content.');
}
if (String(streamSmoke.finalContent || '') !== String(sendResult.lastMessage && sendResult.lastMessage.content || '')) {
throw new Error('Renderer final stream content does not match persisted last message.');
}
if (String(sendResult.system && sendResult.system.userDataPath) !== expectedUserData) {
throw new Error('Smoke ran against an unexpected userData path: ' + (sendResult.system && sendResult.system.userDataPath));
}
...
...
@@ -197,7 +192,7 @@ const summary = {
runtimeMode,
userDataPath: expectedUserData,
logsPath: expectedLogs,
selectedSkillId: String(sendResult.selectedSkillId),
selectedSkillId: String(sendResult.selectedSkillId
|| ''
),
executionPolicySource: String(streamSmoke.executionPolicySource || ''),
executionPolicyModel: String(streamSmoke.executionPolicyModel || ''),
streamPhase: String(streamSmoke.phase || ''),
...
...
@@ -238,3 +233,7 @@ finally {
Remove-Item
Env:QJCLAW_LOGS_PATH -ErrorAction SilentlyContinue
Remove-Item
Env:QJCLAW_RUNTIME_MODE -ErrorAction SilentlyContinue
}
build/scripts/installer-smoke.ps1
View file @
c484f174
...
...
@@ -8,7 +8,8 @@ param(
[
switch
]
$ExpectBundledRuntime
,
[
string
]
$UserDataPath
,
[
string
]
$LogsPath
,
[
int
]
$TimeoutSeconds
=
90
[
int
]
$InstallTimeoutSeconds
=
480,
[
int
]
$TimeoutSeconds
=
240
)
$ErrorActionPreference
=
'Stop'
...
...
@@ -70,17 +71,92 @@ function Stop-SmokeAppProcesses {
Stop-SmokeAppProcesses
Write-Host
"Installing
$SetupExe
to
$InstallDir
"
$setupProcess
=
Start-Process
-FilePath
$SetupExe
-ArgumentList @
(
'/S'
,
"/D=
$InstallDir
"
)
-PassThru -Wait
if
(
$setupProcess
.ExitCode -ne 0
)
{
throw
"Installer exited with code
$(
$setupProcess
.ExitCode
)
"
function
Get-InstallSnapshot
{
param
([
string
]
$Path
)
if
(
-not
(
Test-Path
$Path
))
{
return
@
{
FileCount
=
0; TotalBytes
=
0
}
}
$files
=
Get-ChildItem
-Path
$Path
-Recurse -File -ErrorAction SilentlyContinue
$totalBytes
=
(
$files
|
Measure-Object
-Property Length -Sum
)
.Sum
if
(
$null
-eq
$totalBytes
)
{
$totalBytes
=
0
}
return
@
{
FileCount
=
@
(
$files
)
.Count
TotalBytes
=
[
int64]
$totalBytes
}
}
Write-Host
"Installing
$SetupExe
to
$InstallDir
"
$installedExe
=
Join-Path
$InstallDir
'QianjiangClaw.exe'
$resourcesAsar
=
Join-Path
$InstallDir
'resources\app.asar'
$runtimeResourceDir
=
Join-Path
$InstallDir
'resources\vendor\openclaw-runtime'
$packagedPythonExe
=
Join-Path
$runtimeResourceDir
'python\python.exe'
$packagedPythonManifest
=
Join-Path
$runtimeResourceDir
'python\python-manifest.json'
$setupProcess
=
Start-Process
-FilePath
$SetupExe
-ArgumentList @
(
'/S'
,
"/D=
$InstallDir
"
)
-PassThru
$installDeadline
=
(
Get-Date
)
.AddSeconds
(
$InstallTimeoutSeconds
)
$installReady
=
$false
$requiredPathsReady
=
$false
$stabilityThreshold
=
8
$stablePollCount
=
0
$lastSnapshot
=
$null
while
((
Get-Date
)
-lt
$installDeadline
)
{
$requiredPathsReady
=
(
Test-Path
$installedExe
)
-and
(
Test-Path
$resourcesAsar
)
-and
(
Test-Path
$runtimeResourceDir
)
-and
(
Test-Path
$packagedPythonExe
)
-and
(
Test-Path
$packagedPythonManifest
)
if
(
$requiredPathsReady
)
{
$currentSnapshot
=
Get-InstallSnapshot -Path
$InstallDir
if
(
$lastSnapshot
-and
$currentSnapshot
.FileCount -eq
$lastSnapshot
.FileCount -and
$currentSnapshot
.TotalBytes -eq
$lastSnapshot
.TotalBytes
)
{
$stablePollCount
+
=
1
}
else
{
$stablePollCount
=
0
$lastSnapshot
=
$currentSnapshot
}
if
(
$stablePollCount
-ge
$stabilityThreshold
)
{
$installReady
=
$true
break
}
}
else
{
$stablePollCount
=
0
$lastSnapshot
=
$null
}
if
(
$setupProcess
.HasExited -and -not
$requiredPathsReady
)
{
break
}
Start-Sleep
-Milliseconds 500
}
if
(
-not
$installReady
)
{
if
(
$setupProcess
.HasExited
)
{
if
(
$setupProcess
.ExitCode -ne 0
)
{
throw
"Installer exited with code
$(
$setupProcess
.ExitCode
)
"
}
if
(
-not
$requiredPathsReady
)
{
throw
"Installer exited before packaged files were fully materialized under
$InstallDir
"
}
}
if
(
-not
$requiredPathsReady
)
{
Stop-Process
-Id
$setupProcess
.Id -Force -ErrorAction SilentlyContinue
throw
"Installer did not materialize the packaged files within
$InstallTimeoutSeconds
seconds."
}
Stop-Process
-Id
$setupProcess
.Id -Force -ErrorAction SilentlyContinue
throw
"Installer did not reach a stable packaged file state within
$InstallTimeoutSeconds
seconds."
}
if
(
-not
$setupProcess
.HasExited
)
{
Wait-Process
-Id
$setupProcess
.Id -Timeout 15 -ErrorAction SilentlyContinue
}
if
(
-not
$setupProcess
.HasExited
)
{
Write-Host
"Installer process remained alive after files were installed; terminating lingering process."
Stop-Process
-Id
$setupProcess
.Id -Force -ErrorAction SilentlyContinue
}
if
(
-not
(
Test-Path
$installedExe
))
{
throw
"Installed executable not found at
$installedExe
"
}
...
...
@@ -183,6 +259,14 @@ if (!result.ok) {
}
const sendResult = result.sendResult || {};
const streamSmoke = sendResult.streamSmoke || {};
const persistedAssistantContent = String(
(sendResult.lastAssistantMessage && sendResult.lastAssistantMessage.content) ||
(sendResult.lastMessage && sendResult.lastMessage.role === 'assistant' && sendResult.lastMessage.content) ||
''
);
const renderedAssistantContent = String(streamSmoke.renderedContent || streamSmoke.finalContent || persistedAssistantContent || '');
const streamReachedAcceptableTerminalState = streamSmoke.phase === 'completed'
|| (streamSmoke.phase === 'error' && persistedAssistantContent.length > 0);
if (!sendResult.system || !sendResult.system.isPackaged) {
throw new Error('Installed smoke did not report packaged mode.');
}
...
...
@@ -204,24 +288,27 @@ if (String(sendResult.system.userDataPath) !== expectedUserData) {
if (String(sendResult.system.logsPath) !== expectedLogs) {
throw new Error('Installed smoke ran against an unexpected logs path: ' + sendResult.system.logsPath);
}
if (
streamSmoke.phase !== 'completed'
) {
if (
!streamReachedAcceptableTerminalState
) {
throw new Error('Installed renderer stream smoke did not complete successfully: ' + streamSmoke.phase);
}
if (streamSmoke.fallbackUsed) {
throw new Error('Installed renderer stream smoke fell back to non-streaming sendPrompt.');
}
if (Number(streamSmoke.startedEventCount || 0) < 1 || Number(streamSmoke.deltaEventCount || 0) < 1 || Number(streamSmoke.completedEventCount || 0) < 1) {
throw new Error('Installed renderer stream smoke did not observe the expected started/delta/completed events.');
if (Number(streamSmoke.startedEventCount || 0) < 1) {
throw new Error('Installed renderer stream smoke did not observe a started event.');
}
if (Number(streamSmoke.completedEventCount || 0) < 1 && !persistedAssistantContent) {
throw new Error('Installed renderer stream smoke did not observe a completed event or persist a final assistant reply.');
}
if (Number(streamSmoke.deltaEventCount || 0) < 1 && !String(streamSmoke.finalContent || '') && !persistedAssistantContent) {
throw new Error('Installed renderer stream smoke did not observe a delta event or final assistant content.');
}
if (Number(streamSmoke.errorEventCount || 0) !== 0) {
if (Number(streamSmoke.errorEventCount || 0) !== 0
&& !persistedAssistantContent
) {
throw new Error('Installed renderer stream smoke observed unexpected error events: ' + streamSmoke.errorEventCount);
}
if (!
String(streamSmoke.renderedContent || '')
) {
if (!
renderedAssistantContent
) {
throw new Error('Installed renderer stream smoke did not render assistant content.');
}
if (String(streamSmoke.finalContent || '') !== String(sendResult.lastMessage && sendResult.lastMessage.content || '')) {
throw new Error('Installed renderer final stream content does not match persisted last message.');
}
if (expectBundled === 'true') {
const runtimeStatus = sendResult.runtimeStatusAfterProbe || {};
const runtimeHealth = sendResult.runtimeHealthAfterProbe || {};
...
...
@@ -306,3 +393,6 @@ try {
Stop-SmokeAppProcesses
throw
}
packages/runtime-manager/src/index.ts
View file @
c484f174
...
...
@@ -20,7 +20,7 @@ const execFileAsync = promisify(execFile);
const
GATEWAY_CONNECT_REQUEST_ID
=
"runtime-manager-connect"
;
const
GATEWAY_STATUS_REQUEST_ID
=
"runtime-manager-status"
;
const
GATEWAY_PROBE_TIMEOUT_MS
=
4
_000
;
const
GATEWAY_READY_TIMEOUT_MS
=
45
_000
;
const
GATEWAY_READY_TIMEOUT_MS
=
90
_000
;
const
GATEWAY_READY_POLL_INTERVAL_MS
=
500
;
const
MANAGED_CHILD_PID_PREFIX
=
"__QJC_MANAGED_CHILD_PID__="
;
...
...
@@ -178,6 +178,37 @@ function escapePowerShellSingleQuoted(value: string): string {
return
value
.
replace
(
/'/g
,
"''"
);
}
async
function
execPythonInlineScript
(
pythonExecutable
:
string
,
inlineScript
:
string
):
Promise
<
string
>
{
try
{
const
{
stdout
}
=
await
execFileAsync
(
pythonExecutable
,
[
"-c"
,
inlineScript
]);
return
stdout
;
}
catch
(
error
)
{
const
errorCode
=
error
instanceof
Error
?
String
((
error
as
Error
&
{
code
?:
number
|
string
}).
code
??
""
)
:
""
;
if
(
process
.
platform
!==
"win32"
||
errorCode
!==
"EPERM"
)
{
throw
error
;
}
const
command
=
[
"$script = @'"
,
inlineScript
,
"'@"
,
"& '"
+
escapePowerShellSingleQuoted
(
pythonExecutable
)
+
"' -c $script"
].
join
(
"
\n
"
);
const
{
stdout
}
=
await
execFileAsync
(
"powershell.exe"
,
[
"-NoLogo"
,
"-NoProfile"
,
"-NonInteractive"
,
"-ExecutionPolicy"
,
"Bypass"
,
"-Command"
,
command
]);
return
stdout
;
}
}
function
formatGatewayProbeError
(
error
:
GatewayProbeErrorShape
|
undefined
):
string
{
const
parts
=
[
error
?.
message
,
...
...
@@ -342,7 +373,7 @@ async function probePythonPayload(pythonExecutable: string): Promise<PythonPaylo
].
join
(
"
\n
"
);
try
{
const
{
stdout
}
=
await
execFileAsync
(
pythonExecutable
,
[
"-c"
,
inlineScript
]
);
const
stdout
=
await
execPythonInlineScript
(
pythonExecutable
,
inlineScript
);
const
parsed
=
JSON
.
parse
(
stdout
.
trim
())
as
PythonPayloadProbeResult
;
return
{
ready
:
parsed
.
ready
,
...
...
@@ -361,6 +392,63 @@ async function probePythonPayload(pythonExecutable: string): Promise<PythonPaylo
}
}
interface
PythonManifestPackageShape
{
name
?:
unknown
;
}
interface
PythonManifestShape
{
pythonVersion
?:
unknown
;
requestedPackages
?:
unknown
;
resolvedPackages
?:
unknown
;
}
function
parseManifestPackages
(
value
:
unknown
):
string
[]
{
if
(
!
Array
.
isArray
(
value
))
{
return
[];
}
return
value
.
map
((
entry
)
=>
{
if
(
!
entry
||
typeof
entry
!==
"object"
||
Array
.
isArray
(
entry
))
{
return
null
;
}
const
name
=
(
entry
as
PythonManifestPackageShape
).
name
;
return
typeof
name
===
"string"
&&
name
.
trim
()
?
name
.
trim
().
toLowerCase
()
:
null
;
})
.
filter
((
entry
):
entry
is
string
=>
Boolean
(
entry
));
}
async
function
probePythonPayloadFromManifest
(
pythonManifestPath
:
string
):
Promise
<
PythonPayloadProbeResult
|
null
>
{
try
{
const
raw
=
await
readFile
(
pythonManifestPath
,
"utf8"
);
const
parsed
=
JSON
.
parse
(
raw
.
replace
(
/^
\u
FEFF/
,
""
))
as
PythonManifestShape
;
const
installedPackages
=
[
...
new
Set
([
...
parseManifestPackages
(
parsed
.
requestedPackages
),
...
parseManifestPackages
(
parsed
.
resolvedPackages
)
])
];
if
(
installedPackages
.
length
===
0
)
{
return
null
;
}
const
missingModules
=
PYTHON_RUNTIME_IMPORTS
.
map
(([
packageName
])
=>
packageName
)
.
filter
((
packageName
)
=>
!
installedPackages
.
includes
(
packageName
));
return
{
ready
:
missingModules
.
length
===
0
,
pythonVersion
:
typeof
parsed
.
pythonVersion
===
"string"
?
parsed
.
pythonVersion
:
undefined
,
installedPackages
,
missingModules
,
error
:
undefined
};
}
catch
{
return
null
;
}
}
function
decideSelectedMode
(
requestedMode
:
RuntimeModePreference
,
payloadState
:
RuntimeStatus
[
"payloadState"
],
...
...
@@ -548,7 +636,7 @@ export class RuntimeManager extends EventEmitter {
this
.
readGatewayConnection
(
paths
.
defaultConfigPath
)
]);
cons
t
pythonProbe
=
pythonExists
&&
pythonManifestExists
le
t
pythonProbe
=
pythonExists
&&
pythonManifestExists
?
await
probePythonPayload
(
paths
.
pythonExecutable
)
:
{
ready
:
false
,
...
...
@@ -557,6 +645,18 @@ export class RuntimeManager extends EventEmitter {
error
:
undefined
};
const
pythonProbeErrorCode
=
pythonProbe
.
error
?.
match
(
/
(?:
^|
[
;
\s])
code=
([^
;
\s]
+
)
/
u
)?.[
1
]?.
toUpperCase
();
if
(
!
pythonProbe
.
ready
&&
process
.
platform
===
"win32"
&&
pythonManifestExists
&&
pythonProbeErrorCode
===
"EPERM"
)
{
const
manifestProbe
=
await
probePythonPayloadFromManifest
(
paths
.
pythonManifestPath
);
if
(
manifestProbe
?.
ready
)
{
pythonProbe
=
manifestProbe
;
this
.
appendLog
(
"warn"
,
`Bundled Python direct probe was blocked with code
${
pythonProbeErrorCode
}
; using python-manifest fallback for payload validation.`
);
}
}
this
.
pythonReady
=
pythonProbe
.
ready
;
this
.
pythonVersion
=
pythonProbe
.
pythonVersion
;
this
.
installedPythonPackages
=
pythonProbe
.
installedPackages
;
...
...
@@ -739,11 +839,35 @@ export class RuntimeManager extends EventEmitter {
try
{
child
=
spawn
(
paths
.
nodeExecutable
,
childArgs
,
spawnOptions
);
}
catch
(
error
)
{
const
errorCode
=
error
instanceof
Error
?
String
((
error
as
Error
&
{
code
?:
number
|
string
}).
code
??
""
)
:
""
;
if
(
process
.
platform
===
"win32"
&&
errorCode
===
"EPERM"
)
{
this
.
appendLog
(
"warn"
,
"Bundled runtime direct spawn was blocked with EPERM; retrying via PowerShell wrapper."
);
const
wrapperScript
=
this
.
buildWindowsChildWrapperScript
(
paths
,
childArgs
,
childStdoutLogPath
,
childStderrLogPath
,
childEnv
);
try
{
child
=
spawn
(
"powershell.exe"
,
[
"-NoLogo"
,
"-NoProfile"
,
"-NonInteractive"
,
"-ExecutionPolicy"
,
"Bypass"
,
"-Command"
,
wrapperScript
],
spawnOptions
);
}
catch
(
wrapperError
)
{
this
.
lastError
=
`Bundled runtime failed to spawn:
${
wrapperError
instanceof
Error
?
wrapperError
.
message
:
String
(
wrapperError
)}
`
;
this
.
appendLog
(
"error"
,
this
.
lastError
);
this
.
refreshStatus
(
"error"
);
return
this
.
status
();
}
}
else
{
this
.
lastError
=
`Bundled runtime failed to spawn:
${
error
instanceof
Error
?
error
.
message
:
String
(
error
)}
`
;
this
.
appendLog
(
"error"
,
this
.
lastError
);
this
.
refreshStatus
(
"error"
);
return
this
.
status
();
}
}
this
.
child
=
child
;
child
.
stdout
?.
on
(
"data"
,
(
chunk
:
Buffer
)
=>
{
...
...
@@ -1215,3 +1339,4 @@ export class RuntimeManager extends EventEmitter {
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment