Skip to content

Commit a655585

Browse files
authored
fix: repair plugin bundle dependency closure for qqbot (#1001)
* fix: repair plugin bundle dependency closure for qqbot * fix: satisfy biome formatting for qqbot bundle patch * fix: validate qqbot deps in windows packaged stage
1 parent 2632b3a commit a655585

5 files changed

Lines changed: 186 additions & 4 deletions

File tree

apps/controller/scripts/bundle-runtime-plugins.mjs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ function getVirtualStoreNodeModules(realPkgPath) {
4646
return null;
4747
}
4848

49+
function getPackageNodeModules(packageRoot) {
50+
const candidate = path.join(packageRoot, "node_modules");
51+
try {
52+
readdirSync(candidate);
53+
return candidate;
54+
} catch {
55+
return null;
56+
}
57+
}
58+
4959
function listPackages(nodeModulesDir) {
5060
const result = [];
5161

@@ -166,8 +176,10 @@ async function bundlePlugin({ id, npmName }) {
166176
});
167177
await maybeFixPluginManifest(outputDir);
168178

169-
const rootVirtualNodeModules = getVirtualStoreNodeModules(sourcePackageRoot);
170-
if (!rootVirtualNodeModules) {
179+
const rootDependencyNodeModules =
180+
getPackageNodeModules(sourcePackageRoot) ??
181+
getVirtualStoreNodeModules(sourcePackageRoot);
182+
if (!rootDependencyNodeModules) {
171183
throw new Error(`Unable to resolve node_modules for ${npmName}`);
172184
}
173185

@@ -178,7 +190,9 @@ async function bundlePlugin({ id, npmName }) {
178190
...Object.keys(packageJson.peerDependencies ?? {}),
179191
]);
180192
const collected = new Map();
181-
const queue = [{ nodeModulesDir: rootVirtualNodeModules, skipPkg: npmName }];
193+
const queue = [
194+
{ nodeModulesDir: rootDependencyNodeModules, skipPkg: npmName },
195+
];
182196

183197
while (queue.length > 0) {
184198
const { nodeModulesDir, skipPkg } = queue.shift();

apps/controller/tests/openclaw-runtime-plugin-writer.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,41 @@ describe("OpenClawRuntimePluginWriter", () => {
189189
).toBe("bundled\n");
190190
});
191191

192+
it("keeps bundled qqbot runtime dependencies when materializing extensions", async () => {
193+
const bundledPluginDir = path.join(
194+
env.bundledRuntimePluginsDir,
195+
"openclaw-qqbot",
196+
);
197+
const bundledSilkWasmDir = path.join(
198+
bundledPluginDir,
199+
"node_modules",
200+
"silk-wasm",
201+
);
202+
203+
await mkdir(bundledSilkWasmDir, { recursive: true });
204+
await writeFile(
205+
path.join(bundledSilkWasmDir, "package.json"),
206+
'{ "name": "silk-wasm" }\n',
207+
"utf8",
208+
);
209+
210+
const writer = new OpenClawRuntimePluginWriter(env);
211+
await writer.ensurePlugins();
212+
213+
expect(
214+
await readFile(
215+
path.join(
216+
env.openclawExtensionsDir,
217+
"openclaw-qqbot",
218+
"node_modules",
219+
"silk-wasm",
220+
"package.json",
221+
),
222+
"utf8",
223+
),
224+
).toContain('"name": "silk-wasm"');
225+
});
226+
192227
it("prefers bundled wecom over the legacy runtime plugin source", async () => {
193228
const bundledPluginDir = path.join(env.bundledRuntimePluginsDir, "wecom");
194229
const legacyPluginDir = path.join(env.runtimePluginTemplatesDir, "wecom");

apps/desktop/scripts/dist-mac.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ async function ensureExistingPath(path, label) {
8080
}
8181
}
8282

83+
async function pathExists(targetPath) {
84+
try {
85+
await lstat(targetPath);
86+
return true;
87+
} catch {
88+
return false;
89+
}
90+
}
91+
8392
async function ensureExistingBuildArtifacts() {
8493
await Promise.all([
8594
ensureExistingPath(
@@ -463,6 +472,52 @@ async function stapleNotarizedAppBundles() {
463472
}
464473
}
465474

475+
async function validatePackagedQqbotDependencies(releaseRoot) {
476+
const appBundleDirs = await readdir(releaseRoot, { withFileTypes: true });
477+
const packagedMacBundles = appBundleDirs.filter(
478+
(entry) =>
479+
entry.isDirectory() &&
480+
(entry.name === "mac" || entry.name.startsWith("mac-")),
481+
);
482+
483+
if (packagedMacBundles.length === 0) {
484+
throw new Error(
485+
`[dist:mac] expected packaged macOS app bundles under ${releaseRoot}, but none were found.`,
486+
);
487+
}
488+
489+
for (const entry of packagedMacBundles) {
490+
const appRoot = resolve(releaseRoot, entry.name, "Nexu.app");
491+
const qqbotPluginRoot = resolve(
492+
appRoot,
493+
"Contents",
494+
"Resources",
495+
"runtime",
496+
"controller",
497+
"plugins",
498+
"openclaw-qqbot",
499+
);
500+
const silkWasmPackagePath = resolve(
501+
qqbotPluginRoot,
502+
"node_modules",
503+
"silk-wasm",
504+
"package.json",
505+
);
506+
507+
if (!(await pathExists(qqbotPluginRoot))) {
508+
throw new Error(
509+
`[dist:mac] packaged app is missing openclaw-qqbot: ${qqbotPluginRoot}`,
510+
);
511+
}
512+
513+
if (!(await pathExists(silkWasmPackagePath))) {
514+
throw new Error(
515+
`[dist:mac] packaged app is missing openclaw-qqbot dependency silk-wasm: ${silkWasmPackagePath}`,
516+
);
517+
}
518+
}
519+
}
520+
466521
async function ensureBuildConfig() {
467522
const configPath = resolve(electronRoot, "build-config.json");
468523
const isCi =
@@ -840,6 +895,11 @@ async function main() {
840895
},
841896
timings,
842897
);
898+
await timedStep(
899+
"validate packaged qqbot dependencies",
900+
async () => validatePackagedQqbotDependencies(releaseRoot),
901+
timings,
902+
);
843903
await timedStep(
844904
"staple notarized app bundles",
845905
async () => stapleNotarizedAppBundles(),

apps/desktop/scripts/dist-win-stage.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,36 @@ async function validateWinUnpackedReuse(releaseRoot) {
457457
}
458458
}
459459

460+
async function validatePackagedQqbotDependencies(releaseRoot) {
461+
const winUnpackedRoot = resolve(releaseRoot, "win-unpacked");
462+
const qqbotPluginRoot = resolve(
463+
winUnpackedRoot,
464+
"resources",
465+
"runtime",
466+
"controller",
467+
"plugins",
468+
"openclaw-qqbot",
469+
);
470+
const silkWasmPackagePath = resolve(
471+
qqbotPluginRoot,
472+
"node_modules",
473+
"silk-wasm",
474+
"package.json",
475+
);
476+
477+
if (!(await pathExists(qqbotPluginRoot))) {
478+
throw new Error(
479+
`[dist:win] packaged app is missing openclaw-qqbot: ${qqbotPluginRoot}`,
480+
);
481+
}
482+
483+
if (!(await pathExists(silkWasmPackagePath))) {
484+
throw new Error(
485+
`[dist:win] packaged app is missing openclaw-qqbot dependency silk-wasm: ${silkWasmPackagePath}`,
486+
);
487+
}
488+
}
489+
460490
async function ensureExistingBuildArtifacts() {
461491
await Promise.all([
462492
ensureExistingPath(
@@ -1281,6 +1311,11 @@ async function main() {
12811311
},
12821312
timings,
12831313
);
1314+
await timedStep(
1315+
"validate packaged qqbot dependencies",
1316+
async () => validatePackagedQqbotDependencies(releaseRoot),
1317+
timings,
1318+
);
12841319

12851320
if (!dirOnly) {
12861321
await timedStep(

apps/desktop/scripts/prepare-controller-sidecar.mjs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cp, readFile, writeFile } from "node:fs/promises";
2-
import { resolve } from "node:path";
2+
import { join, resolve } from "node:path";
33
import {
44
copyRuntimeDependencyClosure,
55
getSidecarRoot,
@@ -31,11 +31,41 @@ const sidecarPackageJsonPath = resolve(sidecarRoot, "package.json");
3131
const diagnosticsEnabled =
3232
process.env.NEXU_DESKTOP_DIST_DIAGNOSTICS === "1" ||
3333
process.env.NEXU_DESKTOP_DIST_DIAGNOSTICS?.toLowerCase() === "true";
34+
const qqbotPluginRelativeRoot = "openclaw-qqbot";
35+
const qqbotSilkWasmPackageRelativePath = join(
36+
qqbotPluginRelativeRoot,
37+
"node_modules",
38+
"silk-wasm",
39+
"package.json",
40+
);
3441

3542
function formatDurationMs(durationMs) {
3643
return `${(durationMs / 1000).toFixed(3)}s`;
3744
}
3845

46+
async function ensureQqbotPluginDependencyTree(rootDir, label) {
47+
const pluginDir = resolve(rootDir, qqbotPluginRelativeRoot);
48+
const silkWasmPackagePath = resolve(
49+
rootDir,
50+
qqbotSilkWasmPackageRelativePath,
51+
);
52+
const missing = [];
53+
54+
if (!(await pathExists(pluginDir))) {
55+
missing.push(pluginDir);
56+
}
57+
58+
if (!(await pathExists(silkWasmPackagePath))) {
59+
missing.push(silkWasmPackagePath);
60+
}
61+
62+
if (missing.length > 0) {
63+
throw new Error(
64+
`[controller-sidecar] ${label} is missing bundled QQ plugin dependencies: ${missing.join(", ")}`,
65+
);
66+
}
67+
}
68+
3969
async function ensureBuildArtifacts() {
4070
const missing = [];
4171

@@ -78,10 +108,18 @@ async function prepareControllerSidecar() {
78108
}
79109

80110
if (await pathExists(controllerBundledPluginsRoot)) {
111+
await ensureQqbotPluginDependencyTree(
112+
controllerBundledPluginsRoot,
113+
"controller bundled plugins",
114+
);
81115
await cp(controllerBundledPluginsRoot, sidecarPluginsRoot, {
82116
recursive: true,
83117
dereference: true,
84118
});
119+
await ensureQqbotPluginDependencyTree(
120+
sidecarPluginsRoot,
121+
"controller sidecar plugins",
122+
);
85123
}
86124

87125
const controllerPackageJson = JSON.parse(

0 commit comments

Comments
 (0)