feat: add WeChat (微信) Desktop adapter via AppleScript (6 commands)

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
This commit is contained in:
jackwener committed 2026-03-18 22:51:18 +08:00
1 parent 43b753fa02
commit fc9fc32d14
8 files changed
+314

No files matched your search

+28
View File
@@ -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
+28
View File
@@ -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` 可以输入关键词但无法自动点击结果
+33
View File
@@ -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 }];
}
},
});
+33
View File
@@ -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 }];
}
},
});
+72
View File
@@ -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 }];
}
},
});
+36
View File
@@ -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 }];
}
},
});
+49
View File
@@ -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 }];
}
},
});
+35
View File
@@ -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 }];
}
},
});