diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index f4595f172bc..8145c741c08 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -96,6 +96,19 @@ const BAILEYS_MEDIA_DISPATCHER_HEADER_REPLACEMENT = [ " // `dispatch`.", " ...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),", ].join("\n"); +const BAILEYS_MEDIA_UPLOAD_WITH_FETCH_DISPATCHER_NEEDLE = [ + " const response = await fetch(url, {", + " dispatcher: agent,", + " method: 'POST',", +].join("\n"); +const BAILEYS_MEDIA_UPLOAD_WITH_FETCH_DISPATCHER_REPLACEMENT = [ + " const response = await fetch(url, {", + " // Baileys may pass a generic agent in some runtimes. Undici's dispatcher", + " // option only accepts Dispatcher-compatible implementations, so only wire", + " // it through when the object actually implements dispatch.", + " ...(typeof agent?.dispatch === 'function' ? { dispatcher: agent } : {}),", + " method: 'POST',", +].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; @@ -639,15 +652,22 @@ export function applyBaileysEncryptedStreamFinishHotfix(params = {}) { encryptedStreamResolved = true; } - const dispatcherAlreadyPatched = patchedText.includes( - "...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),", - ); - const dispatcherPatchable = + const dispatcherAlreadyPatched = + patchedText.includes( + "...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),", + ) || + patchedText.includes( + "...(typeof agent?.dispatch === 'function' ? { dispatcher: agent } : {}),", + ); + const legacyDispatcherPatchable = patchedText.includes(BAILEYS_MEDIA_DISPATCHER_NEEDLE) && patchedText.includes(BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE); + const uploadWithFetchDispatcherPatchable = patchedText.includes( + BAILEYS_MEDIA_UPLOAD_WITH_FETCH_DISPATCHER_NEEDLE, + ); let dispatcherResolved = dispatcherAlreadyPatched; - if (!dispatcherResolved && dispatcherPatchable) { + if (!dispatcherResolved && legacyDispatcherPatchable) { patchedText = patchedText .replace(BAILEYS_MEDIA_DISPATCHER_NEEDLE, BAILEYS_MEDIA_DISPATCHER_REPLACEMENT) .replace( @@ -658,6 +678,15 @@ export function applyBaileysEncryptedStreamFinishHotfix(params = {}) { dispatcherResolved = true; } + if (!dispatcherResolved && uploadWithFetchDispatcherPatchable) { + patchedText = patchedText.replace( + BAILEYS_MEDIA_UPLOAD_WITH_FETCH_DISPATCHER_NEEDLE, + BAILEYS_MEDIA_UPLOAD_WITH_FETCH_DISPATCHER_REPLACEMENT, + ); + applied = true; + dispatcherResolved = true; + } + if (!dispatcherResolved) { return { applied: false, reason: "unexpected_content", targetPath }; } diff --git a/test/scripts/postinstall-bundled-plugins.test.ts b/test/scripts/postinstall-bundled-plugins.test.ts index e43df832499..230973aebc4 100644 --- a/test/scripts/postinstall-bundled-plugins.test.ts +++ b/test/scripts/postinstall-bundled-plugins.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { + applyBaileysEncryptedStreamFinishHotfix, collectLegacyPluginRuntimeDepsStateRoots, isSourceCheckoutRoot, isDirectPostinstallInvocation, @@ -58,6 +59,21 @@ async function writePluginPackage( } } +async function writeBaileysMediaFile(packageRoot: string, text: string) { + const mediaFile = path.join( + packageRoot, + "node_modules", + "@whiskeysockets", + "baileys", + "lib", + "Utils", + "messages-media.js", + ); + await fs.mkdir(path.dirname(mediaFile), { recursive: true }); + await fs.writeFile(mediaFile, text); + return mediaFile; +} + describe("bundled plugin postinstall", () => { function existsSyncWithoutGlobalCompileCache(value: string) { if (path.resolve(value) === path.join(tmpdir(), "node-compile-cache")) { @@ -206,6 +222,83 @@ describe("bundled plugin postinstall", () => { expect(warn).not.toHaveBeenCalled(); }); + it("patches the Baileys rc10 upload helper dispatcher guard", async () => { + const packageRoot = await createTempDirAsync("openclaw-baileys-postinstall-"); + const mediaFile = await writeBaileysMediaFile( + packageRoot, + [ + "import { once } from 'events';", + "const encryptedStream = async () => {", + " 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');", + "};", + "const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {", + " const nodeStream = createReadStream(filePath);", + " const webStream = Readable.toWeb(nodeStream);", + " const response = await fetch(url, {", + " dispatcher: agent,", + " method: 'POST',", + " body: webStream,", + " headers,", + " duplex: 'half',", + " signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined", + " });", + "};", + "", + ].join("\n"), + ); + + expect(applyBaileysEncryptedStreamFinishHotfix({ packageRoot })).toMatchObject({ + applied: true, + reason: "patched", + }); + const patchedText = await fs.readFile(mediaFile, "utf8"); + expect(patchedText).toContain( + "...(typeof agent?.dispatch === 'function' ? { dispatcher: agent } : {}),", + ); + expect(patchedText).not.toContain(" dispatcher: agent,"); + }); + + it("recognizes already patched Baileys rc10 upload helpers", async () => { + const packageRoot = await createTempDirAsync("openclaw-baileys-postinstall-"); + await writeBaileysMediaFile( + packageRoot, + [ + "import { once } from 'events';", + "const encryptedStream = async () => {", + " 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');", + "};", + "const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {", + " const response = await fetch(url, {", + " ...(typeof agent?.dispatch === 'function' ? { dispatcher: agent } : {}),", + " method: 'POST',", + " });", + "};", + "", + ].join("\n"), + ); + + expect(applyBaileysEncryptedStreamFinishHotfix({ packageRoot })).toMatchObject({ + applied: false, + reason: "already_patched", + }); + }); + it("does not classify published packages with source files as source checkouts", () => { const packageRoot = "/pkg"; const existingPaths = new Set([