feat: support wecom (#372)
Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
905ce02b0b
commit
b86f47171b
@@ -16,6 +16,14 @@ import { whatsAppLoginManager } from '../../utils/whatsapp-login';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
|
||||
function scheduleGatewayChannelRestart(ctx: HostApiContext, reason: string): void {
|
||||
if (ctx.gatewayManager.getStatus().state === 'stopped') {
|
||||
return;
|
||||
}
|
||||
ctx.gatewayManager.debouncedRestart();
|
||||
void reason;
|
||||
}
|
||||
|
||||
async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
const targetDir = join(homedir(), '.openclaw', 'extensions', 'dingtalk');
|
||||
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
||||
@@ -57,6 +65,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,7 +168,15 @@ 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);
|
||||
scheduleGatewayChannelRestart(ctx, `channel:saveConfig:${body.channelType}`);
|
||||
sendJson(res, 200, { success: true });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
@@ -131,6 +188,7 @@ export async function handleChannelRoutes(
|
||||
try {
|
||||
const body = await parseJsonBody<{ channelType: string; enabled: boolean }>(req);
|
||||
await setChannelEnabled(body.channelType, body.enabled);
|
||||
scheduleGatewayChannelRestart(ctx, `channel:setEnabled:${body.channelType}`);
|
||||
sendJson(res, 200, { success: true });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
@@ -155,6 +213,7 @@ export async function handleChannelRoutes(
|
||||
try {
|
||||
const channelType = decodeURIComponent(url.pathname.slice('/api/channels/config/'.length));
|
||||
await deleteChannelConfig(channelType);
|
||||
scheduleGatewayChannelRestart(ctx, `channel:deleteConfig:${channelType}`);
|
||||
sendJson(res, 200, { success: true });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
|
||||
@@ -1323,6 +1323,15 @@ function registerGatewayHandlers(
|
||||
* For checking package status and channel configuration
|
||||
*/
|
||||
function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
const scheduleGatewayChannelRestart = (reason: string): void => {
|
||||
if (gatewayManager.getStatus().state !== 'stopped') {
|
||||
logger.info(`Scheduling Gateway restart after ${reason}`);
|
||||
gatewayManager.debouncedRestart();
|
||||
} else {
|
||||
logger.info(`Gateway is stopped; skip immediate restart after ${reason}`);
|
||||
}
|
||||
};
|
||||
|
||||
async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
const targetDir = join(homedir(), '.openclaw', 'extensions', 'dingtalk');
|
||||
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
||||
@@ -1373,6 +1382,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();
|
||||
@@ -1435,12 +1494,23 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
};
|
||||
}
|
||||
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})`);
|
||||
scheduleGatewayChannelRestart(`channel:saveConfig (${channelType})`);
|
||||
return {
|
||||
success: true,
|
||||
pluginInstalled: installResult.installed,
|
||||
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);
|
||||
scheduleGatewayChannelRestart(`channel:saveConfig (${channelType})`);
|
||||
return {
|
||||
success: true,
|
||||
pluginInstalled: installResult.installed,
|
||||
@@ -1448,12 +1518,7 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
};
|
||||
}
|
||||
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})`);
|
||||
}
|
||||
scheduleGatewayChannelRestart(`channel:saveConfig (${channelType})`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to save channel config:', error);
|
||||
@@ -1487,12 +1552,7 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
ipcMain.handle('channel:deleteConfig', async (_, channelType: string) => {
|
||||
try {
|
||||
await deleteChannelConfig(channelType);
|
||||
if (gatewayManager.getStatus().state !== 'stopped') {
|
||||
logger.info(`Scheduling Gateway reload after channel:deleteConfig (${channelType})`);
|
||||
gatewayManager.debouncedReload();
|
||||
} else {
|
||||
logger.info(`Gateway is stopped; skip immediate reload after channel:deleteConfig (${channelType})`);
|
||||
}
|
||||
scheduleGatewayChannelRestart(`channel:deleteConfig (${channelType})`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to delete channel config:', error);
|
||||
@@ -1515,12 +1575,7 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
ipcMain.handle('channel:setEnabled', async (_, channelType: string, enabled: boolean) => {
|
||||
try {
|
||||
await setChannelEnabled(channelType, enabled);
|
||||
if (gatewayManager.getStatus().state !== 'stopped') {
|
||||
logger.info(`Scheduling Gateway reload after channel:setEnabled (${channelType}, enabled=${enabled})`);
|
||||
gatewayManager.debouncedReload();
|
||||
} else {
|
||||
logger.info(`Gateway is stopped; skip immediate reload after channel:setEnabled (${channelType})`);
|
||||
}
|
||||
scheduleGatewayChannelRestart(`channel:setEnabled (${channelType}, enabled=${enabled})`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to set channel enabled:', error);
|
||||
|
||||
@@ -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<string, ChannelConfigData>;
|
||||
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)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawx",
|
||||
"version": "0.1.24-alpha.7",
|
||||
"version": "0.1.24-alpha.9",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@whiskeysockets/baileys",
|
||||
@@ -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",
|
||||
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
4
src/assets/channels/wecom.svg
Normal file
4
src/assets/channels/wecom.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.477 2 2 5.955 2 10.834c0 2.508 1.134 4.776 2.96 6.376.082.907-.375 2.112-1.077 3.018a10.05 10.05 0 0 0 3.32-.821c1.472.583 3.099.914 4.814.914 5.523 0 10-3.955 10-8.834S17.523 2 12 2z"/>
|
||||
<path d="M7.5 10l2 3.5 2.5-3.5 2.5 3.5 2-3.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -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://open.work.weixin.qq.com/help2/pc/cat?doc_id=21657",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,25 @@
|
||||
"注意:現在のドキュメントには記載されていませんが、権限管理で 'contact:contact.base:readonly' **アプリケーション権限** を必ず追加してください"
|
||||
]
|
||||
},
|
||||
"wecom": {
|
||||
"description": "プラグイン経由で WeCom Bot (企業微信) に接続します",
|
||||
"docsUrl": "https://open.work.weixin.qq.com/help2/pc/cat?doc_id=21657",
|
||||
"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": "ドキュメントを表示"
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,25 @@
|
||||
"注意:当前OpenClaw文档中未提及,但请务必在权限管理中添加 contact:contact.base:readonly **应用权限**,否则无法正常使用"
|
||||
]
|
||||
},
|
||||
"wecom": {
|
||||
"description": "通过插件连接企业微信机器人",
|
||||
"docsUrl": "https://open.work.weixin.qq.com/help2/pc/cat?doc_id=21657",
|
||||
"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": "查看文档"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ import discordIcon from '@/assets/channels/discord.svg';
|
||||
import whatsappIcon from '@/assets/channels/whatsapp.svg';
|
||||
import dingtalkIcon from '@/assets/channels/dingtalk.svg';
|
||||
import feishuIcon from '@/assets/channels/feishu.svg';
|
||||
import wecomIcon from '@/assets/channels/wecom.svg';
|
||||
|
||||
export function Channels() {
|
||||
const { t } = useTranslation('channels');
|
||||
@@ -112,7 +113,7 @@ export function Channels() {
|
||||
return (
|
||||
<div className="flex flex-col -m-6 dark:bg-background h-[calc(100vh-2.5rem)] overflow-hidden">
|
||||
<div className="w-full max-w-5xl mx-auto flex flex-col h-full p-10 pt-16">
|
||||
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-start justify-between mb-12 shrink-0 gap-4">
|
||||
<div>
|
||||
@@ -123,11 +124,14 @@ export function Channels() {
|
||||
{t('subtitle', 'Manage your messaging channels and connections')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 md:mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={fetchChannels}
|
||||
onClick={() => {
|
||||
void fetchChannels();
|
||||
void fetchConfiguredTypes();
|
||||
}}
|
||||
className="h-9 text-[13px] font-medium rounded-full px-4 border-black/10 dark:border-white/10 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 shadow-none text-foreground/80 hover:text-foreground transition-colors"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-2" />
|
||||
@@ -135,10 +139,9 @@ export function Channels() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-y-auto pr-2 pb-10 min-h-0 -mr-2">
|
||||
|
||||
|
||||
{/* Gateway Warning */}
|
||||
{gatewayStatus.state !== 'running' && (
|
||||
<div className="mb-8 p-4 rounded-xl border border-yellow-500/50 bg-yellow-500/10 flex items-center gap-3">
|
||||
@@ -182,15 +185,15 @@ export function Channels() {
|
||||
<h2 className="text-3xl font-serif text-foreground mb-6 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
|
||||
Supported Channels
|
||||
</h2>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||
{displayedChannelTypes.map((type) => {
|
||||
const meta = CHANNEL_META[type];
|
||||
const isConfigured = channels.some(c => c.type === type) || configuredTypes.includes(type);
|
||||
|
||||
|
||||
// Hide already configured channels from "Supported Channels" section
|
||||
if (isConfigured) return null;
|
||||
|
||||
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
@@ -223,7 +226,7 @@ export function Channels() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -280,6 +283,8 @@ function ChannelLogo({ type }: { type: ChannelType }) {
|
||||
return <img src={dingtalkIcon} alt="DingTalk" className="w-[22px] h-[22px] dark:invert" />;
|
||||
case 'feishu':
|
||||
return <img src={feishuIcon} alt="Feishu" className="w-[22px] h-[22px] dark:invert" />;
|
||||
case 'wecom':
|
||||
return <img src={wecomIcon} alt="WeCom" className="w-[22px] h-[22px] dark:invert" />;
|
||||
default:
|
||||
return <span className="text-[22px]">{CHANNEL_ICONS[type] || '💬'}</span>;
|
||||
}
|
||||
@@ -310,18 +315,18 @@ function ChannelCard({ channel, onDelete }: ChannelCardProps) {
|
||||
{t('pluginBadge', 'Plugin')}
|
||||
</Badge>
|
||||
)}
|
||||
<div
|
||||
<div
|
||||
className={cn(
|
||||
"w-2 h-2 rounded-full shrink-0",
|
||||
channel.status === 'connected' ? "bg-green-500" :
|
||||
channel.status === 'connecting' ? "bg-yellow-500 animate-pulse" :
|
||||
channel.status === 'error' ? "bg-destructive" :
|
||||
"bg-muted-foreground"
|
||||
)}
|
||||
channel.status === 'connecting' ? "bg-yellow-500 animate-pulse" :
|
||||
channel.status === 'error' ? "bg-destructive" :
|
||||
"bg-muted-foreground"
|
||||
)}
|
||||
title={channel.status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -334,7 +339,7 @@ function ChannelCard({ channel, onDelete }: ChannelCardProps) {
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{channel.error ? (
|
||||
<p className="text-[13.5px] text-destructive line-clamp-2 leading-[1.5]">
|
||||
{channel.error}
|
||||
|
||||
@@ -13,6 +13,7 @@ export type ChannelType =
|
||||
| 'discord'
|
||||
| 'signal'
|
||||
| 'feishu'
|
||||
| 'wecom'
|
||||
| 'imessage'
|
||||
| 'matrix'
|
||||
| 'line'
|
||||
@@ -84,6 +85,7 @@ export const CHANNEL_ICONS: Record<ChannelType, string> = {
|
||||
discord: '🎮',
|
||||
signal: '🔒',
|
||||
feishu: '🐦',
|
||||
wecom: '💼',
|
||||
imessage: '💬',
|
||||
matrix: '🔗',
|
||||
line: '🟢',
|
||||
@@ -102,6 +104,7 @@ export const CHANNEL_NAMES: Record<ChannelType, string> = {
|
||||
discord: 'Discord',
|
||||
signal: 'Signal',
|
||||
feishu: 'Feishu / Lark',
|
||||
wecom: 'WeCom',
|
||||
imessage: 'iMessage',
|
||||
matrix: 'Matrix',
|
||||
line: 'LINE',
|
||||
@@ -166,6 +169,36 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
wecom: {
|
||||
id: 'wecom',
|
||||
name: 'WeCom',
|
||||
icon: '💼',
|
||||
description: 'channels:meta.wecom.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.wecom.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'botId',
|
||||
label: 'channels:meta.wecom.fields.botId.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.wecom.fields.botId.placeholder',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'secret',
|
||||
label: 'channels:meta.wecom.fields.secret.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.wecom.fields.secret.placeholder',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.wecom.instructions.0',
|
||||
'channels:meta.wecom.instructions.1',
|
||||
'channels:meta.wecom.instructions.2',
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
telegram: {
|
||||
id: 'telegram',
|
||||
name: 'Telegram',
|
||||
@@ -496,7 +529,7 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
* Get primary supported channels (non-plugin, commonly used)
|
||||
*/
|
||||
export function getPrimaryChannels(): ChannelType[] {
|
||||
return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu'];
|
||||
return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu', 'wecom'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user