diff --git a/electron/api/routes/channels.ts b/electron/api/routes/channels.ts index 1b2a992..3197d20 100644 --- a/electron/api/routes/channels.ts +++ b/electron/api/routes/channels.ts @@ -25,6 +25,7 @@ import { } from '../../utils/agent-config'; import { ensureDingTalkPluginInstalled, + ensureFeishuPluginInstalled, ensureWeChatPluginInstalled, ensureWeComPluginInstalled, } from '../../utils/plugin-install'; @@ -1348,7 +1349,14 @@ export async function handleChannelRoutes( return true; } } - // Feishu is a built-in extension since OpenClaw 2026.4.11 — no plugin install needed + // QQBot is a built-in channel since OpenClaw 3.31 — no plugin install needed + if (storedChannelType === 'feishu') { + const installResult = await ensureFeishuPluginInstalled(); + if (!installResult.installed) { + sendJson(res, 500, { success: false, error: installResult.warning || 'Feishu plugin install failed' }); + return true; + } + } if (storedChannelType === OPENCLAW_WECHAT_CHANNEL_TYPE) { const installResult = await ensureWeChatPluginInstalled(); if (!installResult.installed) { diff --git a/electron/gateway/config-sync.ts b/electron/gateway/config-sync.ts index 4fa3f8c..8d1b142 100644 --- a/electron/gateway/config-sync.ts +++ b/electron/gateway/config-sync.ts @@ -48,6 +48,7 @@ export interface GatewayLaunchContext { const CHANNEL_PLUGIN_MAP: Record = { dingtalk: { dirName: 'dingtalk', npmName: '@soimy/dingtalk' }, wecom: { dirName: 'wecom', npmName: '@wecom/wecom-openclaw-plugin' }, + feishu: { dirName: 'feishu-openclaw-plugin', npmName: '@larksuite/openclaw-lark' }, 'openclaw-weixin': { dirName: 'openclaw-weixin', npmName: '@tencent-weixin/openclaw-weixin' }, }; @@ -58,7 +59,7 @@ const CHANNEL_PLUGIN_MAP: Record = * ~/.openclaw/extensions/, the broken copy overrides the working built-in * plugin and must be removed. */ -const BUILTIN_CHANNEL_EXTENSIONS = ['discord', 'telegram', 'qqbot', 'feishu-openclaw-plugin']; +const BUILTIN_CHANNEL_EXTENSIONS = ['discord', 'telegram', 'qqbot']; function cleanupStaleBuiltInExtensions(): void { for (const ext of BUILTIN_CHANNEL_EXTENSIONS) { @@ -273,6 +274,11 @@ export async function syncGatewayConfigBeforeLaunch( for (const [channelType, info] of Object.entries(CHANNEL_PLUGIN_MAP)) { pluginIdToChannel[info.dirName] = channelType; } + // Known manifest IDs that differ from their dirName/channelType + + pluginIdToChannel['openclaw-lark'] = 'feishu'; + pluginIdToChannel['feishu-openclaw-plugin'] = 'feishu'; + for (const pluginId of allowList) { const channelType = pluginIdToChannel[pluginId] ?? pluginId; if (CHANNEL_PLUGIN_MAP[channelType] && !configuredChannels.includes(channelType)) { diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index f8ebb3a..2958678 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -36,6 +36,7 @@ import { toOpenClawChannelType, toUiChannelType } from '../utils/channel-alias'; import { checkUvInstalled, installUv, setupManagedPython } from '../utils/uv-setup'; import { ensureDingTalkPluginInstalled, + ensureFeishuPluginInstalled, ensureWeComPluginInstalled, } from '../utils/plugin-install'; import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config'; @@ -1496,7 +1497,23 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void { warning: installResult.warning, }; } - // Feishu is a built-in extension since OpenClaw 2026.4.11 — no plugin install needed + // QQBot is a built-in channel since OpenClaw 3.31 — no plugin install needed + if (channelType === 'feishu') { + const installResult = await ensureFeishuPluginInstalled(); + if (!installResult.installed) { + return { + success: false, + error: installResult.warning || 'Feishu plugin install failed', + }; + } + await saveChannelConfig(channelType, config); + scheduleGatewayChannelSaveRefresh(channelType, `channel:saveConfig (${channelType})`); + return { + success: true, + pluginInstalled: installResult.installed, + warning: installResult.warning, + }; + } await saveChannelConfig(channelType, config); scheduleGatewayChannelSaveRefresh(channelType, `channel:saveConfig (${channelType})`); return { success: true }; diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index ff98c69..a461ab3 100644 --- a/electron/utils/channel-config.ts +++ b/electron/utils/channel-config.ts @@ -24,7 +24,7 @@ const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json'); const WECOM_PLUGIN_ID = 'wecom'; // Note: QQBot is a built-in channel since OpenClaw 3.31 — no plugin ID needed. const WECHAT_PLUGIN_ID = OPENCLAW_WECHAT_CHANNEL_TYPE; -const FEISHU_PLUGIN_ID_CANDIDATES = ['feishu', 'openclaw-lark', 'feishu-openclaw-plugin'] as const; +const FEISHU_PLUGIN_ID_CANDIDATES = ['openclaw-lark', 'feishu-openclaw-plugin'] as const; const DEFAULT_ACCOUNT_ID = 'default'; // Channels whose plugin schema uses additionalProperties:false, meaning // credential keys MUST NOT appear at the top level of `channels.`. diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index 3f24435..8b322b3 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -208,7 +208,7 @@ async function discoverAgentIds(): Promise { // ── OpenClaw Config Helpers ────────────────────────────────────── const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json'); -const FEISHU_PLUGIN_ID_CANDIDATES = ['feishu', 'openclaw-lark', 'feishu-openclaw-plugin'] as const; +const FEISHU_PLUGIN_ID_CANDIDATES = ['openclaw-lark', 'feishu-openclaw-plugin'] as const; const VALID_COMPACTION_MODES = new Set(['default', 'safeguard']); const BUILTIN_CHANNEL_IDS = new Set([ 'discord', diff --git a/electron/utils/plugin-install.ts b/electron/utils/plugin-install.ts index db08bf3..6c294b0 100644 --- a/electron/utils/plugin-install.ts +++ b/electron/utils/plugin-install.ts @@ -2,7 +2,7 @@ * Shared OpenClaw Plugin Install Utilities * * Provides version-aware install/upgrade logic for bundled OpenClaw plugins - * (DingTalk, WeCom, WeChat). Used both at app startup (to auto-upgrade + * (DingTalk, WeCom, Feishu, WeChat). Used both at app startup (to auto-upgrade * stale plugins) and when a user configures a channel. * * Note: QQBot was moved to a built-in channel in OpenClaw 3.31 and is no longer @@ -233,6 +233,7 @@ function patchPluginEntryIds(targetDir: string): void { const PLUGIN_NPM_NAMES: Record = { dingtalk: '@soimy/dingtalk', wecom: '@wecom/wecom-openclaw-plugin', + 'feishu-openclaw-plugin': '@larksuite/openclaw-lark', 'openclaw-weixin': '@tencent-weixin/openclaw-weixin', }; @@ -502,6 +503,15 @@ export function ensureWeComPluginInstalled(): { installed: boolean; warning?: st return ensurePluginInstalled('wecom', buildCandidateSources('wecom'), 'WeCom'); } +export function ensureFeishuPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled( + 'feishu-openclaw-plugin', + buildCandidateSources('feishu-openclaw-plugin'), + 'Feishu', + ); +} + + export function ensureWeChatPluginInstalled(): { installed: boolean; warning?: string } { return ensurePluginInstalled('openclaw-weixin', buildCandidateSources('openclaw-weixin'), 'WeChat'); @@ -515,6 +525,8 @@ export function ensureWeChatPluginInstalled(): { installed: boolean; warning?: s const ALL_BUNDLED_PLUGINS = [ { fn: ensureDingTalkPluginInstalled, label: 'DingTalk' }, { fn: ensureWeComPluginInstalled, label: 'WeCom' }, + + { fn: ensureFeishuPluginInstalled, label: 'Feishu' }, { fn: ensureWeChatPluginInstalled, label: 'WeChat' }, ] as const; diff --git a/package.json b/package.json index 4a8e55c..33eb184 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@larksuite/openclaw-lark": "2026.4.7", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fc3118..4f63ab7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,9 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.1.0(jiti@1.21.7)) + '@larksuite/openclaw-lark': + specifier: 2026.4.7 + version: 2026.4.7(openclaw@2026.4.11(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(typescript@5.9.3)) '@playwright/test': specifier: ^1.56.1 version: 1.59.0 @@ -1304,6 +1307,16 @@ packages: peerDependencies: apache-arrow: '>=15.0.0 <=18.1.0' + '@larksuite/openclaw-lark@2026.4.7': + resolution: {integrity: sha512-oiS7hHwJpoOQCHjgAT2xPTO9zmmUKEje2kgsYC+Q8ZMu0gn/sI+FE2NYnQ3dVcgqw7z+2rdajgcTP6kkisFxNw==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + openclaw: '>=2026.3.22' + peerDependenciesMeta: + openclaw: + optional: true + '@larksuiteoapi/node-sdk@1.60.0': resolution: {integrity: sha512-MS1eXx7K6HHIyIcCBkJLb21okoa8ZatUGQWZaCCUePm6a37RWFmT6ZKlKvHxAanSX26wNuNlwP0RhgscsE+T6g==} @@ -4417,6 +4430,11 @@ packages: image-q@4.0.0: resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -8410,6 +8428,19 @@ snapshots: '@lancedb/lancedb-win32-arm64-msvc': 0.27.2 '@lancedb/lancedb-win32-x64-msvc': 0.27.2 + '@larksuite/openclaw-lark@2026.4.7(openclaw@2026.4.11(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(typescript@5.9.3))': + dependencies: + '@larksuiteoapi/node-sdk': 1.60.0 + '@sinclair/typebox': 0.34.48 + image-size: 2.0.2 + zod: 4.3.6 + optionalDependencies: + openclaw: 2026.4.11(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(typescript@5.9.3) + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + '@larksuiteoapi/node-sdk@1.60.0': dependencies: axios: 1.13.6(debug@4.4.3) @@ -9890,7 +9921,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 25.6.0 + '@types/node': 25.5.0 '@types/bun@1.3.11': dependencies: @@ -9915,7 +9946,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.5.0 '@types/debug@4.1.13': dependencies: @@ -9935,7 +9966,7 @@ snapshots: '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.5.0 '@types/qs': 6.15.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -10025,12 +10056,12 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.5.0 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 25.6.0 + '@types/node': 25.5.0 '@types/unist@2.0.11': {} @@ -12001,6 +12032,8 @@ snapshots: dependencies: '@types/node': 16.9.1 + image-size@2.0.2: {} + immediate@3.0.6: {} imurmurhash@0.1.4: {} diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index dd038c4..e3184e5 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -575,6 +575,7 @@ exports.default = async function afterPack(context) { const BUNDLED_PLUGINS = [ { npmName: '@soimy/dingtalk', pluginId: 'dingtalk' }, { npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' }, + { npmName: '@larksuite/openclaw-lark', pluginId: 'feishu-openclaw-plugin' }, { npmName: '@tencent-weixin/openclaw-weixin', pluginId: 'openclaw-weixin' }, ]; diff --git a/scripts/bundle-openclaw-plugins.mjs b/scripts/bundle-openclaw-plugins.mjs index 4853cfb..1d34992 100644 --- a/scripts/bundle-openclaw-plugins.mjs +++ b/scripts/bundle-openclaw-plugins.mjs @@ -5,13 +5,10 @@ * * Build a self-contained mirror of OpenClaw third-party plugins for packaging. * Current plugins: - * - @soimy/dingtalk -> build/openclaw-plugins/dingtalk + * - @soimy/dingtalk -> build/openclaw-plugins/dingtalk * - @wecom/wecom-openclaw-plugin -> build/openclaw-plugins/wecom * - @tencent-weixin/openclaw-weixin -> build/openclaw-plugins/openclaw-weixin * - * Note: Feishu (@larksuite/openclaw-lark) is now a built-in extension in - * OpenClaw 2026.4.11+ and no longer needs to be bundled separately. - * * The output plugin directory contains: * - plugin source files (index.ts, openclaw.plugin.json, package.json, ...) * - plugin runtime node_modules/ (flattened direct + transitive deps) @@ -41,6 +38,7 @@ function normWin(p) { const PLUGINS = [ { npmName: '@soimy/dingtalk', pluginId: 'dingtalk' }, { npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' }, + { npmName: '@larksuite/openclaw-lark', pluginId: 'feishu-openclaw-plugin' }, { npmName: '@tencent-weixin/openclaw-weixin', pluginId: 'openclaw-weixin' }, ]; diff --git a/scripts/bundle-openclaw.mjs b/scripts/bundle-openclaw.mjs index 307fe5b..838a503 100644 --- a/scripts/bundle-openclaw.mjs +++ b/scripts/bundle-openclaw.mjs @@ -470,7 +470,7 @@ function cleanupBundle(outputDir) { 'node_modules/koffi/src', 'node_modules/koffi/vendor', 'node_modules/koffi/doc', - // Note: extensions/feishu is the built-in Feishu plugin since OpenClaw 2026.4.11 + 'extensions/feishu', // Removed in favor of official @larksuite/openclaw-lark plugin ]; for (const rel of LARGE_REMOVALS) { if (rmSafe(path.join(outputDir, rel))) removedCount++; diff --git a/tests/unit/channel-routes.test.ts b/tests/unit/channel-routes.test.ts index 919a2b0..c434550 100644 --- a/tests/unit/channel-routes.test.ts +++ b/tests/unit/channel-routes.test.ts @@ -44,6 +44,7 @@ vi.mock('@electron/utils/agent-config', () => ({ vi.mock('@electron/utils/plugin-install', () => ({ ensureDingTalkPluginInstalled: vi.fn(), + ensureFeishuPluginInstalled: vi.fn(), ensureWeChatPluginInstalled: vi.fn(), ensureWeComPluginInstalled: vi.fn(), }));