diff --git a/electron-builder.yml b/electron-builder.yml index 388af4d4a..d0e818928 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -26,6 +26,9 @@ extraResources: # because electron-builder respects .gitignore which excludes node_modules/) - from: build/openclaw/ to: openclaw/ + # Bundled OpenClaw plugin mirrors (dingtalk, etc.) + - from: build/openclaw-plugins/ + to: openclaw-plugins/ afterPack: ./scripts/after-pack.cjs diff --git a/electron/gateway/client.ts b/electron/gateway/client.ts index 78cdc2a78..7efb144f0 100644 --- a/electron/gateway/client.ts +++ b/electron/gateway/client.ts @@ -7,7 +7,7 @@ import { GatewayManager, GatewayStatus } from './manager'; /** * Channel types supported by OpenClaw */ -export type ChannelType = 'whatsapp' | 'telegram' | 'discord' | 'wechat'; +export type ChannelType = 'whatsapp' | 'dingtalk' | 'telegram' | 'discord' | 'wechat'; /** * Channel status diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 29e588f70..d17b088b5 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -3,7 +3,7 @@ * Registers all IPC handlers for main-renderer communication */ import { ipcMain, BrowserWindow, shell, dialog, app, nativeImage } from 'electron'; -import { existsSync } from 'node:fs'; +import { existsSync, cpSync, mkdirSync, rmSync } from 'node:fs'; import { homedir } from 'node:os'; import { join, extname, basename } from 'node:path'; import crypto from 'node:crypto'; @@ -608,6 +608,50 @@ function registerGatewayHandlers( * For checking package status and channel configuration */ function registerOpenClawHandlers(gatewayManager: GatewayManager): void { + async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { + const targetDir = join(homedir(), '.openclaw', 'extensions', 'dingtalk'); + const targetManifest = join(targetDir, 'openclaw.plugin.json'); + + if (existsSync(targetManifest)) { + logger.info('DingTalk plugin already installed from local mirror'); + return { installed: true }; + } + + const candidateSources = app.isPackaged + ? [join(process.resourcesPath, 'openclaw-plugins', 'dingtalk')] + : [ + join(app.getAppPath(), 'build', 'openclaw-plugins', 'dingtalk'), + join(process.cwd(), 'build', 'openclaw-plugins', 'dingtalk'), + join(__dirname, '../../build/openclaw-plugins/dingtalk'), + ]; + + const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); + if (!sourceDir) { + return { + installed: false, + warning: 'Bundled DingTalk plugin mirror not found. Run: pnpm run bundle:openclaw-plugins', + }; + } + + 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 DingTalk plugin mirror (manifest missing).' }; + } + + logger.info(`Installed DingTalk plugin from bundled mirror: ${sourceDir}`); + return { installed: true }; + } catch (error) { + logger.warn('Failed to install DingTalk plugin from bundled mirror:', error); + return { + installed: false, + warning: 'Failed to install bundled DingTalk plugin mirror', + }; + } + } // Get OpenClaw package status ipcMain.handle('openclaw:status', () => { @@ -666,12 +710,23 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void { ipcMain.handle('channel:saveConfig', async (_, channelType: string, config: Record) => { try { logger.info('channel:saveConfig', { channelType, keys: Object.keys(config || {}) }); + if (channelType === 'dingtalk') { + const installResult = await ensureDingTalkPluginInstalled(); + if (!installResult.installed) { + return { + success: false, + error: installResult.warning || 'DingTalk plugin install failed', + }; + } + saveChannelConfig(channelType, config); + return { + success: true, + pluginInstalled: installResult.installed, + warning: installResult.warning, + }; + } await saveChannelConfig(channelType, config); // Debounced restart so the gateway picks up the new channel config. - // The gateway watches openclaw.json, but a restart ensures a clean - // start for newly-added channels. Using debouncedRestart() here - // instead of an explicit restart on the frontend side means that - // rapid config changes (e.g. setup wizard) coalesce into one restart. gatewayManager.debouncedRestart(); return { success: true }; } catch (error) { diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index 8191a6461..102d6aa63 100644 --- a/electron/utils/channel-config.ts +++ b/electron/utils/channel-config.ts @@ -86,6 +86,36 @@ export async function saveChannelConfig( ): Promise { const currentConfig = await readOpenClawConfig(); + // 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') { + if (!currentConfig.plugins) { + currentConfig.plugins = {}; + } + 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']; + } + } + + // 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') { + if (!currentConfig.plugins) { + currentConfig.plugins = {}; + } + 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']; + } + } + // Plugin-based channels (e.g. WhatsApp) go under plugins.entries, not channels if (PLUGIN_CHANNELS.includes(channelType)) { if (!currentConfig.plugins) { diff --git a/package.json b/package.json index 83a2d9b78..6985fc77e 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "scripts": { "init": "pnpm install && pnpm run uv:download", "dev": "vite", - "build": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder", + "build": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder", "build:vite": "vite build", + "bundle:openclaw-plugins": "zx scripts/bundle-openclaw-plugins.mjs", "lint": "eslint . --fix", "typecheck": "tsc --noEmit", "test": "vitest run", @@ -32,10 +33,10 @@ "uv:download:all": "zx scripts/download-bundled-uv.mjs --all", "icons": "zx scripts/generate-icons.mjs", "package": "electron-builder", - "package:mac": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder --mac", - "package:win": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder --win", - "package:linux": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder --linux", - "release": "pnpm run uv:download && vite build && electron-builder --publish always", + "package:mac": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --mac", + "package:win": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --win", + "package:linux": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --linux", + "release": "pnpm run uv:download && vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --publish always", "version:patch": "pnpm version patch", "version:minor": "pnpm version minor", "version:major": "pnpm version major", @@ -62,6 +63,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", + "@soimy/dingtalk": "^3.1.4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/node": "^25.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ed2b0100..1dc1b3004 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@soimy/dingtalk': + specifier: ^3.1.4 + version: 3.1.4(openclaw@2026.2.26(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3))) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -2596,6 +2599,11 @@ packages: resolution: {integrity: sha512-vNZk5y+IsxjwzTAXikvzz5pqMLb35YytC64nVF2MAFVhjpXu9ITOKUriZ0JG/llwzCAi56jb5x0cXDRIyE2A2A==} engines: {node: '>= 10'} + '@soimy/dingtalk@3.1.4': + resolution: {integrity: sha512-57MRun9Z8Kt7GhsbL8f04m2QhWOvaE9x5o+eAdj/V3MxlFPLPCT2OMgvMxKHggfOk1rRmHI3h0/778uiXmUHKA==} + peerDependencies: + openclaw: '>=2026.2.13' + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -3620,6 +3628,9 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + dingtalk-stream@2.1.4: + resolution: {integrity: sha512-rgQbXLGWfASuB9onFcqXTnRSj4ZotimhBOnzrB4kS19AaU9lshXiuofs1GAYcKh5uzPWCAuEs3tMtiadTQWP4A==} + dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} @@ -9697,6 +9708,19 @@ snapshots: '@snazzah/davey-win32-ia32-msvc': 0.1.9 '@snazzah/davey-win32-x64-msvc': 0.1.9 + '@soimy/dingtalk@3.1.4(openclaw@2026.2.26(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3)))': + dependencies: + axios: 1.13.5(debug@4.4.3) + dingtalk-stream: 2.1.4 + form-data: 4.0.5 + openclaw: 2026.2.26(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3)) + zod: 4.3.6 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + '@standard-schema/spec@1.1.0': {} '@szmarczak/http-timer@4.0.6': @@ -10947,6 +10971,16 @@ snapshots: diff@8.0.3: {} + dingtalk-stream@2.1.4: + dependencies: + axios: 1.13.5(debug@4.4.3) + debug: 4.4.3 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dir-compare@4.2.0: dependencies: minimatch: 3.1.2 diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index 361a21f53..66ba2d625 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -149,6 +149,8 @@ exports.default = async function afterPack(context) { const openclawRoot = join(resourcesDir, 'openclaw'); const dest = join(openclawRoot, 'node_modules'); + const pluginsSrcRoot = join(__dirname, '..', 'build', 'openclaw-plugins'); + const pluginsDestRoot = join(resourcesDir, 'openclaw-plugins'); if (!existsSync(src)) { console.warn('[after-pack] ⚠️ build/openclaw/node_modules not found. Run bundle-openclaw first.'); @@ -164,6 +166,29 @@ exports.default = async function afterPack(context) { cpSync(src, dest, { recursive: true }); console.log('[after-pack] ✅ openclaw node_modules copied.'); + // 1.1 Copy plugin node_modules (also skipped due to .gitignore) + if (existsSync(pluginsSrcRoot) && existsSync(pluginsDestRoot)) { + const pluginDirs = readdirSync(pluginsSrcRoot, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + + for (const pluginId of pluginDirs) { + const pluginSrcNM = join(pluginsSrcRoot, pluginId, 'node_modules'); + const pluginDestRoot = join(pluginsDestRoot, pluginId); + const pluginDestNM = join(pluginDestRoot, 'node_modules'); + if (!existsSync(pluginSrcNM) || !existsSync(pluginDestRoot)) continue; + + console.log(`[after-pack] Copying plugin deps for ${pluginId} -> ${pluginDestNM}`); + cpSync(pluginSrcNM, pluginDestNM, { recursive: true }); + + // Apply the same cleanup strategy for plugin bundles. + cleanupUnnecessaryFiles(pluginDestRoot); + cleanupKoffi(pluginDestNM, platform, arch); + cleanupNativePlatformPackages(pluginDestNM, platform, arch); + } + console.log('[after-pack] ✅ openclaw plugin node_modules copied.'); + } + // 2. General cleanup on the full openclaw directory (not just node_modules) console.log('[after-pack] 🧹 Cleaning up unnecessary files ...'); const removedRoot = cleanupUnnecessaryFiles(openclawRoot); diff --git a/scripts/bundle-openclaw-plugins.mjs b/scripts/bundle-openclaw-plugins.mjs new file mode 100644 index 000000000..0b92a828c --- /dev/null +++ b/scripts/bundle-openclaw-plugins.mjs @@ -0,0 +1,159 @@ +#!/usr/bin/env zx + +/** + * bundle-openclaw-plugins.mjs + * + * Build a self-contained mirror of OpenClaw third-party plugins for packaging. + * Current plugins: + * - @soimy/dingtalk -> build/openclaw-plugins/dingtalk + * + * The output plugin directory contains: + * - plugin source files (index.ts, openclaw.plugin.json, package.json, ...) + * - plugin runtime node_modules/ (flattened direct + transitive deps) + */ + +import 'zx/globals'; +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(__dirname, '..'); +const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins'); +const NODE_MODULES = path.join(ROOT, 'node_modules'); + +const PLUGINS = [ + { npmName: '@soimy/dingtalk', pluginId: 'dingtalk' }, +]; + +function getVirtualStoreNodeModules(realPkgPath) { + let dir = realPkgPath; + while (dir !== path.dirname(dir)) { + if (path.basename(dir) === 'node_modules') return dir; + dir = path.dirname(dir); + } + return null; +} + +function listPackages(nodeModulesDir) { + const result = []; + if (!fs.existsSync(nodeModulesDir)) return result; + + for (const entry of fs.readdirSync(nodeModulesDir)) { + if (entry === '.bin') continue; + const entryPath = path.join(nodeModulesDir, entry); + const stat = fs.lstatSync(entryPath); + + if (entry.startsWith('@')) { + if (!(stat.isDirectory() || stat.isSymbolicLink())) continue; + let scopeEntries = []; + try { + scopeEntries = fs.readdirSync(entryPath); + } catch { + continue; + } + for (const sub of scopeEntries) { + result.push({ + name: `${entry}/${sub}`, + fullPath: path.join(entryPath, sub), + }); + } + } else { + result.push({ name: entry, fullPath: entryPath }); + } + } + return result; +} + +function bundleOnePlugin({ npmName, pluginId }) { + const pkgPath = path.join(NODE_MODULES, ...npmName.split('/')); + if (!fs.existsSync(pkgPath)) { + throw new Error(`Missing dependency "${npmName}". Run pnpm install first.`); + } + + const realPluginPath = fs.realpathSync(pkgPath); + const outputDir = path.join(OUTPUT_ROOT, pluginId); + + echo`📦 Bundling plugin ${npmName} -> ${outputDir}`; + + if (fs.existsSync(outputDir)) { + fs.rmSync(outputDir, { recursive: true, force: true }); + } + fs.mkdirSync(outputDir, { recursive: true }); + + // 1) Copy plugin package itself + fs.cpSync(realPluginPath, outputDir, { recursive: true, dereference: true }); + + // 2) Collect transitive deps from pnpm virtual store + const collected = new Map(); + const queue = []; + const rootVirtualNM = getVirtualStoreNodeModules(realPluginPath); + if (!rootVirtualNM) { + throw new Error(`Cannot resolve virtual store node_modules for ${npmName}`); + } + queue.push({ nodeModulesDir: rootVirtualNM, skipPkg: npmName }); + + const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']); + const SKIP_SCOPES = ['@types/']; + + while (queue.length > 0) { + const { nodeModulesDir, skipPkg } = queue.shift(); + for (const { name, fullPath } of listPackages(nodeModulesDir)) { + if (name === skipPkg) continue; + if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some((s) => name.startsWith(s))) continue; + + let realPath; + try { + realPath = fs.realpathSync(fullPath); + } catch { + continue; + } + if (collected.has(realPath)) continue; + collected.set(realPath, name); + + const depVirtualNM = getVirtualStoreNodeModules(realPath); + if (depVirtualNM && depVirtualNM !== nodeModulesDir) { + queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name }); + } + } + } + + // 3) Copy flattened deps into plugin/node_modules + const outputNodeModules = path.join(outputDir, 'node_modules'); + fs.mkdirSync(outputNodeModules, { recursive: true }); + + let copiedCount = 0; + let skippedDupes = 0; + const copiedNames = new Set(); + + for (const [realPath, pkgName] of collected) { + if (copiedNames.has(pkgName)) { + skippedDupes++; + continue; + } + copiedNames.add(pkgName); + + const dest = path.join(outputNodeModules, pkgName); + try { + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.cpSync(realPath, dest, { recursive: true, dereference: true }); + copiedCount++; + } catch (err) { + echo` ⚠️ Skipped ${pkgName}: ${err.message}`; + } + } + + const manifestPath = path.join(outputDir, 'openclaw.plugin.json'); + if (!fs.existsSync(manifestPath)) { + throw new Error(`Missing openclaw.plugin.json in bundled plugin output: ${pluginId}`); + } + + echo` ✅ ${pluginId}: copied ${copiedCount} deps (skipped dupes: ${skippedDupes})`; +} + +echo`📦 Bundling OpenClaw plugin mirrors...`; +fs.mkdirSync(OUTPUT_ROOT, { recursive: true }); + +for (const plugin of PLUGINS) { + bundleOnePlugin(plugin); +} + +echo`✅ Plugin mirrors ready: ${OUTPUT_ROOT}`; diff --git a/src/i18n/locales/en/channels.json b/src/i18n/locales/en/channels.json index da348ddc3..6f141a641 100644 --- a/src/i18n/locales/en/channels.json +++ b/src/i18n/locales/en/channels.json @@ -111,6 +111,38 @@ "The system will automatically identify your phone number" ] }, + "dingtalk": { + "description": "Connect DingTalk via OpenClaw channel plugin (Stream mode)", + "docsUrl": "https://github.com/soimy/openclaw-channel-dingtalk", + "fields": { + "clientId": { + "label": "Client ID (AppKey)", + "placeholder": "dingxxxxxx" + }, + "clientSecret": { + "label": "Client Secret (AppSecret)", + "placeholder": "Your app secret" + }, + "robotCode": { + "label": "Robot Code (optional)", + "placeholder": "Usually same as Client ID" + }, + "corpId": { + "label": "Corp ID (optional)", + "placeholder": "dingxxxxxx" + }, + "agentId": { + "label": "Agent ID (optional)", + "placeholder": "123456789" + } + }, + "instructions": [ + "Install and enable the dingtalk plugin in OpenClaw", + "Create a DingTalk internal app and enable Stream mode", + "Fill in Client ID and Client Secret (required)", + "Fill in Robot Code / Corp ID / Agent ID if your setup requires them" + ] + }, "signal": { "description": "Connect Signal using signal-cli", "docsUrl": "https://docs.openclaw.ai/channels/signal", diff --git a/src/i18n/locales/ja/channels.json b/src/i18n/locales/ja/channels.json index 7976b1440..a92456877 100644 --- a/src/i18n/locales/ja/channels.json +++ b/src/i18n/locales/ja/channels.json @@ -108,6 +108,38 @@ "システムが自動的に電話番号を識別します" ] }, + "dingtalk": { + "description": "OpenClaw のチャンネルプラグイン経由で DingTalk に接続します(Stream モード)", + "docsUrl": "https://github.com/soimy/openclaw-channel-dingtalk", + "fields": { + "clientId": { + "label": "Client ID (AppKey)", + "placeholder": "dingxxxxxx" + }, + "clientSecret": { + "label": "Client Secret (AppSecret)", + "placeholder": "アプリのシークレット" + }, + "robotCode": { + "label": "Robot Code(任意)", + "placeholder": "通常は Client ID と同じ" + }, + "corpId": { + "label": "Corp ID(任意)", + "placeholder": "dingxxxxxx" + }, + "agentId": { + "label": "Agent ID(任意)", + "placeholder": "123456789" + } + }, + "instructions": [ + "まず OpenClaw に dingtalk プラグインをインストールして有効化します", + "DingTalk 開発者コンソールで社内アプリを作成し Stream モードを有効にします", + "Client ID と Client Secret を入力します(必須)", + "必要に応じて Robot Code / Corp ID / Agent ID を入力します" + ] + }, "signal": { "description": "signal-cli を使用して Signal に接続します", "fields": { diff --git a/src/i18n/locales/zh/channels.json b/src/i18n/locales/zh/channels.json index e13b7e3ac..9bd0c3960 100644 --- a/src/i18n/locales/zh/channels.json +++ b/src/i18n/locales/zh/channels.json @@ -111,6 +111,38 @@ "系统将自动识别您的手机号" ] }, + "dingtalk": { + "description": "通过 OpenClaw 渠道插件连接钉钉(Stream 模式)", + "docsUrl": "https://github.com/soimy/openclaw-channel-dingtalk", + "fields": { + "clientId": { + "label": "Client ID (AppKey)", + "placeholder": "dingxxxxxx" + }, + "clientSecret": { + "label": "Client Secret (AppSecret)", + "placeholder": "您的应用密钥" + }, + "robotCode": { + "label": "Robot Code(可选)", + "placeholder": "通常与 Client ID 相同" + }, + "corpId": { + "label": "Corp ID(可选)", + "placeholder": "dingxxxxxx" + }, + "agentId": { + "label": "Agent ID(可选)", + "placeholder": "123456789" + } + }, + "instructions": [ + "先在 OpenClaw 安装并启用 dingtalk 插件", + "在钉钉开发者后台创建企业内部应用并开启 Stream 模式", + "填写 Client ID 和 Client Secret(必填)", + "根据你的应用配置按需填写 Robot Code / Corp ID / Agent ID" + ] + }, "signal": { "description": "使用 signal-cli 连接 Signal", "docsUrl": "https://docs.openclaw.ai/zh-CN/channels/signal", diff --git a/src/pages/Channels/index.tsx b/src/pages/Channels/index.tsx index 6778d66f5..f47fc6c15 100644 --- a/src/pages/Channels/index.tsx +++ b/src/pages/Channels/index.tsx @@ -570,7 +570,17 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded // Step 2: Save channel configuration via IPC const config: Record = { ...configValues }; - await window.electron.ipcRenderer.invoke('channel:saveConfig', selectedType, config); + const saveResult = await window.electron.ipcRenderer.invoke('channel:saveConfig', selectedType, config) as { + success?: boolean; + warning?: string; + pluginInstalled?: boolean; + }; + if (!saveResult?.success) { + throw new Error('Failed to save channel config'); + } + if (typeof saveResult.warning === 'string' && saveResult.warning) { + toast.warning(saveResult.warning); + } // Step 3: Add a local channel entry for the UI await addChannel({ diff --git a/src/types/channel.ts b/src/types/channel.ts index b6bc8b7bf..26afe814b 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -8,6 +8,7 @@ */ export type ChannelType = | 'whatsapp' + | 'dingtalk' | 'telegram' | 'discord' | 'signal' @@ -78,6 +79,7 @@ export interface ChannelMeta { */ export const CHANNEL_ICONS: Record = { whatsapp: '📱', + dingtalk: '💬', telegram: '✈️', discord: '🎮', signal: '🔒', @@ -95,6 +97,7 @@ export const CHANNEL_ICONS: Record = { */ export const CHANNEL_NAMES: Record = { whatsapp: 'WhatsApp', + dingtalk: 'DingTalk', telegram: 'Telegram', discord: 'Discord', signal: 'Signal', @@ -111,6 +114,58 @@ export const CHANNEL_NAMES: Record = { * Channel metadata with configuration information */ export const CHANNEL_META: Record = { + dingtalk: { + id: 'dingtalk', + name: 'DingTalk', + icon: '💬', + description: 'channels:meta.dingtalk.description', + connectionType: 'token', + docsUrl: 'channels:meta.dingtalk.docsUrl', + configFields: [ + { + key: 'clientId', + label: 'channels:meta.dingtalk.fields.clientId.label', + type: 'text', + placeholder: 'channels:meta.dingtalk.fields.clientId.placeholder', + required: true, + }, + { + key: 'clientSecret', + label: 'channels:meta.dingtalk.fields.clientSecret.label', + type: 'password', + placeholder: 'channels:meta.dingtalk.fields.clientSecret.placeholder', + required: true, + }, + { + key: 'robotCode', + label: 'channels:meta.dingtalk.fields.robotCode.label', + type: 'text', + placeholder: 'channels:meta.dingtalk.fields.robotCode.placeholder', + required: false, + }, + { + key: 'corpId', + label: 'channels:meta.dingtalk.fields.corpId.label', + type: 'text', + placeholder: 'channels:meta.dingtalk.fields.corpId.placeholder', + required: false, + }, + { + key: 'agentId', + label: 'channels:meta.dingtalk.fields.agentId.label', + type: 'text', + placeholder: 'channels:meta.dingtalk.fields.agentId.placeholder', + required: false, + }, + ], + instructions: [ + 'channels:meta.dingtalk.instructions.0', + 'channels:meta.dingtalk.instructions.1', + 'channels:meta.dingtalk.instructions.2', + 'channels:meta.dingtalk.instructions.3', + ], + isPlugin: true, + }, telegram: { id: 'telegram', name: 'Telegram', @@ -441,7 +496,7 @@ export const CHANNEL_META: Record = { * Get primary supported channels (non-plugin, commonly used) */ export function getPrimaryChannels(): ChannelType[] { - return ['telegram', 'discord', 'whatsapp', 'feishu']; + return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu']; } /**