Skip to content
Merged
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
140 changes: 140 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# WeChatPilot

[简体中文](README.md) | [English](README.en.md)

**Local-first, outbound-only, auditable AI automation for WeChat.**

Let local AI automation reach WeChat safely — without letting WeChat control your computer.

![WeChatPilot offline demo preview](docs/assets/demo-preview.png)

Current release: **Public Preview `v0.1.0-beta.1`**. This is not Stable and not Production Ready. Until the seven-day stability review (Issue #4) is complete, do not claim the pipeline is already stable.

> WeChatPilot is not affiliated with, authorized by, or endorsed by Tencent or WeChat.

## Product positioning

WeChatPilot is not a general remote-control WeChat bot. It is a local-first delivery tool for scheduled AI automation results:

- The work runs on your Mac.
- Delivery is outbound-only to the owner-bound WeChat account that scanned the QR code.
- WeChat cannot start jobs, choose recipients, open a shell, or remote-control Codex.
- Delivery state is auditable; indeterminate sends fail closed instead of auto-replaying.

The first built-in workflow is a daily tech digest.

## Offline demo

```sh
npm ci
npm run build
npm run wechatpilot -- demo
```

The demo:

- does not access WeChat
- does not call Codex
- does not read private `.state` credentials
- uses fixed fixtures
- writes a sample digest and preview image under `output/demo/`
- prints safe next steps

## Supported environment

- macOS
- Node.js 22
- Official Codex CLI available on `PATH` and logged in
- Simplified Chinese and `Asia/Shanghai` for v0.1
- Personal WeChat account for owner-bound outbound delivery

## Quick start

```sh
npm ci
npm run build
npm run wechatpilot -- init
npm run wechatpilot -- doctor
./scripts/init-wechat.sh
npm run send:test
```

Common commands:

```sh
npm run wechatpilot -- demo
npm run wechatpilot -- doctor --repair
npm run wechatpilot -- config validate
npm run wechatpilot -- run daily-tech-digest --dry-run
npm run wechatpilot -- schedule install --hour 8 --minute 0
npm run wechatpilot -- schedule status
npm run wechatpilot -- schedule remove
```

v0.1 scheduling is fixed to `08:00 Asia/Shanghai`. The host system timezone must also be `Asia/Shanghai` because LaunchAgent calendar triggers use the machine timezone.

## Security boundary

| Capability | Typical remote-control bot | WeChatPilot |
| --- | ---: | ---: |
| WeChat can trigger host commands | often possible | explicitly forbidden |
| Arbitrary recipients | often possible | owner-bound only |
| Auto-retry when send is indeterminate | may duplicate | stop by default |
| Credentials stay local | maybe | yes |
| Run/delivery ledger | maybe | yes |

Additional guarantees:

- Project-owned minimal WeChat transport only.
- No upstream remote-control entrypoint, router, adapter registry, or `undici` production dependency.
- Codex child stays ephemeral, read-only, non-interactive, stdin-only.
- Secrets, tokens, cookies, QR codes, and personal paths must stay out of Issues and logs.

See also:

- `docs/security/runtime-closure.md`
- `security/openvex.json`
- `SECURITY.md`
- `THREAT_MODEL.md`

## Privacy model

Local-first means scheduling, credentials, ledgers, locks, and audit state stay on the machine. It does **not** mean model inference is fully offline. Candidate titles, summaries, and public URLs may enter the Codex child process. WeChat tokens, context tokens, and user IDs must not.

Details: [PRIVACY.md](PRIVACY.md) and [DATA_FLOW.md](DATA_FLOW.md).

## Known limitations

- macOS only
- Node.js 22 and logged-in Codex CLI required
- v0.1 locked to Simplified Chinese and `Asia/Shanghai`
- outbound delivery only to the bound owner account
- no inbound WeChat commands, remote shell, or Codex remote control
- only one built-in workflow: daily tech digest
- Public Preview, not Stable
- Issue #4 seven-day stability evidence is still open

## Contribution guide

Thanks for helping improve WeChatPilot.

- Keep WeChat one-way. Do not add inbound command execution or remote control.
- Prefer small pull requests with tests for the changed boundary.
- Do not commit secrets, tokens, cookies, QR codes, private digests, real user IDs, or local absolute paths.
- Start with:

```sh
npm ci
npm test
npm run typecheck
npm run build
npm run wechatpilot -- config validate
```

Full contributor guidance: [CONTRIBUTING.md](CONTRIBUTING.md)
Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
Security reporting: [SECURITY.md](SECURITY.md)

## License

MIT for WeChatPilot-owned code. Third-party notices: `THIRD_PARTY_NOTICES.md` and `package-lock.json`.
85 changes: 69 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
# WeChatPilot

WeChatPilot 是一个本地优先、只向本人微信单向投递的自动化项目。当前内置工作流是“每日科技情报”。微信不能执行命令、控制 Codex 或选择收件人;凭据与投递状态只保存在被 Git 忽略且权限受限的 `.state/`。
[简体中文](README.md) | [English](README.en.md)

> WeChatPilot 与微信、腾讯不存在隶属、授权或背书关系。
**让本地 AI 自动化安全地抵达微信,而不是让微信反向控制电脑。**

## 安装
> Local-first, outbound-only, auditable AI automation for WeChat.

要求 macOS、Node.js 22,以及已登录且可从 `PATH` 找到的官方 Codex CLI。仓库不保存个人路径,不提交可直接加载的部署 plist,也不要求固定 Homebrew 目录。
![WeChatPilot offline demo preview](docs/assets/demo-preview.png)

普通用户:
当前版本:**Public Preview `v0.1.0-beta.1`**。这不是 Stable,也不是 Production Ready。七日稳定性复盘(Issue #4)完成前,不得宣称链路已经稳定。

✅ 本地优先
✅ 仅向扫码绑定的本人微信单向发送
✅ 不开放远程命令、Shell 或 Codex 远程控制
✅ 发送结果可审计;无法确认时拒绝自动重放

> WeChatPilot 与微信、腾讯不存在隶属、授权或背书关系。请勿把本项目理解为官方产品。

## 5 分钟快速开始

要求:macOS、Node.js 22、已登录且可从 `PATH` 找到的官方 Codex CLI。

```sh
npm ci
npm run build
npm run wechatpilot -- demo
```

`demo` 完全离线:不访问微信、不调用 Codex、不读取私人 `.state` 凭据。它使用固定 Fixture,在本地生成示例日报文本和预览图,方便你先看最终效果。

完整安装:

```sh
npm run wechatpilot -- init
npm run wechatpilot -- doctor
./scripts/init-wechat.sh
npm run send:test
```

贡献者使用 `./scripts/init-project.sh` 安装依赖、执行测试、类型检查和构建;它与普通用户初始化分开。

统一命令:

```sh
npm run wechatpilot -- init
npm run wechatpilot -- doctor
npm run wechatpilot -- doctor --repair
npm run wechatpilot -- demo
npm run wechatpilot -- config validate
npm run wechatpilot -- run daily-tech-digest --dry-run
npm run wechatpilot -- schedule install --hour 8 --minute 0
Expand All @@ -35,6 +54,34 @@ npm run wechatpilot -- schedule remove

`schedule install` 在安装时探测 Node/Codex,生成当前用户专属 plist。v0.1 固定使用简体中文、`Asia/Shanghai`、每日 08:00 与固定栏目配额;LaunchAgent 日历触发使用机器系统时区,因此运行机器也必须设为 `Asia/Shanghai`。`doctor` 默认输出人类可读结果(`--json` 保留机器格式),且默认只读;只有显式 `--repair` 才会删除已确认进程死亡、inode 未变化且未被 `sending/indeterminate` ledger 阻止的陈旧锁。

## 它解决什么问题

很多“微信 Bot”会把聊天入口变成电脑控制面。WeChatPilot 反过来:

1. 本机任务先跑完。
2. 结果经排序、生成、链接校验和 ledger。
3. 只向扫码绑定的本人微信单向投递。
4. 微信不能启动任务、不能选择收件人、不能执行 Shell。

当前唯一内置工作流:**每日科技情报**。

## 安全边界

| 能力 | 普通远程控制 Bot | WeChatPilot |
| --- | ---: | ---: |
| 微信触发电脑命令 | 可能支持 | 明确禁止 |
| 任意收件人 | 可能支持 | 仅绑定本人 |
| 不确定发送自动重试 | 可能重复发送 | 默认停止 |
| 凭据本地保存 | 不一定 | 是 |
| 运行与投递 Ledger | 不一定 | 是 |

更多细节:

- 配置模型:`UserConfig` / `SecurityProfile` / `ResolvedRuntime`
- 微信 transport 与供应链:`src/transport/wechat-transport.ts`、`docs/security/runtime-closure.md`、`security/openvex.json`
- 隐私与数据流:`PRIVACY.md`、`DATA_FLOW.md`
- 威胁模型:`THREAT_MODEL.md`

## 配置模型

- `UserConfig`:`config/sources.yaml`、`ranking.yaml`、`digest.yaml` 与 observer 的来源和通知策略;v0.1 的时区、语言、配额、栏目和篇幅为固定契约。
Expand All @@ -45,12 +92,7 @@ npm run wechatpilot -- schedule remove

## 微信 transport 与供应链

项目不再携带 `cli-in-wechat` submodule,也不安装其 `undici` 或开发依赖。最小 transport 位于 `src/transport/wechat-transport.ts`,只实现扫码登录、重试请求和主动发送所需能力。运行时闭包、失效条件和机器可读 OpenVEX 见:

- `docs/security/runtime-closure.md`
- `security/openvex.json`

CI 会执行生产依赖审计,并禁止重新引入上游主入口、Router、适配器注册表、代理初始化器、`ProxyAgent` 或 `undici`。
项目不再携带 `cli-in-wechat` submodule,也不安装其 `undici` 或开发依赖。最小 transport 位于 `src/transport/wechat-transport.ts`,只实现扫码登录、重试请求和主动发送所需能力。CI 会执行生产依赖审计,并禁止重新引入上游主入口、Router、适配器注册表、代理初始化器、`ProxyAgent` 或 `undici`。

## 正确性与网络安全

Expand All @@ -60,7 +102,7 @@ CI 会执行生产依赖审计,并禁止重新引入上游主入口、Router

## 隐私与数据流

“本地优先”指调度、凭据、ledger、锁和审计在本地,不代表模型推理完全离线。候选标题、摘要和公开 URL 会进入 Codex 子进程;微信 Token、context token 和用户 ID 不会进入模型。详见 [PRIVACY.md](PRIVACY.md) 与 [DATA_FLOW.md](DATA_FLOW.md)。
“本地优先”指调度、凭据、ledger、锁和审计在本地,不代表模型推理完全离线。候选标题、摘要和公开 URL 会进入 Codex 子进程;微信 Token、context token 和用户 ID 不会进入模型。

## 目录布局

Expand All @@ -70,12 +112,20 @@ CI 会执行生产依赖审计,并禁止重新引入上游主入口、Router

兼容默认仍落在项目目录;可通过 profile root / 环境变量拆分。详见 [ARCHITECTURE.md](ARCHITECTURE.md)。

## 已知限制

- 仅支持 macOS。
- 需要 Node.js 22 与已登录 Codex CLI。
- v0.1 固定简体中文与 `Asia/Shanghai`。
- 仅向扫码绑定的本人微信单向投递。
- 不提供入站命令、远程 Shell 或 Codex 远程控制。
- 当前只有“每日科技情报”一个内置工作流。
- Public Preview,不是 Stable。

## 稳定性门禁

平台抽象 Issue #1 明确依赖七天稳定性复盘 Issue #4。复盘只允许输出脱敏计数和耗时分布,不包含正文、路径、身份或凭据。未收集满七个自然日时,项目不得宣称链路“已经稳定”,也不得开始通用平台抽象。

至少记录:每日成功/失败/跳过、来源降级、Codex 重写、链接替换与门禁失败、微信确定失败与不确定发送、重复拦截、陈旧锁与人工恢复、日报生成和投递耗时。

## 常用命令

```sh
Expand All @@ -84,6 +134,7 @@ npm run typecheck
npm run build
npm run audit:runtime
npm run check:transport-boundary
npm run wechatpilot -- demo
./scripts/generate-digest.sh --dry-run
./scripts/send-test.sh
./scripts/send-digest.sh --date YYYY-MM-DD
Expand All @@ -92,3 +143,5 @@ npm run check:transport-boundary
日志会递归脱敏。请勿在公开 Issue 提交 Token、Cookie、微信用户 ID、context token、二维码、私有正文、日志或本机路径。漏洞请使用 GitHub 私密安全报告。

WeChatPilot 自有代码采用 MIT License;第三方依赖见 `THIRD_PARTY_NOTICES.md` 与 `package-lock.json`。

贡献指南见 [CONTRIBUTING.md](CONTRIBUTING.md)。英文说明见 [README.en.md](README.en.md)。
Binary file added docs/assets/demo-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/social-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "wechatpilot",
"version": "0.1.0",
"private": true,
"description": "Local-first AI automation for WeChat",
"description": "Local-first, outbound-only AI automation for WeChat",
"license": "MIT",
"homepage": "https://github.com/kdnsna/WeChatPilot#readme",
"bugs": {
Expand Down
87 changes: 87 additions & 0 deletions src/cli/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

export interface DemoResult {
readonly ok: true;
readonly status: "demo-ready";
readonly format: "human";
readonly text: string;
readonly digestPath: string;
readonly imagePath: string;
readonly socialPreviewPath: string;
}

function packageRoot(): string {
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
}

function ensureDirectory(path: string): void {
mkdirSync(path, { recursive: true, mode: 0o755 });
}

export function runOfflineDemo(options: {
readonly projectRoot: string;
}): DemoResult {
const root = options.projectRoot;
if (!isAbsolute(root)) {
throw new Error("Project root must be absolute");
}

const sourceRoot = packageRoot();
const fixtureDigest = join(
sourceRoot,
"tests",
"fixtures",
"demo",
"sample-digest.txt",
);
const fixtureImage = join(sourceRoot, "docs", "assets", "demo-preview.png");
const socialPreview = join(sourceRoot, "docs", "assets", "social-preview.png");

if (!existsSync(fixtureDigest) || !existsSync(fixtureImage)) {
throw new Error("Demo fixtures are missing from the package");
}

const outputDirectory = join(root, "output", "demo");
ensureDirectory(outputDirectory);

const digestPath = join(outputDirectory, "sample-digest.txt");
const imagePath = join(outputDirectory, "demo-preview.png");
const digest = readFileSync(fixtureDigest, "utf8");
writeFileSync(digestPath, digest, { encoding: "utf8", mode: 0o644 });
copyFileSync(fixtureImage, imagePath);

const text = [
"WeChatPilot offline demo ready.",
"",
"This demo does not access WeChat, call Codex, or read private .state credentials.",
`Sample digest: ${digestPath}`,
`Sample preview image: ${imagePath}`,
`Social preview asset: ${socialPreview}`,
"",
"Safe next steps:",
"1. npm run wechatpilot -- init",
"2. npm run wechatpilot -- doctor",
"3. ./scripts/init-wechat.sh",
"4. npm run send:test",
"",
"Public Preview only: owner-bound outbound delivery, no remote control.",
].join("\n");

return {
ok: true,
status: "demo-ready",
format: "human",
text: `${text}\n`,
digestPath,
imagePath,
socialPreviewPath: socialPreview,
};
}
Loading
Loading