fix(plugins): repair bundled deps on activation

This commit is contained in:
Peter Steinberger
2026-04-22 20:21:20 +01:00
parent 4663e7394b
commit 9c733956c0
7 changed files with 369 additions and 196 deletions

View File

@@ -64,20 +64,11 @@ package_root="$(npm root -g)/openclaw"
test -d "$package_root/dist/extensions/telegram"
test -d "$package_root/dist/extensions/discord"
test -d "$package_root/dist/extensions/slack"
test -d "$package_root/dist/extensions/feishu"
if [ -d "$package_root/dist/extensions/telegram/node_modules" ]; then
echo "telegram runtime deps should not be preinstalled in package" >&2
find "$package_root/dist/extensions/telegram/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
exit 1
fi
if [ -d "$package_root/dist/extensions/discord/node_modules" ]; then
echo "discord runtime deps should not be preinstalled in package" >&2
find "$package_root/dist/extensions/discord/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
exit 1
fi
if [ -d "$package_root/dist/extensions/slack/node_modules" ]; then
echo "slack runtime deps should not be preinstalled in package" >&2
find "$package_root/dist/extensions/slack/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
if [ -d "$package_root/dist/extensions/$CHANNEL/node_modules" ]; then
echo "$CHANNEL runtime deps should not be preinstalled in package" >&2
find "$package_root/dist/extensions/$CHANNEL/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
exit 1
fi
@@ -156,6 +147,15 @@ if (mode === "slack") {
},
};
}
if (mode === "feishu") {
config.channels = {
...(config.channels || {}),
feishu: {
...(config.channels?.feishu || {}),
enabled: true,
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
@@ -239,10 +239,17 @@ NODE
assert_installed_once() {
local log_file="$1"
local channel="$2"
local dep_path="$3"
local count
count="$(grep -c "\\[plugins\\] $channel installed bundled runtime deps:" "$log_file" || true)"
if [ "$count" -eq 1 ]; then
return 0
fi
if [ "$count" -eq 0 ] && [ -f "$package_root/dist/extensions/$channel/node_modules/$dep_path/package.json" ]; then
return 0
fi
if [ "$count" -ne 1 ]; then
echo "expected exactly one runtime deps install for $channel, got $count" >&2
echo "expected exactly one runtime deps install log or installed sentinel for $channel, got $count log lines" >&2
cat "$log_file" >&2
exit 1
fi
@@ -268,17 +275,27 @@ assert_dep_sentinel() {
fi
}
assert_no_dep_sentinel() {
local channel="$1"
local dep_path="$2"
if [ -f "$package_root/dist/extensions/$channel/node_modules/$dep_path/package.json" ]; then
echo "dependency sentinel should be absent before activation for $channel: $dep_path" >&2
exit 1
fi
}
echo "Starting baseline gateway with OpenAI configured..."
write_config baseline
start_gateway "/tmp/openclaw-$CHANNEL-baseline.log"
wait_for_gateway_health
stop_gateway
assert_no_dep_sentinel "$CHANNEL" "$DEP_SENTINEL"
echo "Enabling $CHANNEL by config edit, then restarting gateway..."
write_config "$CHANNEL"
start_gateway "/tmp/openclaw-$CHANNEL-first.log"
wait_for_gateway_health
assert_installed_once "/tmp/openclaw-$CHANNEL-first.log" "$CHANNEL"
assert_installed_once "/tmp/openclaw-$CHANNEL-first.log" "$CHANNEL" "$DEP_SENTINEL"
assert_dep_sentinel "$CHANNEL" "$DEP_SENTINEL"
assert_channel_status "$CHANNEL"
stop_gateway
@@ -919,6 +936,7 @@ if [ "$RUN_CHANNEL_SCENARIOS" != "0" ]; then
run_channel_scenario telegram grammy
run_channel_scenario discord discord-api-types
run_channel_scenario slack @slack/web-api
run_channel_scenario feishu @larksuiteoapi/node-sdk
fi
if [ "$RUN_UPDATE_SCENARIO" != "0" ]; then
run_update_scenario

View File

@@ -1,7 +1,15 @@
#!/usr/bin/env -S node --import tsx
import { execFileSync, execSync } from "node:child_process";
import { existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
import {
existsSync,
mkdtempSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
@@ -211,7 +219,6 @@ export function createPackedBundledPluginPostinstallEnv(
return {
...env,
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
};
}
@@ -238,20 +245,143 @@ export function collectInstalledBundledPluginRuntimeDepErrors(packageRoot: strin
.toSorted((left, right) => left.localeCompare(right));
}
function assertInstalledBundledPluginRuntimeDepsResolved(packageRoot: string): void {
const errors = collectInstalledBundledPluginRuntimeDepErrors(packageRoot);
if (errors.length === 0) {
function bundledRuntimeDependencySentinelPath(
packageRoot: string,
pluginId: string,
dependencyName: string,
): string {
return join(
packageRoot,
"dist",
"extensions",
pluginId,
"node_modules",
...dependencyName.split("/"),
"package.json",
);
}
function bundledRuntimeDependencySentinelCandidates(
packageRoot: string,
pluginId: string,
dependencyName: string,
): string[] {
const dependencyParts = dependencyName.split("/");
return [
bundledRuntimeDependencySentinelPath(packageRoot, pluginId, dependencyName),
join(packageRoot, "dist", "extensions", "node_modules", ...dependencyParts, "package.json"),
join(packageRoot, "node_modules", ...dependencyParts, "package.json"),
];
}
function assertBundledRuntimeDependencyAbsent(params: {
packageRoot: string;
pluginId: string;
dependencyName: string;
}): void {
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
params.packageRoot,
params.pluginId,
params.dependencyName,
).find((candidate) => existsSync(candidate));
if (sentinelPath) {
throw new Error(
`release-check: ${params.pluginId} runtime dependency ${params.dependencyName} was installed before plugin activation (${sentinelPath}).`,
);
}
}
function assertBundledRuntimeDependencyPresent(params: {
packageRoot: string;
pluginId: string;
dependencyName: string;
}): void {
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
params.packageRoot,
params.pluginId,
params.dependencyName,
).find((candidate) => existsSync(candidate));
if (sentinelPath) {
return;
}
console.error("release-check: packed install is missing bundled plugin runtime dependencies:");
for (const error of errors) {
console.error(` - ${error}`);
}
throw new Error(
"release-check: bundled plugin runtime dependencies were not installed after packed postinstall.",
`release-check: ${params.pluginId} runtime dependency ${params.dependencyName} was not installed during plugin activation.`,
);
}
function writePackedBundledPluginActivationConfig(homeDir: string): void {
const configPath = join(homeDir, ".openclaw", "openclaw.json");
mkdirSync(join(homeDir, ".openclaw"), { recursive: true });
writeFileSync(
configPath,
`${JSON.stringify(
{
agents: {
defaults: {
model: { primary: "openai/gpt-4.1-mini" },
},
},
channels: {
feishu: {
enabled: true,
},
},
models: {
providers: {
openai: {
apiKey: "sk-openclaw-release-check",
baseUrl: "https://api.openai.com/v1",
models: [],
},
},
},
plugins: {
enabled: true,
entries: {
feishu: {
enabled: true,
},
},
},
},
null,
2,
)}\n`,
"utf8",
);
}
function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: string): void {
const lazyDeps = [
{ pluginId: "browser", dependencyName: "playwright-core" },
{ pluginId: "feishu", dependencyName: "@larksuiteoapi/node-sdk" },
] as const;
for (const dep of lazyDeps) {
assertBundledRuntimeDependencyAbsent({ packageRoot, ...dep });
}
const homeDir = join(tmpRoot, "activation-home");
mkdirSync(homeDir, { recursive: true });
writePackedBundledPluginActivationConfig(homeDir);
execFileSync(process.execPath, [join(packageRoot, "openclaw.mjs"), "plugins", "doctor"], {
cwd: packageRoot,
stdio: "inherit",
env: {
...process.env,
HOME: homeDir,
OPENAI_API_KEY: "sk-openclaw-release-check",
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
OPENCLAW_NO_ONBOARD: "1",
OPENCLAW_SUPPRESS_NOTES: "1",
},
});
for (const dep of lazyDeps) {
assertBundledRuntimeDependencyPresent({ packageRoot, ...dep });
}
}
function runPackedBundledChannelEntrySmoke(): void {
const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-"));
try {
@@ -265,7 +395,7 @@ function runPackedBundledChannelEntrySmoke(): void {
const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw");
runPackedBundledPluginPostinstall(packageRoot);
assertInstalledBundledPluginRuntimeDepsResolved(packageRoot);
runPackedBundledPluginActivationSmoke(packageRoot, tmpRoot);
execFileSync(
process.execPath,
[