feat: support feishu official plugin (#418)
This commit is contained in:
committed by
GitHub
Unverified
parent
f37d2ac112
commit
ce7e890509
@@ -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 });
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>/.
|
||||
|
||||
@@ -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
24
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user