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
5c713cfb
Commit
5c713cfb
authored
May 09, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(desktop): support chunked workspace agent bundles
parent
a6063e5b
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
282 additions
and
38 deletions
+282
-38
project-workspace-agent-runner.ts
apps/desktop/src/main/project-workspace-agent-runner.ts
+282
-38
No files found.
apps/desktop/src/main/project-workspace-agent-runner.ts
View file @
5c713cfb
...
...
@@ -16,10 +16,10 @@ interface RunnerInput {
}
interface
AgentCommandModule
{
t
?:
(
options
:
Record
<
string
,
unknown
>
,
runtime
?:
Record
<
string
,
unknown
>
)
=>
Promise
<
unknown
>
;
[
exportName
:
string
]:
unknown
;
}
interface
Instrumented
ModelSelection
Module
{
interface
Instrumented
AgentEvent
Module
{
__qjcOnAgentEvent
?:
(
listener
:
(
event
:
InstrumentedAgentEvent
)
=>
void
)
=>
(()
=>
boolean
)
|
(()
=>
void
);
}
...
...
@@ -66,11 +66,26 @@ async function resolveAgentModulePath(vendorPackageDir: string): Promise<string>
return
path
.
join
(
distDir
,
agentModuleFile
);
}
function
buildInstrumentationKey
(
agentModulePath
:
string
,
modelSelectionSpecifier
:
string
):
string
{
interface
LocalModuleImport
{
specifier
:
string
;
resolvedPath
:
string
;
}
interface
LocalModuleGraphNode
{
filePath
:
string
;
source
:
string
;
imports
:
LocalModuleImport
[];
}
type
AgentCommandFunction
=
(
options
:
Record
<
string
,
unknown
>
,
runtime
?:
Record
<
string
,
unknown
>
)
=>
Promise
<
unknown
>
;
function
buildInstrumentationKey
(
agentModulePath
:
string
,
eventModulePath
:
string
,
instrumentedSourcePaths
:
string
[]):
string
{
return
createHash
(
"sha1"
)
.
update
(
agentModulePath
)
.
update
(
"
\n
"
)
.
update
(
modelSelectionSpecifier
)
.
update
(
eventModulePath
)
.
update
(
"
\n
"
)
.
update
(
instrumentedSourcePaths
.
join
(
"
\n
"
))
.
digest
(
"hex"
)
.
slice
(
0
,
12
);
}
...
...
@@ -106,29 +121,211 @@ function rewriteModuleSpecifiers(
};
return
source
.
replace
(
/
(
^|
[\r\n])(\s
*
(?:
import|export
)\s
+
[^
"'
\r\n
;
]
+
?\s
+from
\s
*
)([
"'
])([^
"'
\r\n]
+
)\3
/g
,
(
.
replace
(
/
\b((?:
import|export
)\s
+
[^
"'
\r\n
;
]
+
?\s
+from
\s
*
)([
"'
])([^
"'
\r\n]
+
)\2
/g
,
(
_match
,
linePrefix
:
string
,
prefix
:
string
,
quote
:
string
,
specifier
:
string
)
=>
{
return
`
${
linePrefix
}${
prefix
}${
quote
}${
toModuleUrl
(
specifier
)}${
quote
}
`
;
return
`
${
prefix
}${
quote
}${
toModuleUrl
(
specifier
)}${
quote
}
`
;
})
.
replace
(
/
(
^|
[\r\n])(\s
*import
\s
*
)([
"'
])([^
"'
\r\n]
+
)\3
/g
,
(
.
replace
(
/
\b(
import
\s
*
)([
"'
])([^
"'
\r\n]
+
)\2
/g
,
(
_match
,
linePrefix
:
string
,
prefix
:
string
,
quote
:
string
,
specifier
:
string
)
=>
{
return
`
${
linePrefix
}${
prefix
}${
quote
}${
toModuleUrl
(
specifier
)}${
quote
}
`
;
return
`
${
prefix
}${
quote
}${
toModuleUrl
(
specifier
)}${
quote
}
`
;
})
.
replace
(
/
\b(
import
\s
*
\(\s
*
)([
"'
])([^
"'
\r\n]
+
)\2
/g
,
(
_match
,
prefix
:
string
,
quote
:
string
,
specifier
:
string
)
=>
{
return
`
${
prefix
}${
quote
}${
toModuleUrl
(
specifier
)}${
quote
}
`
;
});
}
function
normalizePathForCompare
(
value
:
string
):
string
{
return
path
.
resolve
(
value
).
toLowerCase
();
}
function
isPathInsideDirectory
(
filePath
:
string
,
directoryPath
:
string
):
boolean
{
const
relativePath
=
path
.
relative
(
directoryPath
,
filePath
);
return
Boolean
(
relativePath
)
&&
!
relativePath
.
startsWith
(
".."
)
&&
!
path
.
isAbsolute
(
relativePath
);
}
function
resolveLocalJavaScriptSpecifier
(
baseDir
:
string
,
specifier
:
string
,
distDir
:
string
):
string
|
null
{
if
(
!
specifier
.
startsWith
(
"."
)
||
!
specifier
.
endsWith
(
".js"
))
{
return
null
;
}
const
resolvedPath
=
path
.
resolve
(
baseDir
,
specifier
);
if
(
resolvedPath
!==
distDir
&&
!
isPathInsideDirectory
(
resolvedPath
,
distDir
))
{
return
null
;
}
return
resolvedPath
;
}
function
extractLocalModuleImports
(
source
:
string
,
baseDir
:
string
,
distDir
:
string
):
LocalModuleImport
[]
{
const
imports
:
LocalModuleImport
[]
=
[];
const
seen
=
new
Set
<
string
>
();
const
addSpecifier
=
(
specifier
:
string
)
=>
{
const
resolvedPath
=
resolveLocalJavaScriptSpecifier
(
baseDir
,
specifier
,
distDir
);
if
(
!
resolvedPath
)
{
return
;
}
const
key
=
`
${
specifier
}
\n
${
normalizePathForCompare
(
resolvedPath
)}
`
;
if
(
seen
.
has
(
key
))
{
return
;
}
seen
.
add
(
key
);
imports
.
push
({
specifier
,
resolvedPath
});
};
const
fromImportPattern
=
/
\b(?:
import|export
)\s
+
[^
"'
\r\n
;
]
+
?\s
+from
\s
*
([
"'
])([^
"'
\r\n]
+
)\1
/g
;
for
(
const
match
of
source
.
matchAll
(
fromImportPattern
))
{
addSpecifier
(
match
[
2
]);
}
const
sideEffectImportPattern
=
/
\b
import
\s
*
([
"'
])([^
"'
\r\n]
+
)\1
/g
;
for
(
const
match
of
source
.
matchAll
(
sideEffectImportPattern
))
{
addSpecifier
(
match
[
2
]);
}
const
dynamicImportPattern
=
/
\b
import
\s
*
\(\s
*
([
"'
])([^
"'
\r\n]
+
)\1
/g
;
for
(
const
match
of
source
.
matchAll
(
dynamicImportPattern
))
{
addSpecifier
(
match
[
2
]);
}
return
imports
;
}
async
function
buildLocalModuleGraph
(
agentModulePath
:
string
):
Promise
<
Map
<
string
,
LocalModuleGraphNode
>>
{
const
distDir
=
path
.
dirname
(
agentModulePath
);
const
graph
=
new
Map
<
string
,
LocalModuleGraphNode
>
();
const
queue
=
[
agentModulePath
];
while
(
queue
.
length
>
0
)
{
const
filePath
=
queue
.
shift
()
!
;
const
key
=
normalizePathForCompare
(
filePath
);
if
(
graph
.
has
(
key
))
{
continue
;
}
const
source
=
await
readFile
(
filePath
,
"utf8"
);
const
imports
=
extractLocalModuleImports
(
source
,
path
.
dirname
(
filePath
),
distDir
);
graph
.
set
(
key
,
{
filePath
,
source
,
imports
});
for
(
const
importedModule
of
imports
)
{
const
importedKey
=
normalizePathForCompare
(
importedModule
.
resolvedPath
);
if
(
!
graph
.
has
(
importedKey
))
{
queue
.
push
(
importedModule
.
resolvedPath
);
}
}
}
return
graph
;
}
function
resolveOnAgentEventLocalBinding
(
source
:
string
):
string
|
null
{
if
(
/
\b
export
\s
+
(?:
async
\s
+
)?
function
\s
+onAgentEvent
\b
/
.
test
(
source
)
||
/
\b
export
\s
+
(?:
const|let|var
)\s
+onAgentEvent
\b
/
.
test
(
source
))
{
return
"onAgentEvent"
;
}
const
identifierPattern
=
/^
[
A-Za-z_$
][
A-Za-z0-9_$
]
*$/
;
const
exportBlockPattern
=
/
\b
export
\s
*
\{([^
}
]
+
)\}
/g
;
for
(
const
match
of
source
.
matchAll
(
exportBlockPattern
))
{
const
afterExportBlock
=
source
.
slice
((
match
.
index
??
0
)
+
match
[
0
].
length
).
trimStart
();
if
(
afterExportBlock
.
startsWith
(
"from"
))
{
continue
;
}
for
(
const
rawSpecifier
of
match
[
1
].
split
(
","
))
{
const
specifier
=
rawSpecifier
.
trim
();
const
aliasMatch
=
specifier
.
match
(
/^
([
A-Za-z_$
][
A-Za-z0-9_$
]
*
)(?:\s
+as
\s
+
([
A-Za-z_$
][
A-Za-z0-9_$
]
*
))?
$/
);
if
(
!
aliasMatch
)
{
continue
;
}
const
localName
=
aliasMatch
[
1
];
const
exportedName
=
aliasMatch
[
2
]
??
localName
;
if
((
localName
===
"onAgentEvent"
||
exportedName
===
"onAgentEvent"
)
&&
identifierPattern
.
test
(
localName
))
{
return
localName
;
}
}
}
return
null
;
}
function
selectAgentEventModule
(
graph
:
Map
<
string
,
LocalModuleGraphNode
>
):
LocalModuleGraphNode
|
null
{
const
candidates
=
[...
graph
.
values
()].
filter
((
node
)
=>
resolveOnAgentEventLocalBinding
(
node
.
source
));
if
(
candidates
.
length
===
0
)
{
return
null
;
}
return
candidates
.
sort
((
left
,
right
)
=>
{
const
leftFile
=
path
.
basename
(
left
.
filePath
);
const
rightFile
=
path
.
basename
(
right
.
filePath
);
const
leftPriority
=
/^agent-events-
[
A-Za-z0-9_-
]
+
\.
js$/
.
test
(
leftFile
)
?
0
:
1
;
const
rightPriority
=
/^agent-events-
[
A-Za-z0-9_-
]
+
\.
js$/
.
test
(
rightFile
)
?
0
:
1
;
if
(
leftPriority
!==
rightPriority
)
{
return
leftPriority
-
rightPriority
;
}
return
leftFile
.
localeCompare
(
rightFile
);
})[
0
];
}
function
resolveModulePathFromGraph
(
graph
:
Map
<
string
,
LocalModuleGraphNode
>
,
sourcePath
:
string
,
targetPath
:
string
):
string
[]
|
null
{
const
sourceKey
=
normalizePathForCompare
(
sourcePath
);
const
targetKey
=
normalizePathForCompare
(
targetPath
);
const
queue
=
[
sourceKey
];
const
parents
=
new
Map
<
string
,
string
|
null
>
([[
sourceKey
,
null
]]);
while
(
queue
.
length
>
0
)
{
const
currentKey
=
queue
.
shift
()
!
;
if
(
currentKey
===
targetKey
)
{
break
;
}
const
currentNode
=
graph
.
get
(
currentKey
);
if
(
!
currentNode
)
{
continue
;
}
for
(
const
importedModule
of
currentNode
.
imports
)
{
const
importedKey
=
normalizePathForCompare
(
importedModule
.
resolvedPath
);
if
(
parents
.
has
(
importedKey
))
{
continue
;
}
parents
.
set
(
importedKey
,
currentKey
);
queue
.
push
(
importedKey
);
}
}
if
(
!
parents
.
has
(
targetKey
))
{
return
null
;
}
const
pathKeys
:
string
[]
=
[];
let
cursor
:
string
|
null
=
targetKey
;
while
(
cursor
)
{
pathKeys
.
push
(
cursor
);
cursor
=
parents
.
get
(
cursor
)
??
null
;
}
return
pathKeys
.
reverse
()
.
map
((
key
)
=>
graph
.
get
(
key
)?.
filePath
)
.
filter
((
value
):
value
is
string
=>
typeof
value
===
"string"
);
}
async
function
ensureInstrumentationNodeModulesLink
(
instrumentationDir
:
string
,
vendorPackageDir
:
string
):
Promise
<
void
>
{
const
vendorNodeModulesPath
=
path
.
join
(
vendorPackageDir
,
"node_modules"
);
try
{
...
...
@@ -161,43 +358,92 @@ async function ensureInstrumentationNodeModulesLink(instrumentationDir: string,
async
function
ensureInstrumentedWorkspaceModules
(
agentModulePath
:
string
,
instrumentationDir
:
string
):
Promise
<
{
agentModuleUrl
:
string
;
modelSelection
ModuleUrl
:
string
;
event
ModuleUrl
:
string
;
}
>
{
const
agentSource
=
await
readFile
(
agentModulePath
,
"utf8"
);
const
modelSelectionImportMatch
=
agentSource
.
match
(
/from
\s
+
[
"'
](\.\/
model-selection-
[^
"'
]
+
\.
js
)[
"'
]
/
);
if
(
!
modelSelectionImportMatch
)
{
throw
new
Error
(
`Unable to locate model-selection import in
${
agentModulePath
}
.`
);
const
graph
=
await
buildLocalModuleGraph
(
agentModulePath
);
const
eventModule
=
selectAgentEventModule
(
graph
);
const
onAgentEventLocalBinding
=
eventModule
?
resolveOnAgentEventLocalBinding
(
eventModule
.
source
)
:
null
;
if
(
!
eventModule
||
!
onAgentEventLocalBinding
)
{
const
moduleFiles
=
[...
graph
.
values
()].
map
((
node
)
=>
path
.
basename
(
node
.
filePath
)).
sort
();
throw
new
Error
(
`Unable to locate OpenClaw onAgentEvent export from
${
agentModulePath
}
. Reachable modules:
${
moduleFiles
.
join
(
", "
)
||
"(none)"
}
.`
);
}
const
instrumentedSourcePaths
=
resolveModulePathFromGraph
(
graph
,
agentModulePath
,
eventModule
.
filePath
);
if
(
!
instrumentedSourcePaths
?.
length
)
{
throw
new
Error
(
`Unable to resolve import path from
${
agentModulePath
}
to
${
eventModule
.
filePath
}
.`
);
}
const
modelSelectionSpecifier
=
modelSelectionImportMatch
[
1
];
const
distDir
=
path
.
dirname
(
agentModulePath
);
const
vendorPackageDir
=
path
.
dirname
(
distDir
);
const
modelSelectionPath
=
path
.
resolve
(
distDir
,
modelSelectionSpecifier
);
const
instrumentationKey
=
buildInstrumentationKey
(
agentModulePath
,
modelSelectionSpecifier
);
const
instrumentedAgentFileName
=
`.qjc-agent-
${
instrumentationKey
}
.js`
;
const
instrumentedModelSelectionFileName
=
`.qjc-model-selection-
${
instrumentationKey
}
.js`
;
const
instrumentedAgentPath
=
path
.
join
(
instrumentationDir
,
instrumentedAgentFileName
);
const
instrumentedModelSelectionPath
=
path
.
join
(
instrumentationDir
,
instrumentedModelSelectionFileName
);
const
instrumentedModelSelectionUrl
=
pathToFileURL
(
instrumentedModelSelectionPath
).
href
;
const
instrumentationKey
=
buildInstrumentationKey
(
agentModulePath
,
eventModule
.
filePath
,
instrumentedSourcePaths
);
const
instrumentedPathBySourcePath
=
new
Map
<
string
,
string
>
();
await
mkdir
(
instrumentationDir
,
{
recursive
:
true
});
await
writeFile
(
path
.
join
(
instrumentationDir
,
"package.json"
),
JSON
.
stringify
({
type
:
"module"
},
null
,
2
),
"utf8"
);
await
ensureInstrumentationNodeModulesLink
(
instrumentationDir
,
vendorPackageDir
);
const
modelSelectionSource
=
await
readFile
(
modelSelectionPath
,
"utf8"
);
const
instrumentedModelSelectionSource
=
`
${
rewriteModuleSpecifiers
(
modelSelectionSource
,
distDir
)}
\nexport { onAgentEvent as __qjcOnAgentEvent };\n`
;
await
writeFile
(
instrumentedModelSelectionPath
,
instrumentedModelSelectionSource
,
"utf8"
);
for
(
const
sourcePath
of
instrumentedSourcePaths
)
{
const
role
=
normalizePathForCompare
(
sourcePath
)
===
normalizePathForCompare
(
agentModulePath
)
?
"agent"
:
normalizePathForCompare
(
sourcePath
)
===
normalizePathForCompare
(
eventModule
.
filePath
)
?
"agent-events"
:
"agent-link"
;
const
sourceStem
=
path
.
basename
(
sourcePath
,
".js"
).
replace
(
/
[^
A-Za-z0-9_-
]
/g
,
"-"
);
instrumentedPathBySourcePath
.
set
(
normalizePathForCompare
(
sourcePath
),
path
.
join
(
instrumentationDir
,
`.qjc-
${
role
}
-
${
sourceStem
}
-
${
instrumentationKey
}
.js`
)
);
}
const
instrumentedAgentSource
=
rewriteModuleSpecifiers
(
agentSource
,
distDir
,
{
[
modelSelectionSpecifier
]:
instrumentedModelSelectionUrl
});
await
writeFile
(
instrumentedAgentPath
,
instrumentedAgentSource
,
"utf8"
);
for
(
const
sourcePath
of
[...
instrumentedSourcePaths
].
reverse
())
{
const
sourceNode
=
graph
.
get
(
normalizePathForCompare
(
sourcePath
));
const
instrumentedPath
=
instrumentedPathBySourcePath
.
get
(
normalizePathForCompare
(
sourcePath
));
if
(
!
sourceNode
||
!
instrumentedPath
)
{
continue
;
}
const
overrides
=
Object
.
fromEntries
(
sourceNode
.
imports
.
map
((
importedModule
):
[
string
,
string
]
|
null
=>
{
const
importedInstrumentedPath
=
instrumentedPathBySourcePath
.
get
(
normalizePathForCompare
(
importedModule
.
resolvedPath
));
return
importedInstrumentedPath
?
[
importedModule
.
specifier
,
pathToFileURL
(
importedInstrumentedPath
).
href
]
:
null
;
})
.
filter
((
entry
):
entry
is
[
string
,
string
]
=>
Array
.
isArray
(
entry
))
);
const
instrumentedSource
=
rewriteModuleSpecifiers
(
sourceNode
.
source
,
path
.
dirname
(
sourcePath
),
overrides
);
const
finalSource
=
normalizePathForCompare
(
sourcePath
)
===
normalizePathForCompare
(
eventModule
.
filePath
)
?
`
${
instrumentedSource
}
\nexport {
${
onAgentEventLocalBinding
}
as __qjcOnAgentEvent };\n`
:
instrumentedSource
;
await
writeFile
(
instrumentedPath
,
finalSource
,
"utf8"
);
}
const
instrumentedAgentPath
=
instrumentedPathBySourcePath
.
get
(
normalizePathForCompare
(
agentModulePath
));
const
instrumentedEventModulePath
=
instrumentedPathBySourcePath
.
get
(
normalizePathForCompare
(
eventModule
.
filePath
));
if
(
!
instrumentedAgentPath
||
!
instrumentedEventModulePath
)
{
throw
new
Error
(
"Unable to prepare OpenClaw workspace instrumentation files."
);
}
return
{
agentModuleUrl
:
pathToFileURL
(
instrumentedAgentPath
).
href
,
modelSelectionModuleUrl
:
pathToFileURL
(
instrumentedModelSelection
Path
).
href
eventModuleUrl
:
pathToFileURL
(
instrumentedEventModule
Path
).
href
};
}
function
resolveAgentCommand
(
agentModule
:
AgentCommandModule
):
AgentCommandFunction
{
const
preferredExportNames
=
[
"t"
,
"agentCommandFromIngress"
,
"agentCommand"
,
"r"
,
"n"
];
for
(
const
exportName
of
preferredExportNames
)
{
const
candidate
=
agentModule
[
exportName
];
if
(
typeof
candidate
===
"function"
)
{
return
candidate
as
AgentCommandFunction
;
}
}
const
actualExportNames
=
Object
.
keys
(
agentModule
).
sort
();
throw
new
Error
(
`Bundled OpenClaw agent module does not expose a supported agent command export. Expected one of:
${
preferredExportNames
.
join
(
", "
)}
. Actual exports:
${
actualExportNames
.
join
(
", "
)
||
"(none)"
}
.`
);
}
function
extractReplyText
(
result
:
unknown
):
string
{
const
payloads
=
Array
.
isArray
((
result
as
{
payloads
?:
unknown
[]
}
|
null
)?.
payloads
)
?
((
result
as
{
payloads
:
unknown
[]
}).
payloads
)
...
...
@@ -280,14 +526,12 @@ async function main(): Promise<void> {
const
agentModulePath
=
await
resolveAgentModulePath
(
input
.
vendorPackageDir
);
const
instrumentedModules
=
await
ensureInstrumentedWorkspaceModules
(
agentModulePath
,
resolveInstrumentationDir
(
input
));
const
agentModule
=
await
import
(
instrumentedModules
.
agentModuleUrl
)
as
AgentCommandModule
;
const
modelSelectionModule
=
await
import
(
instrumentedModules
.
modelSelectionModuleUrl
)
as
InstrumentedModelSelectionModule
;
if
(
typeof
agentModule
.
t
!==
"function"
)
{
throw
new
Error
(
"Bundled OpenClaw agent module does not expose agentCommand."
);
}
const
eventModule
=
await
import
(
instrumentedModules
.
eventModuleUrl
)
as
InstrumentedAgentEventModule
;
const
agentCommand
=
resolveAgentCommand
(
agentModule
);
let
streamedText
=
""
;
const
unsubscribe
=
typeof
modelSelection
Module
.
__qjcOnAgentEvent
===
"function"
?
modelSelection
Module
.
__qjcOnAgentEvent
((
event
)
=>
{
const
unsubscribe
=
typeof
event
Module
.
__qjcOnAgentEvent
===
"function"
?
event
Module
.
__qjcOnAgentEvent
((
event
)
=>
{
if
(
event
.
runId
!==
runId
||
event
.
stream
!==
"assistant"
)
{
return
;
}
...
...
@@ -327,7 +571,7 @@ async function main(): Promise<void> {
const
runtimeSession
=
createRuntimeSessionIdentity
(
input
.
sessionId
);
const
message
=
`
${
input
.
prompt
}${
renderAttachmentPrelude
(
input
.
projectRoot
,
input
.
attachments
)}
`
.
trim
();
try
{
const
result
=
await
agent
Module
.
t
({
const
result
=
await
agent
Command
({
message
,
sessionId
:
runtimeSession
.
sessionId
,
sessionKey
:
runtimeSession
.
sessionKey
,
...
...
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