import type {
ChannelAccountSnapshot,
ChannelPlugin,
OpenClawConfig,
} from "openclaw/plugin-sdk";
import {
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
setAccountEnabledInConfigSection,
} from "openclaw/plugin-sdk";
import { resolveWecomAccounts } from "./config/index.js";
import { WecomConfigSchema } from "./config/index.js";
import type { ResolvedAgentAccount, ResolvedBotAccount } from "./types/index.js";
import { registerAgentWebhookTarget, registerWecomWebhookTarget } from "./monitor.js";
import { wecomOnboardingAdapter } from "./onboarding.js";
import { wecomOutbound } from "./outbound.js";
const meta = {
id: "wecom",
label: "WeCom",
selectionLabel: "WeCom (plugin)",
docsPath: "/channels/wecom",
docsLabel: "wecom",
blurb: "Enterprise WeCom intelligent bot (API mode) via encrypted webhooks + passive replies.",
aliases: ["wechatwork", "wework", "qywx", "企微", "企业微信"],
order: 85,
quickstartAllowFrom: true,
};
function normalizeWecomMessagingTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
return trimmed.replace(/^(wecom-agent|wecom|wechatwork|wework|qywx):/i, "").trim() || undefined;
}
type ResolvedWecomAccount = {
accountId: string;
name?: string;
enabled: boolean;
configured: boolean;
bot?: ResolvedBotAccount;
agent?: ResolvedAgentAccount;
};
/**
* **resolveWecomAccount (解析账号配置)**
*
* 从全局配置中解析出 WeCom 渠道的配置状态。
* 兼容 Bot 和 Agent 两种模式的配置检查。
*/
function resolveWecomAccount(cfg: OpenClawConfig): ResolvedWecomAccount {
const enabled = (cfg.channels?.wecom as { enabled?: boolean } | undefined)?.enabled !== false;
const accounts = resolveWecomAccounts(cfg);
const bot = accounts.bot;
const agent = accounts.agent;
const configured = Boolean(bot?.configured || agent?.configured);
return {
accountId: DEFAULT_ACCOUNT_ID,
enabled,
configured,
bot,
agent,
};
}
export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
id: "wecom",
meta,
onboarding: wecomOnboardingAdapter,
capabilities: {
chatTypes: ["direct", "group"],
media: true,
reactions: false,
threads: false,
polls: false,
nativeCommands: false,
blockStreaming: true,
},
reload: { configPrefixes: ["channels.wecom"] },
configSchema: buildChannelConfigSchema(WecomConfigSchema),
config: {
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
resolveAccount: (cfg) => resolveWecomAccount(cfg as OpenClawConfig),
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg: cfg as OpenClawConfig,
sectionKey: "wecom",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg }) => {
const next = { ...(cfg as OpenClawConfig) };
if (next.channels?.wecom) {
const channels = { ...(next.channels ?? {}) } as Record<string, unknown>;
delete (channels as Record<string, unknown>).wecom;
return { ...next, channels } as OpenClawConfig;
}
return next;
},
isConfigured: (account) => account.configured,
describeAccount: (account): ChannelAccountSnapshot => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
webhookPath: account.bot?.config ? "/wecom/bot" : account.agent?.config ? "/wecom/agent" : "/wecom",
}),
resolveAllowFrom: ({ cfg, accountId }) => {
const account = resolveWecomAccount(cfg as OpenClawConfig);
// 与其他渠道保持一致:直接返回 allowFrom,空则允许所有人
const allowFrom = account.agent?.config.dm?.allowFrom ?? account.bot?.config.dm?.allowFrom ?? [];
return allowFrom.map((entry) => String(entry));
},
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => entry.toLowerCase()),
},
// security 配置在 WeCom 中不需要,框架会通过 resolveAllowFrom 自动判断
groups: {
// WeCom bots are usually mention-gated by the platform in groups already.
resolveRequireMention: () => true,
},
threading: {
resolveReplyToMode: () => "off",
},
messaging: {
normalizeTarget: normalizeWecomMessagingTarget,
targetResolver: {
looksLikeId: (raw) => Boolean(raw.trim()),
hint: "<userid|chatid>",
},
},
outbound: {
...wecomOutbound,
},
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
buildChannelSummary: ({ snapshot }) => ({
configured: snapshot.configured ?? false,
running: snapshot.running ?? false,
webhookPath: snapshot.webhookPath ?? null,
lastStartAt: snapshot.lastStartAt ?? null,
lastStopAt: snapshot.lastStopAt ?? null,
lastError: snapshot.lastError ?? null,
lastInboundAt: snapshot.lastInboundAt ?? null,
lastOutboundAt: snapshot.lastOutboundAt ?? null,
probe: snapshot.probe,
lastProbeAt: snapshot.lastProbeAt ?? null,
}),
probeAccount: async () => ({ ok: true }),
buildAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
webhookPath: account.bot?.config ? "/wecom/bot" : account.agent?.config ? "/wecom/agent" : "/wecom",
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
lastInboundAt: runtime?.lastInboundAt ?? null,
lastOutboundAt: runtime?.lastOutboundAt ?? null,
dmPolicy: account.bot?.config.dm?.policy ?? "pairing",
}),
},
gateway: {
/**
* **startAccount (启动账号)**
*
* 插件生命周期:启动
* 职责:
* 1. 检查配置是否有效。
* 2. 注册 Bot Webhook (`/wecom`, `/wecom/bot`)。
* 3. 注册 Agent Webhook (`/wecom/agent`)。
* 4. 更新运行时状态 (Running)。
* 5. 返回停止回调 (Cleanup)。
*/
startAccount: async (ctx) => {
const account = ctx.account;
const bot = account.bot;
const agent = account.agent;
const botConfigured = Boolean(bot?.configured);
const agentConfigured = Boolean(agent?.configured);
if (!botConfigured && !agentConfigured) {
ctx.log?.warn(`[${account.accountId}] wecom not configured; skipping webhook registration`);
ctx.setStatus({ accountId: account.accountId, running: false, configured: false });
return { stop: () => { } };
}
const unregisters: Array<() => void> = [];
if (bot && botConfigured) {
for (const path of ["/wecom", "/wecom/bot"]) {
unregisters.push(
registerWecomWebhookTarget({
account: bot,
config: ctx.cfg as OpenClawConfig,
runtime: ctx.runtime,
// The HTTP handler resolves the active PluginRuntime via getWecomRuntime().
// The stored target only needs to be decrypt/verify-capable.
core: ({} as unknown) as any,
path,
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
}),
);
}
ctx.log?.info(`[${account.accountId}] wecom bot webhook registered at /wecom and /wecom/bot`);
}
if (agent && agentConfigured) {
unregisters.push(
registerAgentWebhookTarget({
agent,
config: ctx.cfg as OpenClawConfig,
runtime: ctx.runtime,
}),
);
ctx.log?.info(`[${account.accountId}] wecom agent webhook registered at /wecom/agent`);
}
ctx.setStatus({
accountId: account.accountId,
running: true,
configured: true,
webhookPath: botConfigured ? "/wecom/bot" : "/wecom/agent",
lastStartAt: Date.now(),
});
return {
stop: () => {
for (const unregister of unregisters) {
unregister();
}
ctx.setStatus({
accountId: account.accountId,
running: false,
lastStopAt: Date.now(),
});
},
};
},
stopAccount: async (ctx) => {
ctx.setStatus({
accountId: ctx.account.accountId,
running: false,
lastStopAt: Date.now(),
});
},
},
};