Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
# npm 10 bundled with Node 22 crashes resolving Vitest's peer dependencies.
# https://github.com/npm/cli/issues/9787
- run: npm install --global npm@11.18.0
- run: npm install
- run: npm run build
- run: npx vitest run
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/wechat-codex.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: WeChat Codex skill

on:
pull_request:
paths: ['skills/wechat-codex/**', '.github/workflows/wechat-codex.yml', 'README.md', 'README.EN.MD']
push:
branches: [main]
paths: ['skills/wechat-codex/**', '.github/workflows/wechat-codex.yml']

permissions:
contents: read

jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: node --test skills/wechat-codex/tests/*.test.mjs
- run: python3 -m unittest discover -s skills/wechat-codex/tests -p 'test_*.py'
- name: Scaffold and install the standalone bridge
run: |
python3 skills/wechat-codex/scripts/setup.py --target "$RUNNER_TEMP/wechat-codex"
npm --prefix "$RUNNER_TEMP/wechat-codex" ci --ignore-scripts --no-audit --no-fund
node --check "$RUNNER_TEMP/wechat-codex/codex-wechat.mjs"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ tmp/
next-env.d.ts
target/
package-lock.json
!skills/wechat-codex/assets/bridge/package-lock.json
__pycache__/
*.pyc
*.egg-info/
Expand Down
33 changes: 33 additions & 0 deletions README.EN.MD
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,37 @@ pi install npm:@wechatbot/pi-agent

See [pi-agent/README.md](pi-agent/README.md) for details.

## 🤖 Codex WeChat Skill

[wechat-codex](skills/wechat-codex/SKILL.md) connects WeChat to your local Codex CLI. Scan the QR code, send text tasks, continue the same session, and receive results in WeChat. It reuses your Codex login without requiring Pi, OpenClaw, or SDK core changes.

Requires **Node.js 22+, Python 3, and an authenticated Codex CLI** supporting `exec`, `exec resume`, and `--ignore-user-config`. The bridge currently targets **macOS / Linux**. Keep the host running and connected to the internet.

First-time skill installation, from the repository root:

```bash
skill_target="${CODEX_HOME:-$HOME/.codex}/skills/wechat-codex"
test ! -e "$skill_target" && mkdir -p "$(dirname "$skill_target")" && cp -R skills/wechat-codex "$skill_target"
```

In a Codex task that discovers the skill, ask:

> Use $wechat-codex to connect Codex to WeChat.

The skill checks prerequisites, prepares a standalone bridge directory, displays the QR code, and verifies login and replies. You still scan and confirm on your phone. To run the bridge directly:

```bash
python3 skills/wechat-codex/scripts/setup.py --target ./wechat-codex-local
npm --prefix ./wechat-codex-local ci --ignore-scripts --no-audit --no-fund
node ./wechat-codex-local/codex-wechat.mjs
```

WeChat commands: `/help`, `/status`, `/new`, `/cancel`, `/stop`. Tasks use the bridge's `codex-workspace` directory by default; set `WECHAT_CODEX_WORKDIR` to select a project.

The first task creates an **independent Codex session**. Follow-ups resume its exact ID, shown by `/status`; open it locally with `codex resume <SESSION_ID>`. In Codex App environments exposing task-navigation tools, the skill can also open the session by ID. Avoid simultaneous local and WeChat submissions to the same session.

This version supports text input/replies, paired-sender filtering, one task at a time, a ten-minute execution limit, and bounded login retries. It uses the `workspace-write` sandbox; operations requiring additional approval cannot be approved through the bridge. WeChat credentials stay in memory. Restarting requires a new scan and starts a new conversation; Codex's saved history remains on disk. Claude Code is not included in this skill yet.

## ✨ Features

All SDKs share the following capabilities:
Expand Down Expand Up @@ -209,6 +240,7 @@ graph TD
| [golang/README.md](golang/README.md) | Go SDK Documentation |
| [rust/README.md](rust/README.md) | Rust SDK Documentation |
| [pi-agent/README.md](pi-agent/README.md) | Pi Extension (WeChat ↔ Pi Bridge) |
| [skills/wechat-codex/SKILL.md](skills/wechat-codex/SKILL.md) | Codex WeChat skill, runtime scripts, and troubleshooting |

## 🌐 Website

Expand All @@ -233,6 +265,7 @@ wechatbot/
│ └── src/ # 6 modules
├── pi-agent/ # Pi Extension (WeChat ↔ Pi Bridge)
│ └── src/ # Extension entry & WeChat client
├── skills/wechat-codex/ # Codex WeChat skill and standalone bridge template
└── docs/ # Shared documentation
├── protocol.md # iLink API Protocol Spec
└── architecture.md # Architecture & SDK Comparison
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ graph TD
| [golang/README.md](golang/README.md) | Go SDK 文档 |
| [rust/README.md](rust/README.md) | Rust SDK 文档 |
| [pi-agent/README.md](pi-agent/README.md) | Pi 扩展文档(微信 ↔ Pi 桥接) |
| [skills/wechat-codex/SKILL.md](skills/wechat-codex/SKILL.md) | Codex 微信接入 Skill、运行脚本与排障 |

## 🌐 网站

Expand All @@ -172,6 +173,37 @@ pi install npm:@wechatbot/pi-agent
/wechat # 显示二维码 → 微信扫码 → 连接成功!
```

## 🤖 Codex 微信 Skill

[wechat-codex](skills/wechat-codex/SKILL.md) 将微信连接到本机 Codex CLI:扫码后可以发送文字任务、继续同一个会话,并接收执行结果。它复用已有的 Codex 登录,不需要安装 Pi 或 OpenClaw,也不修改 SDK 核心。

要求 **Node.js 22+、Python 3、已登录的 Codex CLI**(支持 `exec`、`exec resume` 和 `--ignore-user-config`)。桥接进程目前面向 **macOS / Linux**,电脑需要保持开机和联网。

在仓库根目录首次安装 skill:

```bash
skill_target="${CODEX_HOME:-$HOME/.codex}/skills/wechat-codex"
test ! -e "$skill_target" && mkdir -p "$(dirname "$skill_target")" && cp -R skills/wechat-codex "$skill_target"
```

然后在能够发现该 skill 的 Codex 任务中说:

> 用 $wechat-codex 把 Codex 接入微信。

Skill 会检查环境、准备独立桥接目录、展示二维码,并核实登录和消息回传。扫码确认需要本人完成。也可以直接运行桥接:

```bash
python3 skills/wechat-codex/scripts/setup.py --target ./wechat-codex-local
npm --prefix ./wechat-codex-local ci --ignore-scripts --no-audit --no-fund
node ./wechat-codex-local/codex-wechat.mjs
```

微信命令:`/help`、`/status`、`/new`、`/cancel`、`/stop`。默认在桥接目录的 `codex-workspace` 中执行任务,可用 `WECHAT_CODEX_WORKDIR` 指定项目路径。

第一条任务创建**独立的 Codex 会话**,后续消息通过明确的 session ID 续接;`/status` 显示 ID,可在本机用 `codex resume <SESSION_ID>` 查看。Skill 在支持任务导航工具的 Codex App 中也可以按 ID 打开会话。请避免微信和本地同时向同一会话提交任务。

当前支持文字输入与回复、配对账号限制、单任务执行、10 分钟超时和有界登录重试。使用 `workspace-write` 沙箱,额外审批不可通过该桥接完成。微信凭证只在内存中;重启需重新扫码并创建新对话,Codex 自身的历史记录仍保存在本地。Claude Code 暂未包含在此 skill 中。

## 🔧 预编译 Echo Bot

不想写代码?可以直接下载预编译的 Echo Bot 体验。详见 [GitHub Releases](https://github.com/corespeed-io/wechatbot/releases)。
Expand Down Expand Up @@ -203,6 +235,7 @@ wechatbot/
│ └── src/ # 6 个模块
├── pi-agent/ # Pi 扩展(微信 ↔ Pi 桥接)
│ └── src/ # 扩展入口 & 微信客户端
├── skills/wechat-codex/ # Codex 微信 Skill 与独立桥接模板
└── docs/ # 共享文档
├── protocol.md # iLink API 协议规范
└── architecture.md # 架构 & SDK 对比
Expand Down
21 changes: 21 additions & 0 deletions skills/wechat-codex/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 CoreSpeed and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
69 changes: 69 additions & 0 deletions skills/wechat-codex/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: wechat-codex
description: Connect WeChat to a local Codex CLI session, display its login QR code, verify message delivery, locate or open the linked session, and troubleshoot or stop the bridge. Use for requests such as 把 Codex 接入微信 or 查看微信连接状态.
---

# WeChat ↔ Codex

Operate a local bridge backed by `@wechatbot/wechatbot` and the user's installed Codex CLI. A skill orchestrates setup and troubleshooting; a running Node process handles ongoing messages. Do not substitute Pi, Claude, or OpenClaw for a requested Codex connection.

## Start or reconnect

1. Look for the bridge already used in the current task. Read its status before installing or restarting anything. Keep a healthy existing connection running. One WeChat account should have one polling instance; don't restart merely to apply a skill update.
2. Check `node --version` (22+), `codex --version`, `codex login status`, and relevant flags in `codex exec --help`. Reuse existing Codex authentication. If login is missing, let the user complete `codex login`; do not ask them to paste tokens into chat. The bundled cancellation implementation targets macOS/Linux; do not claim Windows support without adapting and testing process handling.
3. Use the user's chosen project as the Codex working directory. If none is chosen, use the bridge's dedicated `codex-workspace` directory and say so. This starts an independent CLI session, not the current App conversation. Only attach an existing session when the user identifies it, and first ensure it has no other writer.
4. Scaffold into a task-owned directory, preferably an `outputs/` directory for a deliverable:

```sh
python3 <skill-dir>/scripts/setup.py --target <absolute-bridge-dir>
npm --prefix <absolute-bridge-dir> ci --ignore-scripts --no-audit --no-fund
```

`setup.py` refuses to overwrite existing files. Dependencies and bridge code belong in the target, never run from or install dependencies into the skill directory.
5. Launch with a long-lived PTY so a WeChat pairing-code prompt can be completed if needed. Keep the host awake and retain the process handle; this skill does not install a system service:

```sh
node <absolute-bridge-dir>/codex-wechat.mjs
```

Optional `WECHAT_CODEX_WORKDIR` selects the task workspace; `WECHAT_CODEX_BIN` selects an explicitly located Codex binary. Verify `QR_READY`, then display `<bridge-dir>/codex-login-qr.png` as a local image. The user scans and confirms. Never claim success just because a QR exists. Login timeouts retry at most three times; terminal failures need diagnosis rather than an unbounded restart loop.
6. Verify `LOGIN_OK` / `ONLINE`. Ask the user to send one short task, then check `REPLY_OK` or the status file's successful reply count. A Codex-only smoke test is not a WeChat round-trip test. Report exactly which stages passed.

## Status and finding the session

```sh
python3 <skill-dir>/scripts/status.py --target <absolute-bridge-dir>
```

The helper reads only bridge metadata. `processAlive` means only that a PID exists; `fresh` checks the heartbeat. Even fresh `online` status describes the last reported state, not a new network probe. Report the last failure when present.

The first task creates a Codex session; the runner resumes only that exact ID afterward. `/status` displays the ID. `/new` resets it for the next task. The bundled bridge starts a new conversation after process restart; don't promise cross-restart conversation recovery.

To view a session:

- In Codex App, use `read_thread` with the exact ID, then `navigate_to_codex_page` when the user wants it opened. A CLI-created session can be readable even when absent from the sidebar list. Do not declare it invisible merely because `list_threads` omits it.
- In a terminal, use `codex resume <SESSION_ID>`. The picker may need `--all --include-non-interactive` on versions supporting those flags. Avoid simultaneous App/terminal and WeChat writes to the same session.
- For an older bridge without status metadata, search only the first `session_meta` line of local session files, restricted to the exact bridge working directory and relevant dates. Treat the newest match as a candidate until verified; do not read or export unrelated session histories.

## Commands and boundaries

Text tasks are sent to Codex and replies return to the authenticated WeChat sender. Commands: `/help`, `/status`, `/new`, `/cancel`, `/stop`. Busy requests are rejected rather than silently queued. The runtime deduplicates message IDs, bounds task duration, and restricts messages to the account that scanned in.

The default uses `workspace-write` and no interactive approvals. Explain failed operations; don't bypass the sandbox. `--ignore-user-config` avoids automatically importing unrelated MCP configuration while retaining CLI authentication. This does not mean the entire machine is inaccessible or all global context is disabled.

WeChat credentials stay in process memory. Codex persists its own sessions normally. Runtime metadata contains the session ID and status, not tokens or message bodies. Keep generated QR codes, runtime files, account identifiers and private sessions out of shared skill bundles and PRs.

Stop via `/stop` or signal the verified bridge process. Do not kill processes using a broad `pkill node` or `pkill codex` pattern. If a bridge is healthy, status requests should be read-only.

## Troubleshooting learned from live use

- `QR_READY` followed by a login `TimeoutError`: QR-status polling failed, not proof of bad Codex authentication. Bounded login retries issue a fresh QR; show the latest one.
- `ONLINE` with no replies: check successful reply count, task errors, sender matching, and Codex auth separately. Don't print credential contents to diagnose.
- Session missing from sidebar: use its exact ID and read/open it directly before proposing a different architecture.
- If the user wants a public contribution, package this bridge and workflow as an example/skill. Local installation alone does not imply permission to publish a PR.

Sources: [WeChatBot SDK](https://www.wechatbot.dev/zh/nodejs), [Codex non-interactive mode](https://learn.chatgpt.com/docs/non-interactive-mode), [Tencent's OpenClaw Weixin plugin](https://github.com/Tencent/openclaw-weixin). The Tencent plugin is an alternative integration path, not a dependency of this bridge.

## Maintainer validation

From the repository root, run `node --test skills/wechat-codex/tests/*.test.mjs` and `python3 -m unittest discover -s skills/wechat-codex/tests -p 'test_*.py'`. Tests use temporary workspaces and fake Codex processes; they do not authenticate to WeChat or send messages. A real scan and reply remains a separate manual integration check.
4 changes: 4 additions & 0 deletions skills/wechat-codex/assets/bridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.runtime/
codex-workspace/
*-login-qr.png
84 changes: 84 additions & 0 deletions skills/wechat-codex/assets/bridge/codex-runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { spawn } from 'node:child_process';
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
import { join } from 'node:path';

export class CodexRunner {
constructor({ cwd, runtimeDir, executable = 'codex', timeoutMs = 10 * 60_000 }) {
Object.assign(this, { cwd, runtimeDir, executable, timeoutMs });
this.threadId = null;
this.child = null;
this.running = false;
}
cancel() {
if (!this.child) return false;
this.cancelled = true;
this.terminate();
return true;
}
terminate() {
const child = this.child;
if (!child?.pid) return;
try { process.kill(-child.pid, 'SIGTERM'); } catch {}
this.killTimer = setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }, 3000);
this.killTimer.unref();
}
reset() {
if (this.running) throw new Error('请先取消当前任务。');
this.threadId = null;
}
async run(prompt) {
if (this.running) throw new Error('Codex 正在处理上一条消息,请稍后再发。');
this.running = true;
this.cancelled = false;
let timedOut = false;
let dir;
try {
await mkdir(this.runtimeDir, { recursive: true, mode: 0o700 });
dir = await mkdtemp(join(this.runtimeDir, 'run-'));
const output = join(dir, 'reply.txt');
const args = ['exec', '--ignore-user-config', '--skip-git-repo-check',
'--sandbox', 'workspace-write', '-c', 'approval_policy="never"',
'-C', this.cwd];
if (this.threadId) args.push('resume', this.threadId);
args.push('--json', '-o', output, '-');
const child = spawn(this.executable, args, { cwd: this.cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'] });
this.child = child;
child.stdout.setEncoding('utf8');
const timer = setTimeout(() => { timedOut = true; this.terminate(); }, this.timeoutMs);
let buffer = '';
let lastError = '';
child.stdout.on('data', chunk => {
buffer += chunk;
let index;
while ((index = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
try {
const event = JSON.parse(line);
if (event.type === 'thread.started') this.threadId = event.thread_id;
if (event.type === 'turn.failed') lastError = event.error?.message ?? 'Codex 执行失败';
} catch {}
}
});
child.stderr.resume();
child.stdin.on('error', () => {});
const completed = new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', code => resolve(code));
});
child.stdin.end(prompt);
let code;
try { code = await completed; } finally { clearTimeout(timer); }
if (this.cancelled) throw new Error('任务已取消。');
if (timedOut) throw new Error('任务超过执行时限,已停止;可以拆分任务后重试。');
if (code !== 0) throw new Error(lastError || 'Codex 执行失败,请在电脑上检查登录状态或额度。');
const reply = (await readFile(output, 'utf8')).trim();
if (!reply) throw new Error('Codex 没有返回文本。');
return reply;
} finally {
clearTimeout(this.killTimer);
this.child = null;
this.running = false;
if (dir) await rm(dir, { recursive: true, force: true });
}
}
}
Loading
Loading