diff --git a/electron/api/routes/channels.ts b/electron/api/routes/channels.ts
index aaded55..29affe2 100644
--- a/electron/api/routes/channels.ts
+++ b/electron/api/routes/channels.ts
@@ -57,6 +57,47 @@ async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; wa
}
}
+async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
+ const targetDir = join(homedir(), '.openclaw', 'extensions', 'wecom');
+ const targetManifest = join(targetDir, 'openclaw.plugin.json');
+
+ if (existsSync(targetManifest)) {
+ return { installed: true };
+ }
+
+ const candidateSources = app.isPackaged
+ ? [
+ join(process.resourcesPath, 'openclaw-plugins', 'wecom'),
+ join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'),
+ join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom'),
+ ]
+ : [
+ join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'),
+ join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'),
+ join(__dirname, '../../../build/openclaw-plugins/wecom'),
+ ];
+
+ const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
+ if (!sourceDir) {
+ return {
+ installed: false,
+ warning: `Bundled WeCom plugin mirror not found. Checked: ${candidateSources.join(' | ')}`,
+ };
+ }
+
+ try {
+ mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
+ rmSync(targetDir, { recursive: true, force: true });
+ cpSync(sourceDir, targetDir, { recursive: true, dereference: true });
+ if (!existsSync(targetManifest)) {
+ return { installed: false, warning: 'Failed to install WeCom plugin mirror (manifest missing).' };
+ }
+ return { installed: true };
+ } catch {
+ return { installed: false, warning: 'Failed to install bundled WeCom plugin mirror' };
+ }
+}
+
export async function handleChannelRoutes(
req: IncomingMessage,
res: ServerResponse,
@@ -119,6 +160,13 @@ export async function handleChannelRoutes(
return true;
}
}
+ if (body.channelType === 'wecom') {
+ const installResult = await ensureWeComPluginInstalled();
+ if (!installResult.installed) {
+ sendJson(res, 500, { success: false, error: installResult.warning || 'WeCom plugin install failed' });
+ return true;
+ }
+ }
await saveChannelConfig(body.channelType, body.config);
sendJson(res, 200, { success: true });
} catch (error) {
diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts
index e5db2bc..58912c9 100644
--- a/electron/main/ipc-handlers.ts
+++ b/electron/main/ipc-handlers.ts
@@ -1373,6 +1373,56 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
}
}
+ async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
+ const targetDir = join(homedir(), '.openclaw', 'extensions', 'wecom');
+ const targetManifest = join(targetDir, 'openclaw.plugin.json');
+
+ if (existsSync(targetManifest)) {
+ logger.info('WeCom plugin already installed from local mirror');
+ return { installed: true };
+ }
+
+ const candidateSources = app.isPackaged
+ ? [
+ join(process.resourcesPath, 'openclaw-plugins', 'wecom'),
+ join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'),
+ join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom')
+ ]
+ : [
+ join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'),
+ join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'),
+ join(__dirname, '../../build/openclaw-plugins/wecom'),
+ ];
+
+ const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
+ if (!sourceDir) {
+ logger.warn('Bundled WeCom plugin mirror not found in candidate paths', { candidateSources });
+ return {
+ installed: false,
+ warning: `Bundled WeCom plugin mirror not found. Checked: ${candidateSources.join(' | ')}`,
+ };
+ }
+
+ try {
+ mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
+ rmSync(targetDir, { recursive: true, force: true });
+ cpSync(sourceDir, targetDir, { recursive: true, dereference: true });
+
+ if (!existsSync(targetManifest)) {
+ return { installed: false, warning: 'Failed to install WeCom plugin mirror (manifest missing).' };
+ }
+
+ logger.info(`Installed WeCom plugin from bundled mirror: ${sourceDir}`);
+ return { installed: true };
+ } catch (error) {
+ logger.warn('Failed to install WeCom plugin from bundled mirror:', error);
+ return {
+ installed: false,
+ warning: 'Failed to install bundled WeCom plugin mirror',
+ };
+ }
+ }
+
// Get OpenClaw package status
ipcMain.handle('openclaw:status', () => {
const status = getOpenClawStatus();
@@ -1447,6 +1497,27 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
warning: installResult.warning,
};
}
+ if (channelType === 'wecom') {
+ const installResult = await ensureWeComPluginInstalled();
+ if (!installResult.installed) {
+ return {
+ success: false,
+ error: installResult.warning || 'WeCom plugin install failed',
+ };
+ }
+ await saveChannelConfig(channelType, config);
+ if (gatewayManager.getStatus().state !== 'stopped') {
+ logger.info(`Scheduling Gateway reload after channel:saveConfig (${channelType})`);
+ gatewayManager.debouncedReload();
+ } else {
+ logger.info(`Gateway is stopped; skip immediate reload after channel:saveConfig (${channelType})`);
+ }
+ return {
+ success: true,
+ pluginInstalled: installResult.installed,
+ warning: installResult.warning,
+ };
+ }
await saveChannelConfig(channelType, config);
if (gatewayManager.getStatus().state !== 'stopped') {
logger.info(`Scheduling Gateway reload after channel:saveConfig (${channelType})`);
diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts
index b1678e8..f8a3f28 100644
--- a/electron/utils/channel-config.ts
+++ b/electron/utils/channel-config.ts
@@ -14,6 +14,7 @@ import { proxyAwareFetch } from './proxy-fetch';
const OPENCLAW_DIR = join(homedir(), '.openclaw');
const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');
+const WECOM_PLUGIN_ID = 'wecom-openclaw-plugin';
// Channels that are managed as plugins (config goes under plugins.entries, not channels)
const PLUGIN_CHANNELS = ['whatsapp'];
@@ -33,6 +34,8 @@ export interface ChannelConfigData {
export interface PluginsConfig {
entries?: Record;
+ allow?: string[];
+ enabled?: boolean;
[key: string]: unknown;
}
@@ -99,15 +102,35 @@ export async function saveChannelConfig(
// DingTalk is a channel plugin; make sure it's explicitly allowed.
// Newer OpenClaw versions may not load non-bundled plugins when allowlist is empty.
if (channelType === 'dingtalk') {
+ const defaultDingtalkAllow = ['dingtalk'];
if (!currentConfig.plugins) {
- currentConfig.plugins = {};
+ currentConfig.plugins = { allow: defaultDingtalkAllow, enabled: true };
+ } else {
+ currentConfig.plugins.enabled = true;
+ const allow: string[] = Array.isArray(currentConfig.plugins.allow)
+ ? (currentConfig.plugins.allow as string[])
+ : [];
+ if (!allow.includes('dingtalk')) {
+ currentConfig.plugins.allow = [...allow, 'dingtalk'];
+ }
}
- currentConfig.plugins.enabled = true;
- const allow = Array.isArray(currentConfig.plugins.allow)
- ? currentConfig.plugins.allow as string[]
- : [];
- if (!allow.includes('dingtalk')) {
- currentConfig.plugins.allow = [...allow, 'dingtalk'];
+ }
+
+ if (channelType === 'wecom') {
+ const defaultWecomAllow = [WECOM_PLUGIN_ID];
+ if (!currentConfig.plugins) {
+ currentConfig.plugins = { allow: defaultWecomAllow, enabled: true };
+ } else {
+ currentConfig.plugins.enabled = true;
+ const allow: string[] = Array.isArray(currentConfig.plugins.allow)
+ ? (currentConfig.plugins.allow as string[])
+ : [];
+ const normalizedAllow = allow.filter((pluginId) => pluginId !== 'wecom');
+ if (!normalizedAllow.includes(WECOM_PLUGIN_ID)) {
+ currentConfig.plugins.allow = [...normalizedAllow, WECOM_PLUGIN_ID];
+ } else if (normalizedAllow.length !== allow.length) {
+ currentConfig.plugins.allow = normalizedAllow;
+ }
}
}
@@ -192,10 +215,11 @@ export async function saveChannelConfig(
}
}
- // Special handling for Feishu: default to open DM policy with wildcard allowlist
- if (channelType === 'feishu') {
+ // Special handling for Feishu / WeCom: default to open DM policy with wildcard allowlist
+ if (channelType === 'feishu' || channelType === 'wecom') {
const existingConfig = currentConfig.channels[channelType] || {};
- transformedConfig.dmPolicy = transformedConfig.dmPolicy ?? existingConfig.dmPolicy ?? 'open';
+ const existingDmPolicy = existingConfig.dmPolicy === 'pairing' ? 'open' : existingConfig.dmPolicy;
+ transformedConfig.dmPolicy = transformedConfig.dmPolicy ?? existingDmPolicy ?? 'open';
let allowFrom = transformedConfig.allowFrom ?? existingConfig.allowFrom ?? ['*'];
if (!Array.isArray(allowFrom)) {
diff --git a/package.json b/package.json
index b2fd0d9..93e627d 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"postversion": "git push && git push --tags"
},
"dependencies": {
+ "@wecom/wecom-openclaw-plugin": "^1.0.6",
"clawhub": "^0.5.0",
"electron-store": "^11.0.2",
"electron-updater": "^6.8.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dc3b1d8..c28c82b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@wecom/wecom-openclaw-plugin':
+ specifier: ^1.0.6
+ version: 1.0.6
clawhub:
specifier: ^0.5.0
version: 0.5.0
@@ -2836,6 +2839,12 @@ packages:
'@vitest/utils@4.0.18':
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
+ '@wecom/aibot-node-sdk@1.0.1':
+ resolution: {integrity: sha512-c/sa1IvRKIP+4rZfRV2v70FaXB92+BJIh+vedZkPa8wZ1dwIUyvGg7ydkfYRIwFDzjO9IJZUX5V14EUQYVopAg==}
+
+ '@wecom/wecom-openclaw-plugin@1.0.6':
+ resolution: {integrity: sha512-1yn6P3KGdEfKoTuGH0Ot4vuoHOFqZJ+qlVrEXYBzkPwtNHb7s2ja2YKizaffYWb0h2s464PEXKhmkQq/RRJwkg==}
+
'@whiskeysockets/baileys@7.0.0-rc.9':
resolution: {integrity: sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw==}
engines: {node: '>=20.0.0'}
@@ -9804,6 +9813,26 @@ snapshots:
'@vitest/pretty-format': 4.0.18
tinyrainbow: 3.0.3
+ '@wecom/aibot-node-sdk@1.0.1':
+ dependencies:
+ axios: 1.13.5(debug@4.4.3)
+ eventemitter3: 5.0.4
+ ws: 8.19.0
+ transitivePeerDependencies:
+ - bufferutil
+ - debug
+ - utf-8-validate
+
+ '@wecom/wecom-openclaw-plugin@1.0.6':
+ dependencies:
+ '@wecom/aibot-node-sdk': 1.0.1
+ file-type: 21.3.0
+ transitivePeerDependencies:
+ - bufferutil
+ - debug
+ - supports-color
+ - utf-8-validate
+
'@whiskeysockets/baileys@7.0.0-rc.9(sharp@0.34.5)':
dependencies:
'@cacheable/node-cache': 1.7.6
diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs
index acdbe71..9014fc2 100644
--- a/scripts/after-pack.cjs
+++ b/scripts/after-pack.cjs
@@ -338,6 +338,7 @@ exports.default = async function afterPack(context) {
// - node_modules/ is excluded by .gitignore so the deps copy must be manual
const BUNDLED_PLUGINS = [
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
+ { npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' }
];
mkdirSync(pluginsDestRoot, { recursive: true });
diff --git a/scripts/bundle-openclaw-plugins.mjs b/scripts/bundle-openclaw-plugins.mjs
index cecb658..b317744 100644
--- a/scripts/bundle-openclaw-plugins.mjs
+++ b/scripts/bundle-openclaw-plugins.mjs
@@ -6,6 +6,7 @@
* Build a self-contained mirror of OpenClaw third-party plugins for packaging.
* Current plugins:
* - @soimy/dingtalk -> build/openclaw-plugins/dingtalk
+ * - @wecom/wecom-openclaw-plugin -> build/openclaw-plugins/wecom
*
* The output plugin directory contains:
* - plugin source files (index.ts, openclaw.plugin.json, package.json, ...)
@@ -15,7 +16,9 @@
import 'zx/globals';
import fs from 'node:fs';
import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins');
const NODE_MODULES = path.join(ROOT, 'node_modules');
@@ -33,6 +36,7 @@ function normWin(p) {
const PLUGINS = [
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
+ { npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' }
];
function getVirtualStoreNodeModules(realPkgPath) {
diff --git a/src/i18n/locales/en/channels.json b/src/i18n/locales/en/channels.json
index 932660d..a9bfd99 100644
--- a/src/i18n/locales/en/channels.json
+++ b/src/i18n/locales/en/channels.json
@@ -185,6 +185,25 @@
"Note: Not mentioned in current OpenClaw docs, but you MUST add 'contact:contact.base:readonly' **Application Permission** in Permission Management"
]
},
+ "wecom": {
+ "description": "Connect WeCom Bot via plugin",
+ "docsUrl": "https://developer.work.weixin.qq.com/document/path/90000/90136/91770",
+ "fields": {
+ "botId": {
+ "label": "Bot ID",
+ "placeholder": "ww_xxxxxx"
+ },
+ "secret": {
+ "label": "App Secret",
+ "placeholder": "Your WeCom Bot secret"
+ }
+ },
+ "instructions": [
+ "Create an application in WeCom Admin Console to get configuration info",
+ "Ensure receive message server config is enabled",
+ "Enter your Bot ID (or Corp ID) and Secret to establish connection"
+ ]
+ },
"imessage": {
"description": "Connect iMessage via BlueBubbles (macOS)",
"docsUrl": "https://docs.openclaw.ai/channels/bluebubbles",
@@ -299,4 +318,4 @@
}
},
"viewDocs": "View Documentation"
-}
+}
\ No newline at end of file
diff --git a/src/i18n/locales/ja/channels.json b/src/i18n/locales/ja/channels.json
index 3fa3ef7..314d28c 100644
--- a/src/i18n/locales/ja/channels.json
+++ b/src/i18n/locales/ja/channels.json
@@ -185,6 +185,25 @@
"注意:現在のドキュメントには記載されていませんが、権限管理で 'contact:contact.base:readonly' **アプリケーション権限** を必ず追加してください"
]
},
+ "wecom": {
+ "description": "プラグイン経由で WeCom Bot (企業微信) に接続します",
+ "docsUrl": "https://developer.work.weixin.qq.com/document/path/90000/90136/91770",
+ "fields": {
+ "botId": {
+ "label": "ボット ID",
+ "placeholder": "ww_xxxxxx"
+ },
+ "secret": {
+ "label": "アプリシークレット",
+ "placeholder": "WeCom Bot のシークレット"
+ }
+ },
+ "instructions": [
+ "WeCom 管理コンソールでアプリケーションを作成し、設定情報を取得します",
+ "メッセージ受信サーバー設定が有効になっていることを確認します",
+ "ボット ID (または 企業 ID) とシークレットを入力して接続を確立します"
+ ]
+ },
"imessage": {
"description": "BlueBubbles (macOS) 経由で iMessage に接続します",
"fields": {
@@ -299,4 +318,4 @@
}
},
"viewDocs": "ドキュメントを表示"
-}
+}
\ No newline at end of file
diff --git a/src/i18n/locales/zh/channels.json b/src/i18n/locales/zh/channels.json
index e0a68e9..1139738 100644
--- a/src/i18n/locales/zh/channels.json
+++ b/src/i18n/locales/zh/channels.json
@@ -185,6 +185,25 @@
"注意:当前OpenClaw文档中未提及,但请务必在权限管理中添加 contact:contact.base:readonly **应用权限**,否则无法正常使用"
]
},
+ "wecom": {
+ "description": "通过插件连接企业微信机器人",
+ "docsUrl": "https://developer.work.weixin.qq.com/document/path/90000/90136/91770",
+ "fields": {
+ "botId": {
+ "label": "机器人 Bot ID",
+ "placeholder": "ww_xxxxxx"
+ },
+ "secret": {
+ "label": "应用 Secret",
+ "placeholder": "您的企业微信机器人 Secret"
+ }
+ },
+ "instructions": [
+ "您可以在企业微信管理后台创建应用并获取配置信息",
+ "确保已启用接收消息服务器配置",
+ "填写 Bot ID(可选企业 ID 或者直接使用机器人专属 ID)及 Secret 即可建立连接"
+ ]
+ },
"imessage": {
"description": "通过 BlueBubbles (macOS) 连接 iMessage",
"docsUrl": "https://docs.openclaw.ai/zh-CN/channels/bluebubbles",
@@ -299,4 +318,4 @@
}
},
"viewDocs": "查看文档"
-}
+}
\ No newline at end of file
diff --git a/src/pages/Channels/index.tsx b/src/pages/Channels/index.tsx
index 63463a3..ec4add7 100644
--- a/src/pages/Channels/index.tsx
+++ b/src/pages/Channels/index.tsx
@@ -121,7 +121,13 @@ export function Channels() {
-