mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 10:02:04 +00:00
refactor: untangle whatsapp runtime boundary
This commit is contained in:
12
extensions/whatsapp/light-runtime-api.ts
Normal file
12
extensions/whatsapp/light-runtime-api.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { getActiveWebListener } from "./src/active-listener.js";
|
||||
export {
|
||||
getWebAuthAgeMs,
|
||||
logWebSelfId,
|
||||
logoutWeb,
|
||||
pickWebChannel,
|
||||
readWebSelfId,
|
||||
WA_WEB_AUTH_DIR,
|
||||
webAuthExists,
|
||||
} from "./src/auth-store.js";
|
||||
export { createWhatsAppLoginTool } from "./src/agent-tools-login.js";
|
||||
export { formatError, getStatusCode } from "./src/session-errors.js";
|
||||
@@ -4,6 +4,9 @@
|
||||
"private": true,
|
||||
"description": "OpenClaw WhatsApp channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from "./src/auth-store.js";
|
||||
export * from "./src/auto-reply.js";
|
||||
export * from "./src/inbound.js";
|
||||
export * from "./src/login.js";
|
||||
export * from "./src/login-qr.js";
|
||||
export * from "./src/media.js";
|
||||
export * from "./src/send.js";
|
||||
export * from "./src/session.js";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ChannelAgentTool } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { startWebLoginWithQr, waitForWebLogin } from "openclaw/plugin-sdk/whatsapp-login-qr";
|
||||
|
||||
export function createWhatsAppLoginTool(): ChannelAgentTool {
|
||||
return {
|
||||
@@ -18,7 +19,6 @@ export function createWhatsAppLoginTool(): ChannelAgentTool {
|
||||
force: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
execute: async (_toolCallId, args) => {
|
||||
const { startWebLoginWithQr, waitForWebLogin } = await import("./login-qr.js");
|
||||
const action = (args as { action?: string })?.action ?? "start";
|
||||
if (action === "wait") {
|
||||
const result = await waitForWebLogin({
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
readWebSelfId as readWebSelfIdImpl,
|
||||
webAuthExists as webAuthExistsImpl,
|
||||
} from "./auth-store.js";
|
||||
import { monitorWebChannel as monitorWebChannelImpl } from "./auto-reply/monitor.js";
|
||||
import { loginWeb as loginWebImpl } from "./login.js";
|
||||
import { monitorWebChannel as monitorWebChannelImpl } from "./runtime-api.js";
|
||||
import { whatsappSetupWizard as whatsappSetupWizardImpl } from "./setup-surface.js";
|
||||
|
||||
type GetActiveWebListener = typeof import("./active-listener.js").getActiveWebListener;
|
||||
@@ -20,7 +20,7 @@ type LoginWeb = typeof import("./login.js").loginWeb;
|
||||
type StartWebLoginWithQr = typeof import("./login-qr.js").startWebLoginWithQr;
|
||||
type WaitForWebLogin = typeof import("./login-qr.js").waitForWebLogin;
|
||||
type WhatsAppSetupWizard = typeof import("./setup-surface.js").whatsappSetupWizard;
|
||||
type MonitorWebChannel = typeof import("./runtime-api.js").monitorWebChannel;
|
||||
type MonitorWebChannel = typeof import("./auto-reply/monitor.js").monitorWebChannel;
|
||||
|
||||
let loginQrPromise: Promise<typeof import("./login-qr.js")> | null = null;
|
||||
|
||||
@@ -75,8 +75,8 @@ export async function waitForWebLogin(
|
||||
|
||||
export const whatsappSetupWizard: WhatsAppSetupWizard = { ...whatsappSetupWizardImpl };
|
||||
|
||||
export async function monitorWebChannel(
|
||||
export function monitorWebChannel(
|
||||
...args: Parameters<MonitorWebChannel>
|
||||
): ReturnType<MonitorWebChannel> {
|
||||
return await monitorWebChannelImpl(...args);
|
||||
return monitorWebChannelImpl(...args);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
// WhatsApp-specific imports from local extension code (moved from src/web/ and src/channels/plugins/)
|
||||
import { resolveWhatsAppAccount, type ResolvedWhatsAppAccount } from "./accounts.js";
|
||||
import type { WebChannelStatus } from "./auto-reply/types.js";
|
||||
import {
|
||||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
@@ -282,7 +283,8 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
ctx.runtime,
|
||||
ctx.abortSignal,
|
||||
{
|
||||
statusSink: (next) => ctx.setStatus({ accountId: ctx.accountId, ...next }),
|
||||
statusSink: (next: WebChannelStatus) =>
|
||||
ctx.setStatus({ accountId: ctx.accountId, ...next }),
|
||||
accountId: account.accountId,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -26,6 +26,6 @@ export {
|
||||
type DmPolicy,
|
||||
type GroupPolicy,
|
||||
type WhatsAppAccountConfig,
|
||||
} from "openclaw/plugin-sdk/whatsapp";
|
||||
} from "openclaw/plugin-sdk/whatsapp-shared";
|
||||
|
||||
export { monitorWebChannel } from "openclaw/plugin-sdk/whatsapp";
|
||||
export { monitorWebChannel } from "./channel.runtime.js";
|
||||
|
||||
123
extensions/whatsapp/src/session-errors.ts
Normal file
123
extensions/whatsapp/src/session-errors.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
function safeStringify(value: unknown, limit = 800): string {
|
||||
try {
|
||||
const seen = new WeakSet();
|
||||
const raw = JSON.stringify(
|
||||
value,
|
||||
(_key, v) => {
|
||||
if (typeof v === "bigint") {
|
||||
return v.toString();
|
||||
}
|
||||
if (typeof v === "function") {
|
||||
const maybeName = (v as { name?: unknown }).name;
|
||||
const name =
|
||||
typeof maybeName === "string" && maybeName.length > 0 ? maybeName : "anonymous";
|
||||
return `[Function ${name}]`;
|
||||
}
|
||||
if (typeof v === "object" && v) {
|
||||
if (seen.has(v)) {
|
||||
return "[Circular]";
|
||||
}
|
||||
seen.add(v);
|
||||
}
|
||||
return v;
|
||||
},
|
||||
2,
|
||||
);
|
||||
if (!raw) {
|
||||
return String(value);
|
||||
}
|
||||
return raw.length > limit ? `${raw.slice(0, limit)}…` : raw;
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function extractBoomDetails(err: unknown): {
|
||||
statusCode?: number;
|
||||
error?: string;
|
||||
message?: string;
|
||||
} | null {
|
||||
if (!err || typeof err !== "object") {
|
||||
return null;
|
||||
}
|
||||
const output = (err as { output?: unknown })?.output as
|
||||
| { statusCode?: unknown; payload?: unknown }
|
||||
| undefined;
|
||||
if (!output || typeof output !== "object") {
|
||||
return null;
|
||||
}
|
||||
const payload = (output as { payload?: unknown }).payload as
|
||||
| { error?: unknown; message?: unknown; statusCode?: unknown }
|
||||
| undefined;
|
||||
const statusCode =
|
||||
typeof (output as { statusCode?: unknown }).statusCode === "number"
|
||||
? ((output as { statusCode?: unknown }).statusCode as number)
|
||||
: typeof payload?.statusCode === "number"
|
||||
? payload.statusCode
|
||||
: undefined;
|
||||
const error = typeof payload?.error === "string" ? payload.error : undefined;
|
||||
const message = typeof payload?.message === "string" ? payload.message : undefined;
|
||||
if (!statusCode && !error && !message) {
|
||||
return null;
|
||||
}
|
||||
return { statusCode, error, message };
|
||||
}
|
||||
|
||||
export function getStatusCode(err: unknown) {
|
||||
return (
|
||||
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
||||
(err as { status?: number })?.status ??
|
||||
(err as { error?: { output?: { statusCode?: number } } })?.error?.output?.statusCode
|
||||
);
|
||||
}
|
||||
|
||||
export function formatError(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
if (typeof err === "string") {
|
||||
return err;
|
||||
}
|
||||
if (!err || typeof err !== "object") {
|
||||
return String(err);
|
||||
}
|
||||
|
||||
const boom =
|
||||
extractBoomDetails(err) ??
|
||||
extractBoomDetails((err as { error?: unknown })?.error) ??
|
||||
extractBoomDetails((err as { lastDisconnect?: { error?: unknown } })?.lastDisconnect?.error);
|
||||
|
||||
const status = boom?.statusCode ?? getStatusCode(err);
|
||||
const code = (err as { code?: unknown })?.code;
|
||||
const codeText = typeof code === "string" || typeof code === "number" ? String(code) : undefined;
|
||||
|
||||
const messageCandidates = [
|
||||
boom?.message,
|
||||
typeof (err as { message?: unknown })?.message === "string"
|
||||
? ((err as { message?: unknown }).message as string)
|
||||
: undefined,
|
||||
typeof (err as { error?: { message?: unknown } })?.error?.message === "string"
|
||||
? ((err as { error?: { message?: unknown } }).error?.message as string)
|
||||
: undefined,
|
||||
].filter((value): value is string => Boolean(value && value.trim().length > 0));
|
||||
const message = messageCandidates[0];
|
||||
|
||||
const pieces: string[] = [];
|
||||
if (typeof status === "number") {
|
||||
pieces.push(`status=${status}`);
|
||||
}
|
||||
if (boom?.error) {
|
||||
pieces.push(boom.error);
|
||||
}
|
||||
if (message) {
|
||||
pieces.push(message);
|
||||
}
|
||||
if (codeText) {
|
||||
pieces.push(`code=${codeText}`);
|
||||
}
|
||||
|
||||
if (pieces.length > 0) {
|
||||
return pieces.join(" ");
|
||||
}
|
||||
return safeStringify(err);
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
resolveWebCredsBackupPath,
|
||||
resolveWebCredsPath,
|
||||
} from "./auth-store.js";
|
||||
import { formatError, getStatusCode } from "./session-errors.js";
|
||||
export { formatError, getStatusCode } from "./session-errors.js";
|
||||
|
||||
export {
|
||||
getWebAuthAgeMs,
|
||||
@@ -190,14 +192,6 @@ export async function waitForWaConnection(sock: ReturnType<typeof makeWASocket>)
|
||||
});
|
||||
}
|
||||
|
||||
export function getStatusCode(err: unknown) {
|
||||
return (
|
||||
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
||||
(err as { status?: number })?.status ??
|
||||
(err as { error?: { output?: { statusCode?: number } } })?.error?.output?.statusCode
|
||||
);
|
||||
}
|
||||
|
||||
/** Await pending credential saves — scoped to one authDir, or all if omitted. */
|
||||
export function waitForCredsSaveQueue(authDir?: string): Promise<void> {
|
||||
if (authDir) {
|
||||
@@ -224,123 +218,6 @@ export async function waitForCredsSaveQueueWithTimeout(
|
||||
});
|
||||
}
|
||||
|
||||
function safeStringify(value: unknown, limit = 800): string {
|
||||
try {
|
||||
const seen = new WeakSet();
|
||||
const raw = JSON.stringify(
|
||||
value,
|
||||
(_key, v) => {
|
||||
if (typeof v === "bigint") {
|
||||
return v.toString();
|
||||
}
|
||||
if (typeof v === "function") {
|
||||
const maybeName = (v as { name?: unknown }).name;
|
||||
const name =
|
||||
typeof maybeName === "string" && maybeName.length > 0 ? maybeName : "anonymous";
|
||||
return `[Function ${name}]`;
|
||||
}
|
||||
if (typeof v === "object" && v) {
|
||||
if (seen.has(v)) {
|
||||
return "[Circular]";
|
||||
}
|
||||
seen.add(v);
|
||||
}
|
||||
return v;
|
||||
},
|
||||
2,
|
||||
);
|
||||
if (!raw) {
|
||||
return String(value);
|
||||
}
|
||||
return raw.length > limit ? `${raw.slice(0, limit)}…` : raw;
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function extractBoomDetails(err: unknown): {
|
||||
statusCode?: number;
|
||||
error?: string;
|
||||
message?: string;
|
||||
} | null {
|
||||
if (!err || typeof err !== "object") {
|
||||
return null;
|
||||
}
|
||||
const output = (err as { output?: unknown })?.output as
|
||||
| { statusCode?: unknown; payload?: unknown }
|
||||
| undefined;
|
||||
if (!output || typeof output !== "object") {
|
||||
return null;
|
||||
}
|
||||
const payload = (output as { payload?: unknown }).payload as
|
||||
| { error?: unknown; message?: unknown; statusCode?: unknown }
|
||||
| undefined;
|
||||
const statusCode =
|
||||
typeof (output as { statusCode?: unknown }).statusCode === "number"
|
||||
? ((output as { statusCode?: unknown }).statusCode as number)
|
||||
: typeof payload?.statusCode === "number"
|
||||
? payload.statusCode
|
||||
: undefined;
|
||||
const error = typeof payload?.error === "string" ? payload.error : undefined;
|
||||
const message = typeof payload?.message === "string" ? payload.message : undefined;
|
||||
if (!statusCode && !error && !message) {
|
||||
return null;
|
||||
}
|
||||
return { statusCode, error, message };
|
||||
}
|
||||
|
||||
export function formatError(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
if (typeof err === "string") {
|
||||
return err;
|
||||
}
|
||||
if (!err || typeof err !== "object") {
|
||||
return String(err);
|
||||
}
|
||||
|
||||
// Baileys frequently wraps errors under `error` with a Boom-like shape.
|
||||
const boom =
|
||||
extractBoomDetails(err) ??
|
||||
extractBoomDetails((err as { error?: unknown })?.error) ??
|
||||
extractBoomDetails((err as { lastDisconnect?: { error?: unknown } })?.lastDisconnect?.error);
|
||||
|
||||
const status = boom?.statusCode ?? getStatusCode(err);
|
||||
const code = (err as { code?: unknown })?.code;
|
||||
const codeText = typeof code === "string" || typeof code === "number" ? String(code) : undefined;
|
||||
|
||||
const messageCandidates = [
|
||||
boom?.message,
|
||||
typeof (err as { message?: unknown })?.message === "string"
|
||||
? ((err as { message?: unknown }).message as string)
|
||||
: undefined,
|
||||
typeof (err as { error?: { message?: unknown } })?.error?.message === "string"
|
||||
? ((err as { error?: { message?: unknown } }).error?.message as string)
|
||||
: undefined,
|
||||
].filter((v): v is string => Boolean(v && v.trim().length > 0));
|
||||
const message = messageCandidates[0];
|
||||
|
||||
const pieces: string[] = [];
|
||||
if (typeof status === "number") {
|
||||
pieces.push(`status=${status}`);
|
||||
}
|
||||
if (boom?.error) {
|
||||
pieces.push(boom.error);
|
||||
}
|
||||
if (message) {
|
||||
pieces.push(message);
|
||||
}
|
||||
if (codeText) {
|
||||
pieces.push(`code=${codeText}`);
|
||||
}
|
||||
|
||||
if (pieces.length > 0) {
|
||||
return pieces.join(" ");
|
||||
}
|
||||
return safeStringify(err);
|
||||
}
|
||||
|
||||
export function newConnectionId() {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user