diff --git a/electron/api/routes/channels.ts b/electron/api/routes/channels.ts index d864c5767..29f742086 100644 --- a/electron/api/routes/channels.ts +++ b/electron/api/routes/channels.ts @@ -75,15 +75,15 @@ async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warni 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(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'), - ]; + 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) { @@ -106,6 +106,47 @@ async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warni } } +async function ensureFeishuPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { + const targetDir = join(homedir(), '.openclaw', 'extensions', 'feishu-openclaw-plugin'); + const targetManifest = join(targetDir, 'openclaw.plugin.json'); + + if (existsSync(targetManifest)) { + return { installed: true }; + } + + const candidateSources = app.isPackaged + ? [ + join(process.resourcesPath, 'openclaw-plugins', 'feishu-openclaw-plugin'), + join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'feishu-openclaw-plugin'), + join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'feishu-openclaw-plugin'), + ] + : [ + join(app.getAppPath(), 'build', 'openclaw-plugins', 'feishu-openclaw-plugin'), + join(process.cwd(), 'build', 'openclaw-plugins', 'feishu-openclaw-plugin'), + join(__dirname, '../../../build/openclaw-plugins/feishu-openclaw-plugin'), + ]; + + const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); + if (!sourceDir) { + return { + installed: false, + warning: `Bundled Feishu 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 Feishu plugin mirror (manifest missing).' }; + } + return { installed: true }; + } catch { + return { installed: false, warning: 'Failed to install bundled Feishu plugin mirror' }; + } +} + async function ensureQQBotPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { const targetDir = join(homedir(), '.openclaw', 'extensions', 'qqbot'); const targetManifest = join(targetDir, 'openclaw.plugin.json'); @@ -116,15 +157,15 @@ async function ensureQQBotPluginInstalled(): Promise<{ installed: boolean; warni const candidateSources = app.isPackaged ? [ - join(process.resourcesPath, 'openclaw-plugins', 'qqbot'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'qqbot'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'qqbot'), - ] + join(process.resourcesPath, 'openclaw-plugins', 'qqbot'), + join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'qqbot'), + join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'qqbot'), + ] : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'qqbot'), - join(process.cwd(), 'build', 'openclaw-plugins', 'qqbot'), - join(__dirname, '../../../build/openclaw-plugins/qqbot'), - ]; + join(app.getAppPath(), 'build', 'openclaw-plugins', 'qqbot'), + join(process.cwd(), 'build', 'openclaw-plugins', 'qqbot'), + join(__dirname, '../../../build/openclaw-plugins/qqbot'), + ]; const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); if (!sourceDir) { @@ -223,6 +264,13 @@ export async function handleChannelRoutes( return true; } } + if (body.channelType === 'feishu') { + const installResult = await ensureFeishuPluginInstalled(); + if (!installResult.installed) { + sendJson(res, 500, { success: false, error: installResult.warning || 'Feishu plugin install failed' }); + return true; + } + } await saveChannelConfig(body.channelType, body.config); scheduleGatewayChannelRestart(ctx, `channel:saveConfig:${body.channelType}`); sendJson(res, 200, { success: true }); diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index 6e3ed988d..161708859 100644 --- a/electron/utils/channel-config.ts +++ b/electron/utils/channel-config.ts @@ -99,6 +99,48 @@ export async function saveChannelConfig( ): Promise { const currentConfig = await readOpenClawConfig(); + if (channelType === 'feishu') { + const FEISHU_PLUGIN_ID = 'feishu-openclaw-plugin'; + if (!currentConfig.plugins) { + currentConfig.plugins = { + allow: [FEISHU_PLUGIN_ID], + enabled: true, + entries: { + feishu: { enabled: false }, + [FEISHU_PLUGIN_ID]: { enabled: true } + } + }; + } else { + currentConfig.plugins.enabled = true; + const allow: string[] = Array.isArray(currentConfig.plugins.allow) + ? (currentConfig.plugins.allow as string[]) + : []; + + // Remove legacy 'feishu' plugin from allowlist + const normalizedAllow = allow.filter((pluginId) => pluginId !== 'feishu'); + + if (!normalizedAllow.includes(FEISHU_PLUGIN_ID)) { + currentConfig.plugins.allow = [...normalizedAllow, FEISHU_PLUGIN_ID]; + } else if (normalizedAllow.length !== allow.length) { + currentConfig.plugins.allow = normalizedAllow; + } + + // Explicitly disable the legacy plugin and enable the official one + if (!currentConfig.plugins.entries) { + currentConfig.plugins.entries = {}; + } + if (!currentConfig.plugins.entries['feishu']) { + currentConfig.plugins.entries['feishu'] = {}; + } + currentConfig.plugins.entries['feishu'].enabled = false; + + if (!currentConfig.plugins.entries[FEISHU_PLUGIN_ID]) { + currentConfig.plugins.entries[FEISHU_PLUGIN_ID] = {}; + } + currentConfig.plugins.entries[FEISHU_PLUGIN_ID].enabled = true; + } + } + // 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') { diff --git a/electron/utils/skill-config.ts b/electron/utils/skill-config.ts index 015f4f4f8..3f67ac200 100644 --- a/electron/utils/skill-config.ts +++ b/electron/utils/skill-config.ts @@ -141,12 +141,7 @@ export async function getAllSkillConfigs(): Promise> * ~/.openclaw/skills/ on first launch. These come from the openclaw package's * extensions directory and are available in both dev and packaged builds. */ -const BUILTIN_SKILLS = [ - { slug: 'feishu-doc', sourceExtension: 'feishu' }, - { slug: 'feishu-drive', sourceExtension: 'feishu' }, - { slug: 'feishu-perm', sourceExtension: 'feishu' }, - { slug: 'feishu-wiki', sourceExtension: 'feishu' }, -] as const; +const BUILTIN_SKILLS = [] as const; /** * Ensure built-in skills are deployed to ~/.openclaw/skills//. diff --git a/package.json b/package.json index d81ebcc1f..773409c7c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@larksuite/openclaw-lark": "2026.3.10", "@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 ab9eccb63..341c5c0ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.0.0(jiti@1.21.7)) + '@larksuite/openclaw-lark': + specifier: 2026.3.10 + version: 2026.3.10 '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@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) @@ -1121,6 +1124,9 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@larksuite/openclaw-lark@2026.3.10': + resolution: {integrity: sha512-5XCPcEch2Bab2g284odCkxRCaRMerBezy9Qc6eUfnsO4HvLrqxR406DefTn1mr/ibPVNa3sGxKvjTg3rFXM4mg==} + '@larksuiteoapi/node-sdk@1.59.0': resolution: {integrity: sha512-sBpkruTvZDOxnVtoTbepWKRX0j1Y1ZElQYu0x7+v088sI9pcpbVp6ZzCGn62dhrKPatzNyCJyzYCPXPYQWccrA==} @@ -4297,6 +4303,11 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + 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==} @@ -7938,6 +7949,17 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@larksuite/openclaw-lark@2026.3.10': + dependencies: + '@larksuiteoapi/node-sdk': 1.59.0 + '@sinclair/typebox': 0.34.48 + image-size: 2.0.2 + zod: 4.3.6 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + '@larksuiteoapi/node-sdk@1.59.0': dependencies: axios: 1.13.5(debug@4.4.3) @@ -11655,6 +11677,8 @@ snapshots: ignore@7.0.5: {} + image-size@2.0.2: {} + immediate@3.0.6: {} imurmurhash@0.1.4: {} diff --git a/scripts/bundle-openclaw-plugins.mjs b/scripts/bundle-openclaw-plugins.mjs index 1d3412cf1..0efbcca89 100644 --- a/scripts/bundle-openclaw-plugins.mjs +++ b/scripts/bundle-openclaw-plugins.mjs @@ -38,6 +38,7 @@ const PLUGINS = [ { npmName: '@soimy/dingtalk', pluginId: 'dingtalk' }, { npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' }, { npmName: '@sliverp/qqbot', pluginId: 'qqbot' }, + { npmName: '@larksuite/openclaw-lark', pluginId: 'feishu-openclaw-plugin' }, ]; function getVirtualStoreNodeModules(realPkgPath) { diff --git a/scripts/bundle-openclaw.mjs b/scripts/bundle-openclaw.mjs index a119853a3..436dbc1c1 100644 --- a/scripts/bundle-openclaw.mjs +++ b/scripts/bundle-openclaw.mjs @@ -350,6 +350,7 @@ function cleanupBundle(outputDir) { 'node_modules/koffi/src', 'node_modules/koffi/vendor', 'node_modules/koffi/doc', + '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/src/i18n/locales/zh/channels.json b/src/i18n/locales/zh/channels.json index de8b9422b..339e3c044 100644 --- a/src/i18n/locales/zh/channels.json +++ b/src/i18n/locales/zh/channels.json @@ -167,8 +167,8 @@ ] }, "feishu": { - "description": "通过 WebSocket 连接飞书/Lark 机器人", - "docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/GKn8wOvHnibpPNkNkPzcAvGlnzK#Py88dTltfoJc1jxAhIBcW3Pkn7b", + "description": "通过飞书官方推出的 OpenClaw 插件连接飞书/Lark 机器人", + "docsUrl": "https://www.feishu.cn/content/article/7613711414611463386", "fields": { "appId": { "label": "应用 ID (App ID)", @@ -180,10 +180,11 @@ } }, "instructions": [ - "阅读文档,前往飞书开放平台", - "创建一个新应用", - "获取 App ID 和 App Secret", - "配置事件订阅" + "前往 飞书开放平台 (open.feishu.cn) 并创建企业自建应用", + "在应用详情页获取 App ID 和 App Secret 并填入下方", + "确保应用已开通“机器人”能力", + "保存配置后,根据网关提示扫码完成机器人创建", + "详细步骤请参考飞书官方文档" ] }, "wecom": { diff --git a/src/pages/Channels/index.tsx b/src/pages/Channels/index.tsx index 6d6a2ab38..634bd0bd2 100644 --- a/src/pages/Channels/index.tsx +++ b/src/pages/Channels/index.tsx @@ -277,7 +277,7 @@ function ChannelCard({ channel, onClick, onDelete }: ChannelCardProps) { const meta = CHANNEL_META[channel.type]; return ( -