// ADP OpenClaw channel plugin for OpenClaw
// Supports: API Token auth, multiple clients, multi-turn conversations
import {
type ChannelPlugin,
type ClawdbotConfig,
DEFAULT_ACCOUNT_ID,
} from "openclaw/plugin-sdk";
import { adpOpenclawOnboardingAdapter } from "./onboarding.js";
import { getActiveWebSocket } from "./runtime.js";
// Default WebSocket URL for ADP OpenClaw
const DEFAULT_WS_URL = "wss://wss.lke.cloud.tencent.com/bot/gateway/conn";
// Channel-level config type (from channels["adp-openclaw"])
export type AdpOpenclawChannelConfig = {
enabled?: boolean;
wsUrl?: string; // WebSocket URL (optional, has default)
clientToken?: string;
signKey?: string; // HMAC key for signature generation
};
export type ResolvedAdpOpenclawAccount = {
accountId: string;
name: string;
enabled: boolean;
configured: boolean;
wsUrl: string; // WebSocket URL
clientToken: string;
signKey: string; // HMAC key for signature generation
};
function resolveAdpOpenclawCredentials(channelCfg?: AdpOpenclawChannelConfig): {
wsUrl: string;
clientToken: string;
signKey: string;
} | null {
// Get wsUrl from config or env (has default value)
let wsUrl = channelCfg?.wsUrl?.trim();
if (!wsUrl) {
wsUrl = process.env.ADP_OPENCLAW_WS_URL || DEFAULT_WS_URL;
}
// Get clientToken from config or env
let clientToken = channelCfg?.clientToken?.trim();
if (!clientToken) {
clientToken = process.env.ADP_OPENCLAW_CLIENT_TOKEN || "";
}
// Get signKey from config or env (default: ADPOpenClaw)
let signKey = channelCfg?.signKey?.trim();
if (!signKey) {
signKey = process.env.ADP_OPENCLAW_SIGN_KEY || "ADPOpenClaw";
}
// clientToken is required for configured status (wsUrl has default)
if (!clientToken) {
return null;
}
return { wsUrl, clientToken, signKey };
}
function resolveAccount(cfg: ClawdbotConfig, accountId?: string): ResolvedAdpOpenclawAccount {
const channelCfg = cfg.channels?.["adp-openclaw"] as AdpOpenclawChannelConfig | undefined;
const enabled = channelCfg?.enabled !== false;
const creds = resolveAdpOpenclawCredentials(channelCfg);
return {
accountId: accountId?.trim() || DEFAULT_ACCOUNT_ID,
name: "ADP OpenClaw",
enabled,
configured: Boolean(creds),
wsUrl: creds?.wsUrl || DEFAULT_WS_URL,
clientToken: creds?.clientToken || "",
signKey: creds?.signKey || "ADPOpenClaw",
};
}
export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
id: "adp-openclaw",
meta: {
id: "adp-openclaw",
label: "ADP OpenClaw",
selectionLabel: "ADP OpenClaw",
docsPath: "/channels/adp-openclaw",
blurb: "ADP channel backed by a Go WebSocket server.",
order: 999,
},
onboarding: adpOpenclawOnboardingAdapter,
capabilities: {
chatTypes: ["direct"],
polls: false,
reactions: false,
threads: false,
media: true, // 启用文件/媒体支持
/**
* blockStreaming: true 启用 SDK 的块流式功能
* SDK 会通过 deliver 回调的 info.kind="block" 传递流式块
*/
blockStreaming: true,
},
reload: { configPrefixes: ["channels.adp-openclaw"] },
configSchema: {
schema: {
type: "object",
additionalProperties: false,
properties: {
enabled: { type: "boolean" },
wsUrl: { type: "string" }, // WebSocket URL (optional, default: wss://wss.lke.cloud.tencent.com/bot/gateway/conn)
clientToken: { type: "string" },
signKey: { type: "string" },
},
},
},
config: {
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
resolveAccount: (cfg) => resolveAccount(cfg),
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
setAccountEnabled: ({ cfg, enabled }) => ({
...cfg,
channels: {
...cfg.channels,
"adp-openclaw": {
...cfg.channels?.["adp-openclaw"],
enabled,
},
},
}),
deleteAccount: ({ cfg }) => {
const next = { ...cfg } as ClawdbotConfig;
const nextChannels = { ...cfg.channels };
delete (nextChannels as Record<string, unknown>)["adp-openclaw"];
if (Object.keys(nextChannels).length > 0) {
next.channels = nextChannels;
} else {
delete next.channels;
}
return next;
},
isConfigured: (_account, cfg) =>
Boolean(resolveAdpOpenclawCredentials(cfg.channels?.["adp-openclaw"] as AdpOpenclawChannelConfig | undefined)),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
wsUrl: account.wsUrl,
}),
resolveAllowFrom: () => [],
formatAllowFrom: ({ allowFrom }) => allowFrom,
},
setup: {
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
applyAccountConfig: ({ cfg }) => ({
...cfg,
channels: {
...cfg.channels,
"adp-openclaw": {
...cfg.channels?.["adp-openclaw"],
enabled: true,
},
},
}),
},
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
collectStatusIssues: () => [],
buildChannelSummary: ({ snapshot }) => ({
configured: snapshot.configured ?? false,
wsUrl: snapshot.wsUrl ?? null,
running: snapshot.running ?? false,
lastStartAt: snapshot.lastStartAt ?? null,
lastStopAt: snapshot.lastStopAt ?? null,
lastError: snapshot.lastError ?? null,
}),
probeAccount: async ({ cfg }) => {
// For WebSocket-only architecture, we just check if config is valid
const account = resolveAccount(cfg);
const start = Date.now();
return {
ok: account.configured && Boolean(account.clientToken),
elapsedMs: Date.now() - start,
};
},
buildAccountSnapshot: ({ account, runtime, probe }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
wsUrl: account.wsUrl,
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
probe,
}),
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
ctx.setStatus({ accountId: account.accountId, wsUrl: account.wsUrl });
ctx.log?.info(`[adp-openclaw] starting WebSocket connection → ${account.wsUrl}`);
const { monitorAdpOpenclaw } = await import("./monitor.js");
return monitorAdpOpenclaw({
wsUrl: account.wsUrl,
clientToken: account.clientToken,
signKey: account.signKey,
abortSignal: ctx.abortSignal,
log: ctx.log,
cfg: ctx.cfg,
});
},
},
// Outbound message support for the "message" tool
outbound: {
send: async ({ text, to, log }) => {
const ws = getActiveWebSocket();
if (!ws) {
log?.error?.("[adp-openclaw] No active WebSocket connection for outbound message");
return { ok: false, error: "No active WebSocket connection" };
}
// Parse target: expected format is "adp-openclaw:{userId}" or "adp-openclaw:bot"
// The "to" parameter comes from the message tool with format like "adp-openclaw:user123"
const targetParts = to.split(":");
const targetUserId = targetParts.length > 1 ? targetParts.slice(1).join(":") : to;
log?.info?.(`[adp-openclaw] Sending outbound message to ${targetUserId}: ${text.slice(0, 50)}...`);
try {
// Generate unique request ID
const requestId = `outbound-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const outMsg = {
type: "outbound",
requestId,
payload: {
to: targetUserId,
text: text,
},
timestamp: Date.now(),
};
ws.send(JSON.stringify(outMsg));
return { ok: true };
} catch (err) {
log?.error?.(`[adp-openclaw] Failed to send outbound message: ${err}`);
return { ok: false, error: String(err) };
}
},
},
};