mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 02:30:46 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
806
openclaw/scripts/postinstall-bundled-plugins.mjs
Normal file
806
openclaw/scripts/postinstall-bundled-plugins.mjs
Normal file
|
|
@ -0,0 +1,806 @@
|
|||
#!/usr/bin/env node
|
||||
// Runs after install to keep packaged dist safe and compatible.
|
||||
// Bundled extension runtime dependencies are extension-owned. Do not install
|
||||
// every bundled extension dependency during core package install unless the
|
||||
// legacy eager-install escape hatch is explicitly enabled; `openclaw doctor
|
||||
// --fix` owns the repair path for extensions that are actually used.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
chmodSync,
|
||||
closeSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
rmdirSync,
|
||||
rmSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolveNpmRunner } from "./npm-runner.mjs";
|
||||
|
||||
export const BUNDLED_PLUGIN_INSTALL_TARGETS = [];
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_EXTENSIONS_DIR = join(__dirname, "..", "dist", "extensions");
|
||||
const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
|
||||
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
|
||||
const EAGER_BUNDLED_PLUGIN_DEPS_ENV = "OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS";
|
||||
const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
|
||||
const LEGACY_UPDATE_COMPAT_SIDECARS = [
|
||||
{
|
||||
path: "dist/extensions/qa-channel/runtime-api.js",
|
||||
removedPrefix: "dist/extensions/qa-channel/",
|
||||
content:
|
||||
"// Compatibility stub for older OpenClaw updaters. The QA channel implementation is not packaged.\nexport {};\n",
|
||||
},
|
||||
{
|
||||
path: "dist/extensions/qa-lab/runtime-api.js",
|
||||
removedPrefix: "dist/extensions/qa-lab/",
|
||||
content:
|
||||
"// Compatibility stub for older OpenClaw updaters. The QA lab implementation is not packaged.\nexport {};\n",
|
||||
},
|
||||
];
|
||||
const BAILEYS_MEDIA_FILE = join(
|
||||
"node_modules",
|
||||
"@whiskeysockets",
|
||||
"baileys",
|
||||
"lib",
|
||||
"Utils",
|
||||
"messages-media.js",
|
||||
);
|
||||
const BAILEYS_MEDIA_HOTFIX_NEEDLE = [
|
||||
" encFileWriteStream.write(mac);",
|
||||
" encFileWriteStream.end();",
|
||||
" originalFileStream?.end?.();",
|
||||
" stream.destroy();",
|
||||
" logger?.debug('encrypted data successfully');",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_HOTFIX_REPLACEMENT = [
|
||||
" encFileWriteStream.write(mac);",
|
||||
" const encFinishPromise = once(encFileWriteStream, 'finish');",
|
||||
" const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();",
|
||||
" encFileWriteStream.end();",
|
||||
" originalFileStream?.end?.();",
|
||||
" stream.destroy();",
|
||||
" await Promise.all([encFinishPromise, originalFinishPromise]);",
|
||||
" logger?.debug('encrypted data successfully');",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_HOTFIX_SEQUENTIAL_REPLACEMENT = [
|
||||
" encFileWriteStream.write(mac);",
|
||||
" const encFinishPromise = once(encFileWriteStream, 'finish');",
|
||||
" const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();",
|
||||
" encFileWriteStream.end();",
|
||||
" originalFileStream?.end?.();",
|
||||
" stream.destroy();",
|
||||
" await encFinishPromise;",
|
||||
" await originalFinishPromise;",
|
||||
" logger?.debug('encrypted data successfully');",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_HOTFIX_FINISH_PROMISES_RE =
|
||||
/const\s+encFinishPromise\s*=\s*once\(encFileWriteStream,\s*'finish'\);\s*\n[\s\S]*const\s+originalFinishPromise\s*=\s*originalFileStream\s*\?\s*once\(originalFileStream,\s*'finish'\)\s*:\s*Promise\.resolve\(\);/u;
|
||||
const BAILEYS_MEDIA_HOTFIX_PROMISE_ALL_RE =
|
||||
/await\s+Promise\.all\(\[\s*encFinishPromise\s*,\s*originalFinishPromise\s*\]\);/u;
|
||||
const BAILEYS_MEDIA_HOTFIX_SEQUENTIAL_AWAITS_RE =
|
||||
/await\s+encFinishPromise;\s*(?:\/\/[^\n]*\n|\s)*await\s+originalFinishPromise;/u;
|
||||
const BAILEYS_MEDIA_DISPATCHER_NEEDLE = [
|
||||
" const response = await fetch(url, {",
|
||||
" dispatcher: fetchAgent,",
|
||||
" method: 'POST',",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_DISPATCHER_REPLACEMENT = [
|
||||
" const response = await fetch(url, {",
|
||||
" method: 'POST',",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE = [
|
||||
" 'Content-Type': 'application/octet-stream',",
|
||||
" Origin: DEFAULT_ORIGIN",
|
||||
" },",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_DISPATCHER_HEADER_REPLACEMENT = [
|
||||
" 'Content-Type': 'application/octet-stream',",
|
||||
" Origin: DEFAULT_ORIGIN",
|
||||
" },",
|
||||
" // Baileys passes a generic agent here in some runtimes. Undici's",
|
||||
" // `dispatcher` only works with Dispatcher-compatible implementations,",
|
||||
" // so only wire it through when the object actually implements",
|
||||
" // `dispatch`.",
|
||||
" ...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_ONCE_IMPORT_RE = /import\s+\{\s*once\s*\}\s+from\s+['"]events['"]/u;
|
||||
const BAILEYS_MEDIA_ASYNC_CONTEXT_RE =
|
||||
/async\s+function\s+encryptedStream|encryptedStream\s*=\s*async/u;
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function normalizeRelativePath(filePath) {
|
||||
return filePath.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function readInstalledDistInventory(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const readFile = params.readFileSync ?? readFileSync;
|
||||
const inventoryPath = join(packageRoot, DIST_INVENTORY_PATH);
|
||||
if (!pathExists(inventoryPath)) {
|
||||
throw new Error(`missing dist inventory: ${DIST_INVENTORY_PATH}`);
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(readFile(inventoryPath, "utf8"));
|
||||
} catch {
|
||||
throw new Error(`invalid dist inventory: ${DIST_INVENTORY_PATH}`);
|
||||
}
|
||||
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
||||
throw new Error(`invalid dist inventory: ${DIST_INVENTORY_PATH}`);
|
||||
}
|
||||
return new Set(parsed.map(normalizeRelativePath));
|
||||
}
|
||||
|
||||
function isRecoverableInstalledDistInventoryError(error) {
|
||||
return error instanceof Error && /^(missing|invalid) dist inventory: /u.test(error.message);
|
||||
}
|
||||
|
||||
function resolveInstalledDistRoot(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const pathLstat = params.lstatSync ?? lstatSync;
|
||||
const resolveRealPath = params.realpathSync ?? realpathSync;
|
||||
const distDir = join(packageRoot, "dist");
|
||||
if (!pathExists(distDir)) {
|
||||
return null;
|
||||
}
|
||||
const distStats = pathLstat(distDir);
|
||||
if (!distStats.isDirectory() || distStats.isSymbolicLink()) {
|
||||
throw new Error("unsafe dist root: dist must be a real directory");
|
||||
}
|
||||
const packageRootReal = resolveRealPath(packageRoot);
|
||||
const distDirReal = resolveRealPath(distDir);
|
||||
const relativeDistPath = relative(packageRootReal, distDirReal);
|
||||
if (relativeDistPath !== "dist") {
|
||||
throw new Error("unsafe dist root: dist escaped package root");
|
||||
}
|
||||
return { distDir, distDirReal, packageRootReal };
|
||||
}
|
||||
|
||||
function assertSafeInstalledDistPath(relativePath, params) {
|
||||
const resolveRealPath = params.realpathSync ?? realpathSync;
|
||||
const candidatePath = join(params.packageRoot, relativePath);
|
||||
const candidateRealPath = resolveRealPath(candidatePath);
|
||||
const relativeCandidatePath = relative(params.distDirReal, candidateRealPath);
|
||||
if (relativeCandidatePath.startsWith("..") || isAbsolute(relativeCandidatePath)) {
|
||||
throw new Error(`unsafe dist path: ${relativePath}`);
|
||||
}
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
function listInstalledDistFiles(params = {}) {
|
||||
const readDir = params.readdirSync ?? readdirSync;
|
||||
const distRoot = resolveInstalledDistRoot(params);
|
||||
if (distRoot === null) {
|
||||
return [];
|
||||
}
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const pending = [distRoot.distDir];
|
||||
const files = [];
|
||||
while (pending.length > 0) {
|
||||
const currentDir = pending.pop();
|
||||
if (!currentDir) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readDir(currentDir, { withFileTypes: true })) {
|
||||
const entryPath = join(currentDir, entry.name);
|
||||
if (entry.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`unsafe dist entry: ${normalizeRelativePath(relative(packageRoot, entryPath))}`,
|
||||
);
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
pending.push(entryPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
const relativePath = normalizeRelativePath(relative(packageRoot, entryPath));
|
||||
if (relativePath === DIST_INVENTORY_PATH) {
|
||||
continue;
|
||||
}
|
||||
files.push(relativePath);
|
||||
}
|
||||
}
|
||||
return files.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function pruneEmptyDistDirectories(params = {}) {
|
||||
const readDir = params.readdirSync ?? readdirSync;
|
||||
const removeDirectory = params.rmdirSync ?? rmdirSync;
|
||||
const distRoot = resolveInstalledDistRoot(params);
|
||||
if (distRoot === null) {
|
||||
return;
|
||||
}
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const pathLstat = params.lstatSync ?? lstatSync;
|
||||
|
||||
function prune(currentDir) {
|
||||
for (const entry of readDir(currentDir, { withFileTypes: true })) {
|
||||
if (entry.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`unsafe dist entry: ${normalizeRelativePath(relative(packageRoot, join(currentDir, entry.name)))}`,
|
||||
);
|
||||
}
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
prune(join(currentDir, entry.name));
|
||||
}
|
||||
if (currentDir === distRoot.distDir) {
|
||||
return;
|
||||
}
|
||||
const currentStats = pathLstat(currentDir);
|
||||
if (!currentStats.isDirectory() || currentStats.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`unsafe dist directory: ${normalizeRelativePath(relative(packageRoot, currentDir))}`,
|
||||
);
|
||||
}
|
||||
if (readDir(currentDir).length === 0) {
|
||||
removeDirectory(
|
||||
assertSafeInstalledDistPath(normalizeRelativePath(relative(packageRoot, currentDir)), {
|
||||
packageRoot,
|
||||
distDirReal: distRoot.distDirReal,
|
||||
realpathSync: params.realpathSync,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
prune(distRoot.distDir);
|
||||
}
|
||||
|
||||
export function pruneInstalledPackageDist(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const removeFile = params.unlinkSync ?? unlinkSync;
|
||||
const log = params.log ?? console;
|
||||
const distRoot = resolveInstalledDistRoot(params);
|
||||
if (distRoot === null) {
|
||||
return [];
|
||||
}
|
||||
let expectedFiles = params.expectedFiles ?? null;
|
||||
if (expectedFiles === null) {
|
||||
try {
|
||||
expectedFiles = readInstalledDistInventory(params);
|
||||
} catch (error) {
|
||||
if (!isRecoverableInstalledDistInventoryError(error)) {
|
||||
throw error;
|
||||
}
|
||||
log.warn?.(`[postinstall] skipping dist prune: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const installedFiles = listInstalledDistFiles(params);
|
||||
const removed = [];
|
||||
|
||||
for (const relativePath of installedFiles) {
|
||||
if (expectedFiles.has(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
removeFile(
|
||||
assertSafeInstalledDistPath(relativePath, {
|
||||
packageRoot,
|
||||
distDirReal: distRoot.distDirReal,
|
||||
realpathSync: params.realpathSync,
|
||||
}),
|
||||
);
|
||||
removed.push(relativePath);
|
||||
}
|
||||
|
||||
pruneEmptyDistDirectories(params);
|
||||
|
||||
if (removed.length > 0) {
|
||||
log.log(`[postinstall] pruned stale dist files: ${removed.join(", ")}`);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
export function restoreLegacyUpdaterCompatSidecars(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const writeFile = params.writeFileSync ?? writeFileSync;
|
||||
const makeDirectory = params.mkdirSync ?? mkdirSync;
|
||||
const log = params.log ?? console;
|
||||
const restored = [];
|
||||
|
||||
for (const sidecar of LEGACY_UPDATE_COMPAT_SIDECARS) {
|
||||
// Older npm updater builds verify these exact sidecars after npm has
|
||||
// already replaced the package, so generate them independently of prune
|
||||
// results.
|
||||
const sidecarPath = join(packageRoot, sidecar.path);
|
||||
makeDirectory(dirname(sidecarPath), { recursive: true });
|
||||
writeFile(sidecarPath, sidecar.content, "utf8");
|
||||
restored.push(sidecar.path);
|
||||
}
|
||||
|
||||
if (restored.length > 0) {
|
||||
log.log(`[postinstall] restored legacy updater compat sidecars: ${restored.join(", ")}`);
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
function dependencySentinelPath(depName) {
|
||||
return join("node_modules", ...depName.split("/"), "package.json");
|
||||
}
|
||||
|
||||
const KNOWN_NATIVE_PLATFORMS = new Set([
|
||||
"aix",
|
||||
"android",
|
||||
"darwin",
|
||||
"freebsd",
|
||||
"linux",
|
||||
"openbsd",
|
||||
"sunos",
|
||||
"win32",
|
||||
]);
|
||||
const KNOWN_NATIVE_ARCHES = new Set(["arm", "arm64", "ia32", "ppc64", "riscv64", "s390x", "x64"]);
|
||||
|
||||
function packageNameTokens(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.split(/[/@._-]+/u)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function optionalDependencyTargetsRuntime(name, params = {}) {
|
||||
const platform = params.platform ?? process.platform;
|
||||
const arch = params.arch ?? process.arch;
|
||||
const tokens = new Set(packageNameTokens(name));
|
||||
const hasNativePlatformToken = [...tokens].some((token) => KNOWN_NATIVE_PLATFORMS.has(token));
|
||||
const hasNativeArchToken = [...tokens].some((token) => KNOWN_NATIVE_ARCHES.has(token));
|
||||
return hasNativePlatformToken && hasNativeArchToken && tokens.has(platform) && tokens.has(arch);
|
||||
}
|
||||
|
||||
function runtimeDepNeedsInstall(params) {
|
||||
const packageJsonPath = join(params.packageRoot, params.dep.sentinelPath);
|
||||
if (!params.existsSync(packageJsonPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = params.readJson(packageJsonPath);
|
||||
return Object.keys(packageJson.optionalDependencies ?? {}).some(
|
||||
(childName) =>
|
||||
optionalDependencyTargetsRuntime(childName, {
|
||||
arch: params.arch,
|
||||
platform: params.platform,
|
||||
}) && !params.existsSync(join(params.packageRoot, dependencySentinelPath(childName))),
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function collectRuntimeDeps(packageJson) {
|
||||
return {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.optionalDependencies,
|
||||
};
|
||||
}
|
||||
|
||||
export function discoverBundledPluginRuntimeDeps(params = {}) {
|
||||
const extensionsDir = params.extensionsDir ?? DEFAULT_EXTENSIONS_DIR;
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const readDir = params.readdirSync ?? readdirSync;
|
||||
const readJsonFile = params.readJson ?? readJson;
|
||||
const deps = new Map(
|
||||
BUNDLED_PLUGIN_INSTALL_TARGETS.map((target) => [
|
||||
target.name,
|
||||
{
|
||||
name: target.name,
|
||||
version: target.version,
|
||||
sentinelPath: dependencySentinelPath(target.name),
|
||||
pluginIds: [...(target.pluginIds ?? [])],
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
if (!pathExists(extensionsDir)) {
|
||||
return [...deps.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
for (const entry of readDir(extensionsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const pluginId = entry.name;
|
||||
const packageJsonPath = join(extensionsDir, pluginId, "package.json");
|
||||
if (!pathExists(packageJsonPath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const packageJson = readJsonFile(packageJsonPath);
|
||||
for (const [name, version] of Object.entries(collectRuntimeDeps(packageJson))) {
|
||||
const existing = deps.get(name);
|
||||
if (existing) {
|
||||
if (existing.version !== version) {
|
||||
continue;
|
||||
}
|
||||
if (!existing.pluginIds.includes(pluginId)) {
|
||||
existing.pluginIds.push(pluginId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
deps.set(name, {
|
||||
name,
|
||||
version,
|
||||
sentinelPath: dependencySentinelPath(name),
|
||||
pluginIds: [pluginId],
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed plugin manifests; runtime will surface those separately.
|
||||
}
|
||||
}
|
||||
|
||||
return [...deps.values()]
|
||||
.map((dep) =>
|
||||
Object.assign({}, dep, {
|
||||
pluginIds: [...dep.pluginIds].toSorted((a, b) => a.localeCompare(b)),
|
||||
}),
|
||||
)
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export function createNestedNpmInstallEnv(env = process.env) {
|
||||
const nextEnv = { ...env };
|
||||
delete nextEnv.npm_config_global;
|
||||
delete nextEnv.npm_config_location;
|
||||
delete nextEnv.npm_config_prefix;
|
||||
return nextEnv;
|
||||
}
|
||||
|
||||
function shouldEagerInstallBundledPluginDeps(env = process.env) {
|
||||
return env?.[EAGER_BUNDLED_PLUGIN_DEPS_ENV]?.trim() === "1";
|
||||
}
|
||||
|
||||
export function applyBaileysEncryptedStreamFinishHotfix(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const pathLstat = params.lstatSync ?? lstatSync;
|
||||
const readFile = params.readFileSync ?? readFileSync;
|
||||
const resolveRealPath = params.realpathSync ?? realpathSync;
|
||||
const chmodFile = params.chmodSync ?? chmodSync;
|
||||
const openFile = params.openSync ?? openSync;
|
||||
const closeFile = params.closeSync ?? closeSync;
|
||||
const renameFile = params.renameSync ?? renameSync;
|
||||
const removePath = params.rmSync ?? rmSync;
|
||||
const createTempPath =
|
||||
params.createTempPath ??
|
||||
((unsafeTargetPath) =>
|
||||
join(
|
||||
dirname(unsafeTargetPath),
|
||||
`.${basename(unsafeTargetPath)}.openclaw-hotfix-${randomUUID()}`,
|
||||
));
|
||||
const writeFile =
|
||||
params.writeFileSync ?? ((filePath, value) => writeFileSync(filePath, value, "utf8"));
|
||||
const targetPath = join(packageRoot, BAILEYS_MEDIA_FILE);
|
||||
const nodeModulesRoot = join(packageRoot, "node_modules");
|
||||
|
||||
function validateTargetPath() {
|
||||
if (!pathExists(targetPath)) {
|
||||
return { ok: false, reason: "missing" };
|
||||
}
|
||||
|
||||
const targetStats = pathLstat(targetPath);
|
||||
if (!targetStats.isFile() || targetStats.isSymbolicLink()) {
|
||||
return { ok: false, reason: "unsafe_target", targetPath };
|
||||
}
|
||||
|
||||
const nodeModulesRootReal = resolveRealPath(nodeModulesRoot);
|
||||
const targetPathReal = resolveRealPath(targetPath);
|
||||
const relativeTargetPath = relative(nodeModulesRootReal, targetPathReal);
|
||||
if (relativeTargetPath.startsWith("..") || isAbsolute(relativeTargetPath)) {
|
||||
return { ok: false, reason: "path_escape", targetPath };
|
||||
}
|
||||
|
||||
return { ok: true, targetPathReal, mode: targetStats.mode & 0o777 };
|
||||
}
|
||||
|
||||
try {
|
||||
const initialTargetValidation = validateTargetPath();
|
||||
if (!initialTargetValidation.ok) {
|
||||
return { applied: false, reason: initialTargetValidation.reason, targetPath };
|
||||
}
|
||||
|
||||
const currentText = readFile(targetPath, "utf8");
|
||||
let patchedText = currentText;
|
||||
let applied = false;
|
||||
|
||||
const encryptedStreamAlreadyPatched =
|
||||
patchedText.includes(BAILEYS_MEDIA_HOTFIX_REPLACEMENT) ||
|
||||
patchedText.includes(BAILEYS_MEDIA_HOTFIX_SEQUENTIAL_REPLACEMENT) ||
|
||||
(BAILEYS_MEDIA_HOTFIX_FINISH_PROMISES_RE.test(patchedText) &&
|
||||
(BAILEYS_MEDIA_HOTFIX_PROMISE_ALL_RE.test(patchedText) ||
|
||||
BAILEYS_MEDIA_HOTFIX_SEQUENTIAL_AWAITS_RE.test(patchedText)));
|
||||
const encryptedStreamPatchable = patchedText.includes(BAILEYS_MEDIA_HOTFIX_NEEDLE);
|
||||
|
||||
let encryptedStreamResolved = encryptedStreamAlreadyPatched;
|
||||
if (!encryptedStreamResolved && encryptedStreamPatchable) {
|
||||
if (!BAILEYS_MEDIA_ONCE_IMPORT_RE.test(patchedText)) {
|
||||
return { applied: false, reason: "missing_once_import", targetPath };
|
||||
}
|
||||
if (!BAILEYS_MEDIA_ASYNC_CONTEXT_RE.test(patchedText)) {
|
||||
return { applied: false, reason: "not_async_context", targetPath };
|
||||
}
|
||||
patchedText = patchedText.replace(
|
||||
BAILEYS_MEDIA_HOTFIX_NEEDLE,
|
||||
BAILEYS_MEDIA_HOTFIX_REPLACEMENT,
|
||||
);
|
||||
applied = true;
|
||||
encryptedStreamResolved = true;
|
||||
}
|
||||
|
||||
const dispatcherAlreadyPatched = patchedText.includes(
|
||||
"...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),",
|
||||
);
|
||||
const dispatcherPatchable =
|
||||
patchedText.includes(BAILEYS_MEDIA_DISPATCHER_NEEDLE) &&
|
||||
patchedText.includes(BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE);
|
||||
let dispatcherResolved = dispatcherAlreadyPatched;
|
||||
|
||||
if (!dispatcherResolved && dispatcherPatchable) {
|
||||
patchedText = patchedText
|
||||
.replace(BAILEYS_MEDIA_DISPATCHER_NEEDLE, BAILEYS_MEDIA_DISPATCHER_REPLACEMENT)
|
||||
.replace(
|
||||
BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE,
|
||||
BAILEYS_MEDIA_DISPATCHER_HEADER_REPLACEMENT,
|
||||
);
|
||||
applied = true;
|
||||
dispatcherResolved = true;
|
||||
}
|
||||
|
||||
if (!dispatcherResolved) {
|
||||
return { applied: false, reason: "unexpected_content", targetPath };
|
||||
}
|
||||
|
||||
if (!applied) {
|
||||
return { applied: false, reason: "already_patched" };
|
||||
}
|
||||
const tempPath = createTempPath(targetPath);
|
||||
const tempFd = openFile(tempPath, "wx", initialTargetValidation.mode);
|
||||
let tempFdClosed = false;
|
||||
try {
|
||||
writeFile(tempFd, patchedText, "utf8");
|
||||
closeFile(tempFd);
|
||||
tempFdClosed = true;
|
||||
const finalTargetValidation = validateTargetPath();
|
||||
if (!finalTargetValidation.ok) {
|
||||
return { applied: false, reason: finalTargetValidation.reason, targetPath };
|
||||
}
|
||||
renameFile(tempPath, targetPath);
|
||||
chmodFile(targetPath, initialTargetValidation.mode);
|
||||
} finally {
|
||||
if (!tempFdClosed) {
|
||||
try {
|
||||
closeFile(tempFd);
|
||||
} catch {
|
||||
// ignore failed-open cleanup
|
||||
}
|
||||
}
|
||||
removePath(tempPath, { force: true });
|
||||
}
|
||||
return { applied: true, reason: "patched", targetPath };
|
||||
} catch (error) {
|
||||
return {
|
||||
applied: false,
|
||||
reason: "error",
|
||||
targetPath,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function applyBundledPluginRuntimeHotfixes(params = {}) {
|
||||
const log = params.log ?? console;
|
||||
const baileysResult = applyBaileysEncryptedStreamFinishHotfix(params);
|
||||
if (baileysResult.applied) {
|
||||
log.log("[postinstall] patched @whiskeysockets/baileys runtime hotfixes");
|
||||
return;
|
||||
}
|
||||
if (baileysResult.reason !== "missing" && baileysResult.reason !== "already_patched") {
|
||||
log.warn(
|
||||
`[postinstall] could not patch @whiskeysockets/baileys runtime hotfixes: ${baileysResult.reason}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSourceCheckoutRoot(params) {
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
return (
|
||||
pathExists(join(params.packageRoot, ".git")) &&
|
||||
pathExists(join(params.packageRoot, "src")) &&
|
||||
pathExists(join(params.packageRoot, "extensions"))
|
||||
);
|
||||
}
|
||||
|
||||
export function pruneBundledPluginSourceNodeModules(params = {}) {
|
||||
const extensionsDir = params.extensionsDir ?? join(DEFAULT_PACKAGE_ROOT, "extensions");
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const readDir = params.readdirSync ?? readdirSync;
|
||||
const removePath = params.rmSync ?? rmSync;
|
||||
|
||||
if (!pathExists(extensionsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of readDir(extensionsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginDir = join(extensionsDir, entry.name);
|
||||
if (!pathExists(join(pluginDir, "package.json"))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
removePath(join(pluginDir, "node_modules"), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRunBundledPluginPostinstall(params) {
|
||||
if (params.env?.[DISABLE_POSTINSTALL_ENV]?.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (!params.existsSync(params.extensionsDir)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function runBundledPluginPostinstall(params = {}) {
|
||||
const env = params.env ?? process.env;
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const extensionsDir = params.extensionsDir ?? join(packageRoot, "dist", "extensions");
|
||||
const spawn = params.spawnSync ?? spawnSync;
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const log = params.log ?? console;
|
||||
if (env?.[DISABLE_POSTINSTALL_ENV]?.trim()) {
|
||||
return;
|
||||
}
|
||||
if (isSourceCheckoutRoot({ packageRoot, existsSync: pathExists })) {
|
||||
try {
|
||||
pruneBundledPluginSourceNodeModules({
|
||||
extensionsDir: join(packageRoot, "extensions"),
|
||||
existsSync: pathExists,
|
||||
readdirSync: params.readdirSync,
|
||||
rmSync: params.rmSync,
|
||||
});
|
||||
} catch (e) {
|
||||
log.warn(`[postinstall] could not prune bundled plugin source node_modules: ${String(e)}`);
|
||||
}
|
||||
applyBundledPluginRuntimeHotfixes({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const prunedDistFiles = pruneInstalledPackageDist({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
readdirSync: params.readdirSync,
|
||||
rmSync: params.rmSync,
|
||||
log,
|
||||
});
|
||||
restoreLegacyUpdaterCompatSidecars({
|
||||
packageRoot,
|
||||
removedFiles: prunedDistFiles,
|
||||
mkdirSync: params.mkdirSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
if (
|
||||
!shouldRunBundledPluginPostinstall({
|
||||
env,
|
||||
extensionsDir,
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!shouldEagerInstallBundledPluginDeps(env)) {
|
||||
applyBundledPluginRuntimeHotfixes({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const runtimeDeps =
|
||||
params.runtimeDeps ??
|
||||
discoverBundledPluginRuntimeDeps({ extensionsDir, existsSync: pathExists });
|
||||
const missingSpecs = runtimeDeps
|
||||
.filter((dep) =>
|
||||
runtimeDepNeedsInstall({
|
||||
dep,
|
||||
existsSync: pathExists,
|
||||
packageRoot,
|
||||
arch: params.arch,
|
||||
platform: params.platform,
|
||||
readJson: params.readJson ?? readJson,
|
||||
}),
|
||||
)
|
||||
.map((dep) => `${dep.name}@${dep.version}`);
|
||||
|
||||
if (missingSpecs.length === 0) {
|
||||
applyBundledPluginRuntimeHotfixes({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const nestedEnv = createNestedNpmInstallEnv(env);
|
||||
const npmRunner =
|
||||
params.npmRunner ??
|
||||
resolveNpmRunner({
|
||||
env: nestedEnv,
|
||||
execPath: params.execPath,
|
||||
existsSync: pathExists,
|
||||
platform: params.platform,
|
||||
comSpec: params.comSpec,
|
||||
npmArgs: [
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--no-save",
|
||||
"--package-lock=false",
|
||||
"--legacy-peer-deps",
|
||||
...missingSpecs,
|
||||
],
|
||||
});
|
||||
const result = spawn(npmRunner.command, npmRunner.args, {
|
||||
cwd: packageRoot,
|
||||
encoding: "utf8",
|
||||
env: npmRunner.env ?? nestedEnv,
|
||||
stdio: "pipe",
|
||||
shell: npmRunner.shell,
|
||||
windowsVerbatimArguments: npmRunner.windowsVerbatimArguments,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
const output = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
||||
throw new Error(output || "npm install failed");
|
||||
}
|
||||
log.log(`[postinstall] installed bundled plugin deps: ${missingSpecs.join(", ")}`);
|
||||
} catch (e) {
|
||||
// Non-fatal: gateway will surface the missing dep via doctor.
|
||||
log.warn(`[postinstall] could not install bundled plugin deps: ${String(e)}`);
|
||||
}
|
||||
|
||||
applyBundledPluginRuntimeHotfixes({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
runBundledPluginPostinstall();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue