mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 15:21:06 +00:00
fix(outbound): restore generic delivery and security seams
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { listBundledPluginManifestSnapshots } from "./bundled-manifest-snapshots.js";
|
||||
import { listBundledPluginMetadata } from "./bundled-plugin-metadata.js";
|
||||
|
||||
export type BundledPluginContractSnapshot = {
|
||||
pluginId: string;
|
||||
@@ -15,27 +12,6 @@ export type BundledPluginContractSnapshot = {
|
||||
toolNames: string[];
|
||||
};
|
||||
|
||||
function resolveBundledManifestSnapshotDir(): string | undefined {
|
||||
const packageRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
|
||||
if (!packageRoot) {
|
||||
return undefined;
|
||||
}
|
||||
for (const candidate of [
|
||||
path.join(packageRoot, "extensions"),
|
||||
path.join(packageRoot, "dist", "extensions"),
|
||||
path.join(packageRoot, "dist-runtime", "extensions"),
|
||||
]) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const BUNDLED_PLUGIN_MANIFEST_SNAPSHOTS = listBundledPluginManifestSnapshots({
|
||||
bundledDir: resolveBundledManifestSnapshotDir(),
|
||||
});
|
||||
|
||||
function uniqueStrings(values: readonly string[] | undefined): string[] {
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -50,8 +26,13 @@ function uniqueStrings(values: readonly string[] | undefined): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
const BUNDLED_PLUGIN_METADATA_FOR_CAPABILITIES = listBundledPluginMetadata({
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
});
|
||||
|
||||
export const BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS: readonly BundledPluginContractSnapshot[] =
|
||||
BUNDLED_PLUGIN_MANIFEST_SNAPSHOTS.map(({ manifest }) => ({
|
||||
BUNDLED_PLUGIN_METADATA_FOR_CAPABILITIES.map(({ manifest }) => ({
|
||||
pluginId: manifest.id,
|
||||
cliBackendIds: uniqueStrings(manifest.cliBackends),
|
||||
providerIds: uniqueStrings(manifest.providers),
|
||||
@@ -130,7 +111,7 @@ export const BUNDLED_PROVIDER_PLUGIN_ID_ALIASES = Object.fromEntries(
|
||||
) as Readonly<Record<string, string>>;
|
||||
|
||||
export const BUNDLED_LEGACY_PLUGIN_ID_ALIASES = Object.fromEntries(
|
||||
BUNDLED_PLUGIN_MANIFEST_SNAPSHOTS.flatMap(({ manifest }) =>
|
||||
BUNDLED_PLUGIN_METADATA_FOR_CAPABILITIES.flatMap(({ manifest }) =>
|
||||
(manifest.legacyPluginIds ?? []).map(
|
||||
(legacyPluginId) => [legacyPluginId, manifest.id] as const,
|
||||
),
|
||||
@@ -138,7 +119,7 @@ export const BUNDLED_LEGACY_PLUGIN_ID_ALIASES = Object.fromEntries(
|
||||
) as Readonly<Record<string, string>>;
|
||||
|
||||
export const BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS = Object.fromEntries(
|
||||
BUNDLED_PLUGIN_MANIFEST_SNAPSHOTS.flatMap(({ manifest }) =>
|
||||
BUNDLED_PLUGIN_METADATA_FOR_CAPABILITIES.flatMap(({ manifest }) =>
|
||||
(manifest.autoEnableWhenConfiguredProviders ?? []).map((providerId) => [
|
||||
providerId,
|
||||
manifest.id,
|
||||
|
||||
@@ -432,8 +432,6 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"resolveThreadBindingThreadName",
|
||||
"resolveThreadBindingsEnabled",
|
||||
"formatThreadBindingDisabledError",
|
||||
"DISCORD_THREAD_BINDING_CHANNEL",
|
||||
"MATRIX_THREAD_BINDING_CHANNEL",
|
||||
"resolveControlCommandGate",
|
||||
"resolveCommandAuthorizedFromAuthorizers",
|
||||
"resolveDualTextControlCommandGate",
|
||||
@@ -621,8 +619,6 @@ describe("plugin-sdk subpath exports", () => {
|
||||
]);
|
||||
|
||||
expectSourceMentions("conversation-runtime", [
|
||||
"DISCORD_THREAD_BINDING_CHANNEL",
|
||||
"MATRIX_THREAD_BINDING_CHANNEL",
|
||||
"formatThreadBindingDisabledError",
|
||||
"resolveThreadBindingFarewellText",
|
||||
"resolveThreadBindingConversationIdFromBindingId",
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createDiscordTypingLease,
|
||||
type CreateDiscordTypingLeaseParams,
|
||||
} from "./runtime-discord-typing.js";
|
||||
|
||||
describe("createDiscordTypingLease", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("uses the Discord default interval and forwards pulse params", async () => {
|
||||
vi.useFakeTimers();
|
||||
const pulse: CreateDiscordTypingLeaseParams["pulse"] = vi.fn(async () => undefined);
|
||||
const cfg = { channels: { discord: { token: "x" } } };
|
||||
|
||||
const lease = await createDiscordTypingLease({
|
||||
channelId: "123",
|
||||
accountId: "work",
|
||||
cfg,
|
||||
intervalMs: Number.NaN,
|
||||
pulse,
|
||||
});
|
||||
|
||||
expect(pulse).toHaveBeenCalledTimes(1);
|
||||
expect(pulse).toHaveBeenCalledWith({
|
||||
channelId: "123",
|
||||
accountId: "work",
|
||||
cfg,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(7_999);
|
||||
expect(pulse).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(pulse).toHaveBeenCalledTimes(2);
|
||||
|
||||
lease.stop();
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createTypingLease } from "./typing-lease.js";
|
||||
|
||||
export type CreateDiscordTypingLeaseParams = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
intervalMs?: number;
|
||||
pulse: (params: {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
|
||||
const DEFAULT_DISCORD_TYPING_INTERVAL_MS = 8_000;
|
||||
|
||||
export async function createDiscordTypingLease(params: CreateDiscordTypingLeaseParams): Promise<{
|
||||
refresh: () => Promise<void>;
|
||||
stop: () => void;
|
||||
}> {
|
||||
return await createTypingLease({
|
||||
defaultIntervalMs: DEFAULT_DISCORD_TYPING_INTERVAL_MS,
|
||||
errorLabel: "discord",
|
||||
intervalMs: params.intervalMs,
|
||||
pulse: params.pulse,
|
||||
pulseArgs: {
|
||||
channelId: params.channelId,
|
||||
accountId: params.accountId,
|
||||
cfg: params.cfg,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -53,6 +53,46 @@ export function resolvePluginRuntimeRecord(
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginRuntimeRecordByEntryBaseNames(
|
||||
entryBaseNames: string[],
|
||||
onMissing?: () => never,
|
||||
): PluginRuntimeRecord | null {
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
config: readPluginBoundaryConfigSafely(),
|
||||
cache: true,
|
||||
});
|
||||
const matches = manifestRegistry.plugins.filter((plugin) => {
|
||||
if (!plugin?.source) {
|
||||
return false;
|
||||
}
|
||||
const record = {
|
||||
rootDir: plugin.rootDir,
|
||||
source: plugin.source,
|
||||
};
|
||||
return entryBaseNames.every(
|
||||
(entryBaseName) => resolvePluginRuntimeModulePath(record, entryBaseName) !== null,
|
||||
);
|
||||
});
|
||||
if (matches.length === 0) {
|
||||
if (onMissing) {
|
||||
onMissing();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
const pluginIds = matches.map((plugin) => plugin.id).join(", ");
|
||||
throw new Error(
|
||||
`plugin runtime boundary is ambiguous for entries [${entryBaseNames.join(", ")}]: ${pluginIds}`,
|
||||
);
|
||||
}
|
||||
const record = matches[0];
|
||||
return {
|
||||
...(record.origin ? { origin: record.origin } : {}),
|
||||
rootDir: record.rootDir,
|
||||
source: record.source,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginRuntimeModulePath(
|
||||
record: Pick<PluginRuntimeRecord, "rootDir" | "source">,
|
||||
entryBaseName: string,
|
||||
|
||||
@@ -1,26 +1,108 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { createJiti } from "jiti";
|
||||
type WebChannelHeavyRuntimeModule = typeof import("@openclaw/whatsapp/runtime-api.js");
|
||||
type WebChannelLightRuntimeModule = typeof import("@openclaw/whatsapp/light-runtime-api.js");
|
||||
import type { ChannelAgentTool } from "../../channels/plugins/types.core.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
getDefaultLocalRoots as getDefaultLocalRootsImpl,
|
||||
loadWebMedia as loadWebMediaImpl,
|
||||
loadWebMediaRaw as loadWebMediaRawImpl,
|
||||
optimizeImageToJpeg as optimizeImageToJpegImpl,
|
||||
} from "../../media/web-media.js";
|
||||
import type { PollInput } from "../../polls.js";
|
||||
import {
|
||||
loadPluginBoundaryModuleWithJiti,
|
||||
resolvePluginRuntimeRecordByEntryBaseNames,
|
||||
resolvePluginRuntimeModulePath,
|
||||
resolvePluginRuntimeRecord,
|
||||
} from "./runtime-plugin-boundary.js";
|
||||
|
||||
const WEB_CHANNEL_PLUGIN_ID = "whatsapp";
|
||||
|
||||
type WebChannelPluginRecord = {
|
||||
origin: string;
|
||||
origin?: string;
|
||||
rootDir?: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
type WebChannelLightRuntimeModule = {
|
||||
getActiveWebListener: (accountId?: string | null) => unknown;
|
||||
getWebAuthAgeMs: (authDir?: string) => number | null;
|
||||
logWebSelfId: (authDir?: string, runtime?: unknown, includeChannelPrefix?: boolean) => void;
|
||||
logoutWeb: (params: {
|
||||
authDir?: string;
|
||||
isLegacyAuthDir?: boolean;
|
||||
runtime?: unknown;
|
||||
}) => Promise<boolean>;
|
||||
readWebSelfId: (authDir?: string) => {
|
||||
e164: string | null;
|
||||
jid: string | null;
|
||||
lid: string | null;
|
||||
};
|
||||
webAuthExists: (authDir?: string) => Promise<boolean>;
|
||||
createWhatsAppLoginTool: () => ChannelAgentTool;
|
||||
formatError: (error: unknown) => string;
|
||||
getStatusCode: (error: unknown) => number | undefined;
|
||||
pickWebChannel: (pref: string, authDir?: string) => Promise<string>;
|
||||
WA_WEB_AUTH_DIR: string;
|
||||
};
|
||||
|
||||
type WebChannelHeavyRuntimeModule = {
|
||||
loginWeb: (
|
||||
verbose: boolean,
|
||||
waitForConnection?: (sock: unknown) => Promise<void>,
|
||||
runtime?: unknown,
|
||||
accountId?: string,
|
||||
) => Promise<void>;
|
||||
sendMessageWhatsApp: (
|
||||
to: string,
|
||||
body: string,
|
||||
options: {
|
||||
verbose: boolean;
|
||||
cfg?: OpenClawConfig;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
gifPlayback?: boolean;
|
||||
accountId?: string;
|
||||
},
|
||||
) => Promise<{ messageId: string; toJid: string }>;
|
||||
sendPollWhatsApp: (
|
||||
to: string,
|
||||
poll: PollInput,
|
||||
options: { verbose: boolean; accountId?: string; cfg?: OpenClawConfig },
|
||||
) => Promise<{ messageId: string; toJid: string }>;
|
||||
sendReactionWhatsApp: (
|
||||
chatJid: string,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
options: {
|
||||
verbose: boolean;
|
||||
fromMe?: boolean;
|
||||
participant?: string;
|
||||
accountId?: string;
|
||||
},
|
||||
) => Promise<void>;
|
||||
createWaSocket: (
|
||||
printQr: boolean,
|
||||
verbose: boolean,
|
||||
opts?: { authDir?: string; onQr?: (qr: string) => void },
|
||||
) => Promise<unknown>;
|
||||
handleWhatsAppAction: (
|
||||
params: Record<string, unknown>,
|
||||
cfg: OpenClawConfig,
|
||||
) => Promise<AgentToolResult<unknown>>;
|
||||
monitorWebChannel: (...args: unknown[]) => Promise<unknown>;
|
||||
monitorWebInbox: (...args: unknown[]) => Promise<unknown>;
|
||||
runWebHeartbeatOnce: (...args: unknown[]) => Promise<unknown>;
|
||||
startWebLoginWithQr: (...args: unknown[]) => Promise<unknown>;
|
||||
waitForWaConnection: (sock: unknown) => Promise<void>;
|
||||
waitForWebLogin: (...args: unknown[]) => Promise<unknown>;
|
||||
extractMediaPlaceholder: (...args: unknown[]) => unknown;
|
||||
extractText: (...args: unknown[]) => unknown;
|
||||
resolveHeartbeatRecipients: (...args: unknown[]) => unknown;
|
||||
};
|
||||
|
||||
let cachedHeavyModulePath: string | null = null;
|
||||
let cachedHeavyModule: WebChannelHeavyRuntimeModule | null = null;
|
||||
let cachedLightModulePath: string | null = null;
|
||||
@@ -29,9 +111,9 @@ let cachedLightModule: WebChannelLightRuntimeModule | null = null;
|
||||
const jitiLoaders = new Map<boolean, ReturnType<typeof createJiti>>();
|
||||
|
||||
function resolveWebChannelPluginRecord(): WebChannelPluginRecord {
|
||||
return resolvePluginRuntimeRecord(WEB_CHANNEL_PLUGIN_ID, () => {
|
||||
return resolvePluginRuntimeRecordByEntryBaseNames(["light-runtime-api", "runtime-api"], () => {
|
||||
throw new Error(
|
||||
`web channel plugin runtime is unavailable: missing plugin '${WEB_CHANNEL_PLUGIN_ID}'`,
|
||||
"web channel plugin runtime is unavailable: missing plugin that provides light-runtime-api and runtime-api",
|
||||
);
|
||||
}) as WebChannelPluginRecord;
|
||||
}
|
||||
@@ -41,14 +123,10 @@ function resolveWebChannelRuntimeModulePath(
|
||||
entryBaseName: "light-runtime-api" | "runtime-api",
|
||||
): string {
|
||||
const modulePath = resolvePluginRuntimeModulePath(record, entryBaseName, () => {
|
||||
throw new Error(
|
||||
`web channel plugin runtime is unavailable: missing ${entryBaseName} for plugin '${WEB_CHANNEL_PLUGIN_ID}'`,
|
||||
);
|
||||
throw new Error(`web channel plugin runtime is unavailable: missing ${entryBaseName}`);
|
||||
});
|
||||
if (!modulePath) {
|
||||
throw new Error(
|
||||
`web channel plugin runtime is unavailable: missing ${entryBaseName} for plugin '${WEB_CHANNEL_PLUGIN_ID}'`,
|
||||
);
|
||||
throw new Error(`web channel plugin runtime is unavailable: missing ${entryBaseName}`);
|
||||
}
|
||||
return modulePath;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user