feat: add Feishu (飞书) adapter + Notion favorites command

Feishu/Lark (5 commands via AppleScript):
- status, send, new, search (Cmd+K), read
- Lark Framework wraps Chromium v131 but doesn't expose CDP
- Uses AppleScript + clipboard automation (same as WeChat/ChatGPT)

Notion:
- Added favorites command (list pages from Favorites section)

Total: 32 sites · 162 commands
This commit is contained in:
jackwener committed 2026-03-18 21:19:47 +08:00
1 parent 40a6a4cace
commit b1fda7da3b
9 files changed
+212 -4

No files matched your search

+3 -2
View File
@@ -143,7 +143,7 @@ npm install -g @jackwener/opencli@latest
## Built-in Commands
**30 sites · 156 commands** — run `opencli list` for the live registry.
**32 sites · 162 commands** — run `opencli list` for the live registry.
| Site | Commands | Count | Mode |
|------|----------|:-----:|------|
@@ -153,7 +153,7 @@ npm install -g @jackwener/opencli@latest
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` | 11 | 🔐 Browser |
| **codex** | `status` `send` `read` `new` `extract-diff` `model` `ask` `screenshot` `history` `export` | 10 | 🖥️ Desktop |
| **chatwise** | `status` `new` `send` `read` `ask` `model` `history` `export` `screenshot` | 9 | 🖥️ Desktop |
| **notion** | `status` `search` `read` `new` `write` `sidebar` `export` | 7 | 🖥️ Desktop |
| **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` | 8 | 🖥️ Desktop |
| **discord** | `status` `send` `read` `channels` `servers` `search` `members` | 7 | 🖥️ Desktop |
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 Browser |
@@ -161,6 +161,7 @@ npm install -g @jackwener/opencli@latest
| **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 5 | 🔐 Browser |
| **chatgpt** | `status` `new` `send` `read` `ask` | 5 | 🖥️ Desktop |
| **wechat** | `status` `send` `new` `search` `read` | 5 | 🖥️ Desktop |
| **feishu** | `status` `send` `new` `search` `read` | 5 | 🖥️ Desktop |
| **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` | 3 | 🌐 Public |
| **youtube** | `search` `video` `transcript` | 3 | 🔐 Browser |
| **zhihu** | `hot` `search` `question` | 3 | 🔐 Browser |
+3 -2
View File
@@ -144,7 +144,7 @@ npm install -g @jackwener/opencli@latest
## 内置命令
**30 个站点 · 156 命令** — 运行 `opencli list` 查看完整注册表。
**32 个站点 · 162 命令** — 运行 `opencli list` 查看完整注册表。
| 站点 | 命令 | 数量 | 模式 |
|------|------|:----:|------|
@@ -154,13 +154,14 @@ npm install -g @jackwener/opencli@latest
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` | 11 | 🔐 浏览器 |
| **codex** | `status` `send` `read` `new` `extract-diff` `model` `ask` `screenshot` `history` `export` | 10 | 🖥️ 桌面端 |
| **chatwise** | `status` `new` `send` `read` `ask` `model` `history` `export` `screenshot` | 9 | 🖥️ 桌面端 |
| **notion** | `status` `search` `read` `new` `write` `sidebar` `export` | 7 | 🖥️ 桌面端 |
| **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` | 8 | 🖥️ 桌面端 |
| **discord** | `status` `send` `read` `channels` `servers` `search` `members` | 7 | 🖥️ 桌面端 |
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 浏览器 |
| **antigravity** | `status` `send` `read` `new` `evaluate` | 5 | 🖥️ 桌面端 |
| **chatgpt** | `status` `new` `send` `read` `ask` | 5 | 🖥️ 桌面端 |
| **wechat** | `status` `send` `new` `search` `read` | 5 | 🖥️ 桌面端 |
| **feishu** | `status` `send` `new` `search` `read` | 5 | 🖥️ 桌面端 |
| **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 5 | 🔐 浏览器 |
| **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` | 3 | 🌐 公开 |
| **youtube** | `search` `video` `transcript` | 3 | 🔐 浏览器 |
+21
View File
@@ -0,0 +1,21 @@
# Feishu (飞书/Lark) Desktop Adapter
Control the **Feishu Desktop App** from the terminal via macOS AppleScript automation.
> **Note:** Feishu/Lark uses a custom `Lark Framework` (Chromium v131 embedded) but does **not** expose CDP. This adapter uses AppleScript + clipboard instead.
## Commands
| Command | Description |
|---------|-------------|
| `feishu status` | Check if Feishu is running |
| `feishu send "message"` | Send a message in the active conversation |
| `feishu new` | Start a new chat (Cmd+N) |
| `feishu search "query"` | Open global search (Cmd+K) |
| `feishu read` | Copy recent messages from the active chat |
## Requirements
- macOS only (uses AppleScript)
- **Accessibility permissions** must be granted to your terminal app
- Feishu must be running with a conversation window open
+21
View File
@@ -0,0 +1,21 @@
# 飞书桌面端适配器
通过 macOS AppleScript 在终端中控制 **飞书桌面应用**。
> **注意:** 飞书使用自研 `Lark Framework`(内嵌 Chromium v131),但不暴露 CDP。因此使用 AppleScript + 剪贴板方式。
## 命令
| 命令 | 说明 |
|------|------|
| `feishu status` | 检查飞书是否在运行 |
| `feishu send "消息"` | 在当前对话窗口发送消息 |
| `feishu new` | 开始新对话(Cmd+N) |
| `feishu search "关键词"` | 打开全局搜索(Cmd+K) |
| `feishu read` | 复制当前对话的聊天记录 |
## 前置条件
- 仅 macOS(使用 AppleScript)
- 需要为终端应用开启**辅助功能权限**
- 飞书必须打开且有对话窗口
+24
View File
@@ -0,0 +1,24 @@
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: 'Start a new chat in Feishu (Cmd+N)',
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'");
execSync("osascript -e 'tell application \"System Events\" to keystroke \"n\" using command down'");
return [{ Status: 'Success' }];
} catch (err: any) {
return [{ Status: 'Error: ' + err.message }];
}
},
});
+46
View File
@@ -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 readCommand = cli({
site: 'feishu',
name: 'read',
description: 'Copy recent messages from the active Feishu conversation (Cmd+A → Cmd+C)',
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 {}
spawnSync('pbcopy', { input: '__OPENCLI_EMPTY__' });
execSync("osascript -e 'tell application \"Lark\" to activate'");
execSync("osascript -e 'delay 0.3'");
const cmd = "osascript " +
"-e 'tell application \"System Events\"' " +
"-e 'keystroke \"a\" using command down' " +
"-e 'delay 0.2' " +
"-e 'keystroke \"c\" using command down' " +
"-e 'end tell'";
execSync(cmd);
execSync("osascript -e 'delay 0.3'");
const result = execSync('pbpaste', { encoding: 'utf-8' }).trim();
if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
if (!result || result === '__OPENCLI_EMPTY__') {
return [{ Content: 'Could not copy chat content. Try clicking in the message area first.' }];
}
return [{ Content: result }];
} catch (err: any) {
return [{ Content: 'Error: ' + err.message }];
}
},
});
+32
View File
@@ -0,0 +1,32 @@
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 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\" to keystroke \"k\" using command down'");
execSync("osascript -e 'delay 0.5'");
const escaped = query.replace(/'/g, "'\\''");
execSync(`osascript -e 'tell application "System Events" to keystroke "${escaped}"'`);
return [{ Status: `Searched for: ${query}` }];
} catch (err: any) {
return [{ Status: 'Error: ' + err.message }];
}
},
});
+40
View File
@@ -0,0 +1,40 @@
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 conversation window',
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 {
let clipBackup = '';
try { clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); } catch {}
spawnSync('pbcopy', { input: text });
execSync("osascript -e 'tell application \"Lark\" to activate'");
execSync("osascript -e 'delay 0.5'");
const cmd = "osascript " +
"-e 'tell application \"System Events\"' " +
"-e 'keystroke \"v\" using command down' " +
"-e 'delay 0.2' " +
"-e 'keystroke return' " +
"-e 'end tell'";
execSync(cmd);
if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
return [{ Status: 'Success' }];
} catch (err: any) {
return [{ Status: 'Error: ' + err.message }];
}
},
});
+22
View File
@@ -0,0 +1,22 @@
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 App is running on macOS',
domain: 'localhost',
strategy: Strategy.PUBLIC,
browser: false,
args: [],
columns: ['Status'],
func: async (page: IPage | null) => {
try {
const output = execSync("osascript -e 'application \"Lark\" is running'", { encoding: 'utf-8' }).trim();
return [{ Status: output === 'true' ? 'Running' : 'Stopped' }];
} catch {
return [{ Status: 'Error querying application state' }];
}
},
});