From fc9fc32d14a90ee0b931f838910daf3b84382fe8 Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 18 Mar 2026 22:51:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20WeChat=20(=E5=BE=AE=E4=BF=A1)=20D?= =?UTF-8?q?esktop=20adapter=20via=20AppleScript=20(6=20commands)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WeChat is a native macOS app (not Electron), so uses AppleScript + clipboard: - status: check if running + window count - send: clipboard paste + Enter - read: Cmd+A → Cmd+C with clipboard backup/restore - search: Cmd+F + type query - chats: switch to chats tab (Cmd+1) - contacts: switch to contacts tab (Cmd+2) Includes adapter READMEs (EN+ZH). Total: 30 sites · 157 commands --- src/clis/wechat/README.md | 28 +++++++++++++ src/clis/wechat/README.zh-CN.md | 28 +++++++++++++ src/clis/wechat/chats.ts | 33 +++++++++++++++ src/clis/wechat/contacts.ts | 33 +++++++++++++++ src/clis/wechat/read.ts | 72 +++++++++++++++++++++++++++++++++ src/clis/wechat/search.ts | 36 +++++++++++++++++ src/clis/wechat/send.ts | 49 ++++++++++++++++++++++ src/clis/wechat/status.ts | 35 ++++++++++++++++ 8 files changed, 314 insertions(+) create mode 100644 src/clis/wechat/README.md create mode 100644 src/clis/wechat/README.zh-CN.md create mode 100644 src/clis/wechat/chats.ts create mode 100644 src/clis/wechat/contacts.ts create mode 100644 src/clis/wechat/read.ts create mode 100644 src/clis/wechat/search.ts create mode 100644 src/clis/wechat/send.ts create mode 100644 src/clis/wechat/status.ts diff --git a/src/clis/wechat/README.md b/src/clis/wechat/README.md new file mode 100644 index 0000000..b5c8466 --- /dev/null +++ b/src/clis/wechat/README.md @@ -0,0 +1,28 @@ +# WeChat (微信) Desktop Adapter + +Control **WeChat Mac Desktop** from the terminal via AppleScript + Accessibility API. + +> **Note:** WeChat is a native macOS app (not Electron), so CDP is not available. This adapter uses AppleScript keyboard simulation and clipboard operations. + +## Prerequisites + +1. WeChat must be running and logged in +2. Terminal must have **Accessibility permission** (System Settings → Privacy & Security → Accessibility) + +## Commands + +| Command | Description | +|---------|-------------| +| `wechat status` | Check if WeChat is running | +| `wechat send "msg"` | Send message in the active chat (clipboard paste + Enter) | +| `wechat read` | Read current chat content (Cmd+A → Cmd+C) | +| `wechat search "keyword"` | Open search and type a query (Cmd+F) | +| `wechat chats` | Switch to Chats tab (Cmd+1) | +| `wechat contacts` | Switch to Contacts tab (Cmd+2) | + +## Limitations + +- **No CDP support** — WeChat is native Cocoa, not Electron +- `send` requires the correct conversation to be already open +- `read` captures whatever is visible via select-all + copy +- `search` types the query but cannot programmatically click results diff --git a/src/clis/wechat/README.zh-CN.md b/src/clis/wechat/README.zh-CN.md new file mode 100644 index 0000000..a70860b --- /dev/null +++ b/src/clis/wechat/README.zh-CN.md @@ -0,0 +1,28 @@ +# 微信桌面端适配器 + +通过 AppleScript + Accessibility API 在终端中控制**微信 Mac 桌面端**。 + +> **注意:** 微信是原生 macOS 应用(非 Electron),因此无法使用 CDP。此适配器使用 AppleScript 键盘模拟和剪贴板操作。 + +## 前置条件 + +1. 微信必须正在运行且已登录 +2. Terminal 需要 **辅助功能权限**(系统设置 → 隐私与安全性 → 辅助功能) + +## 命令 + +| 命令 | 说明 | +|------|------| +| `wechat status` | 检查微信是否在运行 | +| `wechat send "消息"` | 发送消息(剪贴板粘贴 + 回车) | +| `wechat read` | 读取当前聊天内容(Cmd+A → Cmd+C) | +| `wechat search "关键词"` | 打开搜索并输入关键词(Cmd+F) | +| `wechat chats` | 切换到聊天列表(Cmd+1) | +| `wechat contacts` | 切换到通讯录(Cmd+2) | + +## 限制 + +- **不支持 CDP** — 微信是原生 Cocoa 应用 +- `send` 需要先手动打开正确的会话 +- `read` 通过全选+复制来获取可见内容 +- `search` 可以输入关键词但无法自动点击结果 diff --git a/src/clis/wechat/chats.ts b/src/clis/wechat/chats.ts new file mode 100644 index 0000000..e577028 --- /dev/null +++ b/src/clis/wechat/chats.ts @@ -0,0 +1,33 @@ +import { execSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const chatsCommand = cli({ + site: 'wechat', + name: 'chats', + description: 'Open the WeChat chats panel (conversation list)', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [], + columns: ['Status'], + func: async (page: IPage | null) => { + try { + // Activate WeChat + execSync("osascript -e 'tell application \"WeChat\" to activate'"); + execSync("osascript -e 'delay 0.3'"); + + // Cmd+1 switches to Chats tab in WeChat Mac + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'keystroke \"1\" using command down' " + + "-e 'end tell'" + ); + + return [{ Status: 'Chats panel opened (Cmd+1)' }]; + } catch (err: any) { + return [{ Status: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/wechat/contacts.ts b/src/clis/wechat/contacts.ts new file mode 100644 index 0000000..c4ba6df --- /dev/null +++ b/src/clis/wechat/contacts.ts @@ -0,0 +1,33 @@ +import { execSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const contactsCommand = cli({ + site: 'wechat', + name: 'contacts', + description: 'Open the WeChat contacts panel', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [], + columns: ['Status'], + func: async (page: IPage | null) => { + try { + // Activate WeChat + execSync("osascript -e 'tell application \"WeChat\" to activate'"); + execSync("osascript -e 'delay 0.3'"); + + // Cmd+2 switches to Contacts tab in WeChat Mac + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'keystroke \"2\" using command down' " + + "-e 'end tell'" + ); + + return [{ Status: 'Contacts panel opened (Cmd+2)' }]; + } catch (err: any) { + return [{ Status: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/wechat/read.ts b/src/clis/wechat/read.ts new file mode 100644 index 0000000..5c0a46a --- /dev/null +++ b/src/clis/wechat/read.ts @@ -0,0 +1,72 @@ +import { execSync, spawnSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const readCommand = cli({ + site: 'wechat', + 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 { + // Backup clipboard + let clipBackup = ''; + try { + clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); + } catch { /* empty */ } + + // Activate WeChat + execSync("osascript -e 'tell application \"WeChat\" to activate'"); + execSync("osascript -e 'delay 0.3'"); + + // Click on the chat area first, then select all and copy + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'tell application process \"WeChat\"' " + + // Click in the message area (center-right of the window) + "-e 'set frontWin to front window' " + + "-e 'set winPos to position of frontWin' " + + "-e 'set winSize to size of frontWin' " + + "-e 'end tell' " + + "-e 'end tell'" + ); + + execSync("osascript -e 'delay 0.2'"); + + // Select all text in chat area and copy + 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(); + + // Restore clipboard + if (clipBackup) { + spawnSync('pbcopy', { input: clipBackup }); + } + + // Press Escape to deselect + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'key code 53' " + // Escape + "-e 'end tell'" + ); + + return [{ Content: content || '(no content captured)' }]; + } catch (err: any) { + return [{ Content: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/wechat/search.ts b/src/clis/wechat/search.ts new file mode 100644 index 0000000..916e27b --- /dev/null +++ b/src/clis/wechat/search.ts @@ -0,0 +1,36 @@ +import { execSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const searchCommand = cli({ + site: 'wechat', + name: 'search', + description: 'Open WeChat search and type a query (find contacts or messages)', + domain: 'localhost', + strategy: Strategy.PUBLIC, + browser: false, + args: [{ name: 'query', required: true, positional: true, help: 'Search query (contact name or keyword)' }], + columns: ['Status'], + func: async (page: IPage | null, kwargs: any) => { + const query = kwargs.query as string; + try { + // Activate WeChat + execSync("osascript -e 'tell application \"WeChat\" to activate'"); + execSync("osascript -e 'delay 0.3'"); + + // Cmd+F to open search (WeChat Mac uses Cmd+F for search) + execSync( + "osascript " + + "-e 'tell application \"System Events\"' " + + "-e 'keystroke \"f\" 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/wechat/send.ts b/src/clis/wechat/send.ts new file mode 100644 index 0000000..a293113 --- /dev/null +++ b/src/clis/wechat/send.ts @@ -0,0 +1,49 @@ +import { execSync, spawnSync } from 'node:child_process'; +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const sendCommand = cli({ + site: 'wechat', + name: 'send', + description: 'Send a message in the active WeChat conversation via clipboard paste', + 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 { /* clipboard may be empty */ } + + // Copy text to clipboard + spawnSync('pbcopy', { input: text }); + + // Activate WeChat and paste + execSync("osascript -e 'tell application \"WeChat\" 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'" + ); + + // Restore clipboard + if (clipBackup) { + spawnSync('pbcopy', { input: clipBackup }); + } + + return [{ Status: 'Success' }]; + } catch (err: any) { + return [{ Status: 'Error: ' + err.message }]; + } + }, +}); diff --git a/src/clis/wechat/status.ts b/src/clis/wechat/status.ts new file mode 100644 index 0000000..d9fb4db --- /dev/null +++ b/src/clis/wechat/status.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 statusCommand = cli({ + site: 'wechat', + name: 'status', + description: 'Check if WeChat 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 \"WeChat\" is running'", { encoding: 'utf-8' }).trim(); + if (running !== 'true') { + return [{ Status: 'Stopped', Detail: 'WeChat is not running' }]; + } + + // Get window count to check if logged in + const windowCount = execSync( + "osascript -e 'tell application \"System Events\" to count windows of application process \"WeChat\"'", + { encoding: 'utf-8' } + ).trim(); + + return [{ + Status: 'Running', + Detail: `${windowCount} window(s) open`, + }]; + } catch (err: any) { + return [{ Status: 'Error', Detail: err.message }]; + } + }, +});