feat: support feishu official plugin (#418)

This commit is contained in:
paisley
2026-03-11 18:18:46 +08:00
committed by GitHub
Unverified
parent f37d2ac112
commit ce7e890509
9 changed files with 142 additions and 29 deletions

View File

@@ -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 });

View File

@@ -99,6 +99,48 @@ export async function saveChannelConfig(
): Promise<void> {
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') {

View File

@@ -141,12 +141,7 @@ export async function getAllSkillConfigs(): Promise<Record<string, SkillEntry>>
* ~/.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/<slug>/.

View File

@@ -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",

24
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -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) {

View File

@@ -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++;

View File

@@ -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": {

View File

@@ -277,7 +277,7 @@ function ChannelCard({ channel, onClick, onDelete }: ChannelCardProps) {
const meta = CHANNEL_META[channel.type];
return (
<div
<div
onClick={onClick}
className="group flex items-start gap-4 p-4 rounded-2xl transition-all text-left border relative overflow-hidden bg-transparent border-transparent hover:bg-black/5 dark:hover:bg-white/5 cursor-pointer"
>