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:
1 parent
43b753fa02
commit
fc9fc32d14
8 files changed
+314
No files matched your search
@@ -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
|
||||
@@ -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` 可以输入关键词但无法自动点击结果
|
||||
@@ -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 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user