diff --git a/electron/api/routes/channels.ts b/electron/api/routes/channels.ts index aaded553c..168c6d581 100644 --- a/electron/api/routes/channels.ts +++ b/electron/api/routes/channels.ts @@ -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) }); diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index e5db2bc0e..13229952c 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -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); diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index b1678e88a..f8a3f284a 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 87dc1d64c..966e5887f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc3b1d851..c28c82b26 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 acdbe7134..9014fc218 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 cecb658ce..b3177441e 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/assets/channels/wecom.svg b/src/assets/channels/wecom.svg new file mode 100644 index 000000000..e644dd9f1 --- /dev/null +++ b/src/assets/channels/wecom.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/i18n/locales/en/channels.json b/src/i18n/locales/en/channels.json index 932660d88..de223142b 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://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" -} +} \ No newline at end of file diff --git a/src/i18n/locales/ja/channels.json b/src/i18n/locales/ja/channels.json index 3fa3ef7cb..b9eae181b 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://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": "ドキュメントを表示" -} +} \ No newline at end of file diff --git a/src/i18n/locales/zh/channels.json b/src/i18n/locales/zh/channels.json index e0a68e914..fddcc6c90 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://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": "查看文档" -} +} \ No newline at end of file diff --git a/src/pages/Channels/index.tsx b/src/pages/Channels/index.tsx index da8f6d43b..dfcc2e524 100644 --- a/src/pages/Channels/index.tsx +++ b/src/pages/Channels/index.tsx @@ -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 (
- + {/* Header */}
@@ -123,11 +124,14 @@ export function Channels() { {t('subtitle', 'Manage your messaging channels and connections')}

- +
- {/* Content Area */}
- + {/* Gateway Warning */} {gatewayStatus.state !== 'running' && (
@@ -182,15 +185,15 @@ export function Channels() {

Supported Channels

- +
{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 (
- +
@@ -280,6 +283,8 @@ function ChannelLogo({ type }: { type: ChannelType }) { return DingTalk; case 'feishu': return Feishu; + case 'wecom': + return WeCom; default: return {CHANNEL_ICONS[type] || '💬'}; } @@ -310,18 +315,18 @@ function ChannelCard({ channel, onDelete }: ChannelCardProps) { {t('pluginBadge', 'Plugin')} )} -
- +
- + {channel.error ? (

{channel.error} diff --git a/src/types/channel.ts b/src/types/channel.ts index 26afe814b..aac7c2080 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -13,6 +13,7 @@ export type ChannelType = | 'discord' | 'signal' | 'feishu' + | 'wecom' | 'imessage' | 'matrix' | 'line' @@ -84,6 +85,7 @@ export const CHANNEL_ICONS: Record = { discord: '🎮', signal: '🔒', feishu: '🐦', + wecom: '💼', imessage: '💬', matrix: '🔗', line: '🟢', @@ -102,6 +104,7 @@ export const CHANNEL_NAMES: Record = { discord: 'Discord', signal: 'Signal', feishu: 'Feishu / Lark', + wecom: 'WeCom', imessage: 'iMessage', matrix: 'Matrix', line: 'LINE', @@ -166,6 +169,36 @@ export const CHANNEL_META: Record = { ], 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 = { * 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']; } /**