diff --git a/README.md b/README.md index 1fe5267..e77fad9 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index 723bd27..73e3e48 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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 | 🔐 浏览器 | diff --git a/src/clis/feishu/README.md b/src/clis/feishu/README.md new file mode 100644 index 0000000..a0ba95c --- /dev/null +++ b/src/clis/feishu/README.md @@ -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 diff --git a/src/clis/feishu/README.zh-CN.md b/src/clis/feishu/README.zh-CN.md new file mode 100644 index 0000000..284a140 --- /dev/null +++ b/src/clis/feishu/README.zh-CN.md @@ -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) +- 需要为终端应用开启**辅助功能权限** +- 飞书必须打开且有对话窗口 diff --git a/src/clis/feishu/new.ts b/src/clis/feishu/new.ts new file mode 100644 index 0000000..6f426f2 --- /dev/null +++ b/src/clis/feishu/new.ts @@ -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 }]; + } + }, +}); diff --git a/src/clis/feishu/read.ts b/src/clis/feishu/read.ts new file mode 100644 index 0000000..4f71061 --- /dev/null +++ b/src/clis/feishu/read.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 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 }]; + } + }, +}); diff --git a/src/clis/feishu/search.ts b/src/clis/feishu/search.ts new file mode 100644 index 0000000..5f13dd1 --- /dev/null +++ b/src/clis/feishu/search.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 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 }]; + } + }, +}); diff --git a/src/clis/feishu/send.ts b/src/clis/feishu/send.ts new file mode 100644 index 0000000..94df63b --- /dev/null +++ b/src/clis/feishu/send.ts @@ -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 }]; + } + }, +}); diff --git a/src/clis/feishu/status.ts b/src/clis/feishu/status.ts new file mode 100644 index 0000000..a33d97f --- /dev/null +++ b/src/clis/feishu/status.ts @@ -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' }]; + } + }, +});