From 832370f6e2fd494a21c0829ef6d72bc2a171a851 Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 18 Mar 2026 23:03:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Feishu=20(=E9=A3=9E=E4=B9=A6/Lark?= =?UTF-8?q?)=20Desktop=20adapter=20via=20AppleScript=20(5=20commands)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feishu uses custom 'Lark Framework' (Chromium-based but NOT Electron). CDP port test failed — --remote-debugging-port has no effect. Uses AppleScript + clipboard approach (same as WeChat/ChatGPT). Commands: status, send, read, search (Cmd+K), new (Cmd+N) Includes adapter READMEs (EN+ZH). --- src/clis/feishu/README.md | 20 ++++++++++++++ src/clis/feishu/README.zh-CN.md | 20 ++++++++++++++ src/clis/feishu/new.ts | 32 ++++++++++++++++++++++ src/clis/feishu/read.ts | 48 +++++++++++++++++++++++++++++++++ src/clis/feishu/search.ts | 35 ++++++++++++++++++++++++ src/clis/feishu/send.ts | 46 +++++++++++++++++++++++++++++++ src/clis/feishu/status.ts | 34 +++++++++++++++++++++++ 7 files changed, 235 insertions(+) create mode 100644 src/clis/feishu/README.md create mode 100644 src/clis/feishu/README.zh-CN.md create mode 100644 src/clis/feishu/new.ts create mode 100644 src/clis/feishu/read.ts create mode 100644 src/clis/feishu/search.ts create mode 100644 src/clis/feishu/send.ts create mode 100644 src/clis/feishu/status.ts diff --git a/src/clis/feishu/README.md b/src/clis/feishu/README.md new file mode 100644 index 0000000..ea7848f --- /dev/null +++ b/src/clis/feishu/README.md @@ -0,0 +1,20 @@ +# Feishu (飞书/Lark) Desktop Adapter + +Control **Feishu/Lark Desktop** from the terminal via AppleScript. + +> **Note:** Feishu uses a custom `Lark Framework` (Chromium-based but NOT Electron). CDP is not available, so this adapter uses AppleScript + clipboard. + +## Prerequisites + +1. Feishu/Lark must be running and logged in +2. Terminal must have **Accessibility permission** + +## Commands + +| Command | Description | +|---------|-------------| +| `feishu status` | Check if Feishu/Lark is running | +| `feishu send "msg"` | Send message in active chat (paste + Enter) | +| `feishu read` | Read current chat (Cmd+A → Cmd+C) | +| `feishu search "query"` | Global search (Cmd+K) | +| `feishu new` | New message/document (Cmd+N) | diff --git a/src/clis/feishu/README.zh-CN.md b/src/clis/feishu/README.zh-CN.md new file mode 100644 index 0000000..2bf42b5 --- /dev/null +++ b/src/clis/feishu/README.zh-CN.md @@ -0,0 +1,20 @@ +# 飞书 (Lark) 桌面端适配器 + +通过 AppleScript 在终端中控制**飞书桌面端**。 + +> **注意:** 飞书使用自研 `Lark Framework`(基于 Chromium 但非 Electron),不支持 CDP。 + +## 前置条件 + +1. 飞书必须正在运行且已登录 +2. Terminal 需要辅助功能权限 + +## 命令 + +| 命令 | 说明 | +|------|------| +| `feishu status` | 检查飞书是否在运行 | +| `feishu send "消息"` | 发送消息(粘贴 + 回车) | +| `feishu read` | 读取当前聊天(Cmd+A → Cmd+C) | +| `feishu search "关键词"` | 全局搜索(Cmd+K) | +| `feishu new` | 新建消息/文档(Cmd+N) | diff --git a/src/clis/feishu/new.ts b/src/clis/feishu/new.ts new file mode 100644 index 0000000..d847325 --- /dev/null +++ b/src/clis/feishu/new.ts @@ -0,0 +1,32 @@ +import { execSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const newCommand = cli({ + site: 'feishu', + name: 'new', + description: 'Create a new message or document in Feishu', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [], + columns: ['Status'], + func: async (page: IPage | null) => { + try { + execSync("osascript -e 'tell application \"Lark\" to activate'"); + execSync("osascript -e 'delay 0.3'"); + + // Cmd+N for new conversation/document + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'keystroke \"n\" using command down' " + + "-e 'end tell'" + ); + + return [{ Status: 'New item dialog opened (Cmd+N)' }]; + } catch (err: any) { + return [{ Status: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/feishu/read.ts b/src/clis/feishu/read.ts new file mode 100644 index 0000000..fb60c7f --- /dev/null +++ b/src/clis/feishu/read.ts @@ -0,0 +1,48 @@ +import { execSync, spawnSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const readCommand = cli({ + site: 'feishu', + name: 'read', + description: 'Read the current chat content by selecting all and copying', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [], + columns: ['Content'], + func: async (page: IPage | null) => { + try { + let clipBackup = ''; + try { + clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); + } catch { /* empty */ } + + execSync("osascript -e 'tell application \"Lark\" to activate'"); + execSync("osascript -e 'delay 0.3'"); + + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'keystroke \"a\" using command down' " + + "-e 'delay 0.2' " + + "-e 'keystroke \"c\" using command down' " + + "-e 'delay 0.2' " + + "-e 'end tell'" + ); + + const content = execSync('pbpaste', { encoding: 'utf-8' }).trim(); + + if (clipBackup) { + spawnSync('pbcopy', { input: clipBackup }); + } + + // Deselect + execSync("osascript -e 'tell application \"System Events\" to key code 53'"); + + return [{ Content: content || '(no content captured)' }]; + } catch (err: any) { + return [{ Content: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/feishu/search.ts b/src/clis/feishu/search.ts new file mode 100644 index 0000000..e9c5f4b --- /dev/null +++ b/src/clis/feishu/search.ts @@ -0,0 +1,35 @@ +import { execSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const searchCommand = cli({ + site: 'feishu', + name: 'search', + description: 'Open Feishu global search and type a query (Cmd+K)', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [{ name: 'query', required: true, positional: true, help: 'Search query' }], + columns: ['Status'], + func: async (page: IPage | null, kwargs: any) => { + const query = kwargs.query as string; + try { + execSync("osascript -e 'tell application \"Lark\" to activate'"); + execSync("osascript -e 'delay 0.3'"); + + // Feishu uses Cmd+K for global search (similar to Slack/Notion) + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'keystroke \"k\" using command down' " + + "-e 'delay 0.5' " + + `-e 'keystroke ${JSON.stringify(query)}' ` + + "-e 'end tell'" + ); + + return [{ Status: `Searching for: ${query}` }]; + } catch (err: any) { + return [{ Status: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/feishu/send.ts b/src/clis/feishu/send.ts new file mode 100644 index 0000000..942d67e --- /dev/null +++ b/src/clis/feishu/send.ts @@ -0,0 +1,46 @@ +import { execSync, spawnSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const sendCommand = cli({ + site: 'feishu', + name: 'send', + description: 'Send a message in the active Feishu (Lark) conversation', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }], + columns: ['Status'], + func: async (page: IPage | null, kwargs: any) => { + const text = kwargs.text as string; + try { + // Backup clipboard + let clipBackup = ''; + try { + clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); + } catch { /* empty */ } + + spawnSync('pbcopy', { input: text }); + + execSync("osascript -e 'tell application \"Lark\" to activate'"); + execSync("osascript -e 'delay 0.5'"); + + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'keystroke \"v\" using command down' " + + "-e 'delay 0.2' " + + "-e 'keystroke return' " + + "-e 'end tell'" + ); + + if (clipBackup) { + spawnSync('pbcopy', { input: clipBackup }); + } + + return [{ Status: 'Success' }]; + } catch (err: any) { + return [{ Status: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/feishu/status.ts b/src/clis/feishu/status.ts new file mode 100644 index 0000000..fe4be46 --- /dev/null +++ b/src/clis/feishu/status.ts @@ -0,0 +1,34 @@ +import { execSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const statusCommand = cli({ + site: 'feishu', + name: 'status', + description: 'Check if Feishu (Lark) Desktop is running on macOS', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [], + columns: ['Status', 'Detail'], + func: async (page: IPage | null) => { + try { + const running = execSync("osascript -e 'application \"Lark\" is running'", { encoding: 'utf-8' }).trim(); + if (running !== 'true') { + return [{ Status: 'Stopped', Detail: 'Feishu/Lark is not running' }]; + } + + const windowCount = execSync( + "osascript -e 'tell application \"System Events\" to count windows of application process \"Lark\"'", + { encoding: 'utf-8' } + ).trim(); + + return [{ + Status: 'Running', + Detail: `${windowCount} window(s) open`, + }]; + } catch (err: any) { + return [{ Status: 'Error', Detail: err.message }]; + } + }, +});