{"id":50859327,"url":"https://github.com/est/wx-bot","last_synced_at":"2026-06-14T20:30:43.304Z","repository":{"id":364179441,"uuid":"1266763839","full_name":"est/wx-bot","owner":"est","description":"🦞 for 张小龙 ","archived":false,"fork":false,"pushed_at":"2026-06-12T00:15:04.000Z","size":49,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T02:07:07.723Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/est.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-11T23:46:31.000Z","updated_at":"2026-06-12T00:15:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/est/wx-bot","commit_stats":null,"previous_names":["est/wx-bot"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/est/wx-bot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/est%2Fwx-bot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/est%2Fwx-bot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/est%2Fwx-bot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/est%2Fwx-bot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/est","download_url":"https://codeload.github.com/est/wx-bot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/est%2Fwx-bot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34337551,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-14T20:30:42.543Z","updated_at":"2026-06-14T20:30:43.296Z","avatar_url":"https://github.com/est.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# wx-bot\n\n微信 Bot 管理平台。通过 `@tencent-weixin/openclaw-weixin` 对接腾讯 iLink API，提供 Web 界面收发微信消息和多媒体。\n\n## 核心原则\n\n**不自己移植/重写官方包的代码。** 所有与微信 API 的交互都通过 `@tencent-weixin/openclaw-weixin` 的导出函数完成。不复制 fetch、headers、请求格式等实现。\n\n原因：\n- 官方包频繁更新，自己写的代码会与官方脱节\n- 官方 HTTP API 地址、数据格式可能随时变化\n- 自己重写容易出错（比如 `aes_key` 编码、`SendMessageReq` 的 `msg` 包装）\n- 官方包依赖 openclaw（一个 364MB 的框架），通过 postinstall 脚本生成 stub 代替它\n\n如果包缺少某个功能，**讨论解决方案**，不要自己重写。详见 `AGENTS.md`。\n\n（这也导致了这个项目不可能跑在Cloudflare Worker上，必须是 Vercel 这种有 fs 和真正nodejs环境的）\n\n## 功能状态\n\n| 功能 | 状态 |\n|---|---|\n| Passkey 登录 (WebAuthn) | ✅ |\n| 多 Bot 管理 | ✅ |\n| QR 扫码登录微信 | ✅ |\n| 文本消息收发 | ✅ |\n| 图片消息收发 | ✅ |\n| 语音消息发送 (SILK 编码) | ✅ |\n| 语音消息接收/播放 (SILK 解码) | ✅ |\n| 文件/视频消息收发 | ✅ |\n| 实时消息 (SSE) | ✅ |\n| 后台消息收集 (QStash) | ✅ |\n| 自动更新 (Vercel Cron + Deploy Hook) | ✅ |\n| npm 包版本监控 (NpmBanner) | ✅ |\n\n## 技术栈\n\n| 层面 | 方案 |\n|---|---|\n| 框架 | Next.js 16 (App Router) + TypeScript |\n| 部署 | Vercel (Serverless) |\n| 数据库 | Turso (libSQL) + Drizzle ORM |\n| 认证 | @simplewebauthn (WebAuthn) + iron-session |\n| 微信通信 | @tencent-weixin/openclaw-weixin (iLink API) |\n| 音频编解码 | silk-wasm (CDN 加载，浏览器侧 WASM) |\n| 后台任务 | QStash (自链式轮询) |\n\n## 架构\n\n```\n┌─────────────┐     ┌──────────────┐     ┌───────────────┐\n│  浏览器 UI   │────▶│ Next.js API  │────▶│ iLink WeChat  │\n│             │◀────│   Routes     │◀────│     API       │\n│  SSE 长连接  │     │              │     │               │\n│  SILK WASM  │     │ cdn-proxy     │     │  CDN Upload   │\n│  AES-ECB    │     │ (decrypt)    │     │  CDN Download │\n└─────────────┘     └──────────────┘     └───────────────┘\n                           │\n                    ┌──────┴───────┐\n                    │ Turso Sqlite │\n                    │ (libSQL)     │\n                    └──────────────┘\n```\n\n**关键设计：**\n\n- **适配器模式** — `src/lib/weixin/adapter.ts` 薄封装官方包的函数，不复制实现。上游变更只需 redeploy，破坏性变更由 TypeScript 编译时捕获。\n- **openclaw stub** — postinstall 脚本生成 36KB stub 代替 364MB 的 openclaw 包。`dist/` 编译 JS 运行时导入，本地 `.d.ts` 提供类型。\n- **浏览器侧媒体处理** — CDN 媒体的 AES-128-ECB 解密和 SILK→WAV 转码都在浏览器完成，不经过服务器代理，节省 serverless 调用。\n- **环境变量最小化** — `TURSO_DATABASE_URL` + `TURSO_AUTH_TOKEN` 是唯一必需变量。认证配置从部署上下文自动推导。\n\n## 项目结构\n\n```\nsrc/\n├── app/                # Next.js App Router 页面和 API 路由\n├── lib/\n│   ├── db/             # 数据库 (Drizzle schema + Turso 客户端)\n│   ├── auth/           # 认证 (session + WebAuthn + 路由守卫)\n│   └── weixin/         # 微信集成层 (适配器、客户端、媒体、CDN)\n├── components/         # React UI 组件\n└── proxy.ts            # 路由保护\n\nscripts/                # postinstall 脚本 (openclaw stub + 类型同步)\npublic/                 # 静态资源 (silk.mjs 等浏览器侧纯 JS)\n```\n\n---\n\n## `@tencent-weixin/openclaw-weixin` 包概览\n\n\u003e 以下基于 v2.4.4。包没有 `exports` 字段，所有功能通过 `dist/src/` 深路径导入。\n\n### 包的本质\n\n这是一个 **OpenClaw 插件**，不是独立 SDK。顶层默认导出是 OpenClaw 插件描述符，所有实用功能都在 `dist/src/` 子模块中以命名导出提供。本项目不使用 OpenClaw 运行时，只导入这些子模块。\n\n### 模块划分\n\n| 模块路径 | 职责 |\n|---|---|\n| `dist/src/api/api.js` | iLink HTTP API 客户端（所有网络请求） |\n| `dist/src/api/types.js` | 协议类型定义和常量枚举 |\n| `dist/src/cdn/*.js` | CDN 上传/下载、AES-ECB 加解密、URL 构造 |\n| `dist/src/messaging/*.js` | 消息发送（文本/图片/视频/文件）、入站处理 |\n| `dist/src/auth/*.js` | 账号管理、QR 登录、配对 |\n| `dist/src/media/*.js` | 媒体下载、MIME 工具、SILK 转码 |\n| `dist/src/monitor/` | 长轮询主循环 |\n| `dist/src/storage/` | 状态持久化（sync buffer、state 目录） |\n| `dist/src/util/` | 日志、ID 生成、脱敏工具 |\n\n### iLink API 端点\n\n| 端点 | 函数 | 说明 |\n|---|---|---|\n| `ilink/bot/getupdates` | `getUpdates` | 长轮询拉取消息，服务端 hold 约 35s |\n| `ilink/bot/sendmessage` | `sendMessage` | 发送消息（文本/图片/语音/文件/视频） |\n| `ilink/bot/getuploadurl` | `getUploadUrl` | 获取 CDN 上传预签名 URL |\n| `ilink/bot/getconfig` | `getConfig` | 获取 bot 配置（typing_ticket） |\n| `ilink/bot/sendtyping` | `sendTyping` | 发送输入状态指示 |\n| `ilink/bot/msg/notifystop` | `notifyStop` | 通知服务端通道停止 |\n| `ilink/bot/msg/notifystart` | `notifyStart` | 通知服务端通道启动 |\n| `ilink/bot/get_bot_qrcode` | QR 登录 | 获取扫码二维码 |\n| `ilink/bot/get_qrcode_status` | QR 轮询 | 轮询扫码状态 |\n\n认证方式：`Authorization: Bearer {bot_token}`，附加 `iLink-App-Id: bot` 等 headers。\n\n### 消息协议\n\n```\nSendMessageReq {\n  msg: WeixinMessage {\n    from_user_id, to_user_id, client_id,\n    message_type: 2,    // BOT\n    message_state: 2,   // FINISH\n    item_list: MessageItem[],\n    context_token\n  }\n}\n```\n\n`MessageItem.type` 枚举：TEXT=1, IMAGE=2, VOICE=3, FILE=4, VIDEO=5, TOOL_CALL_START=11, TOOL_CALL_RESULT=12。\n\n每种 type 对应不同的 item 字段（`text_item`, `image_item`, `voice_item` 等）。\n\n### CDN 媒体协议\n\n上传流程：\n1. 生成 16 字节随机 AES key\n2. `getUploadUrl(aeskey_hex, filekey, media_type, sizes)` → `upload_full_url`\n3. `uploadBufferToCdn(buf, url, aeskey)` → `downloadParam`（`x-encrypted-param` 响应头）\n4. 消息中引用：`{ encrypt_query_param, aes_key, encrypt_type }`\n\n`aes_key` 编码：`Buffer.from(hex_string).toString(\"base64\")` —— 是 hex 字符串的 base64，不是原始字节的 base64。\n\n`parseAesKey` 支持两种输入：原始 16 字节 base64，或 hex 32 字符的 base64。\n\n### QR 登录流程\n\n1. `get_bot_qrcode?bot_type=3` → 返回二维码图片 URL\n2. 轮询 `get_qrcode_status`（35s 超时，5min TTL）\n3. 状态：`wait` → `scaned` → `confirmed`（成功，返回 `bot_token` + `ilink_bot_id` + `baseurl`）\n\n### 本项目实际使用的导出\n\n| 来源 | 导入 |\n|---|---|\n| `dist/src/api/api.js` | `getUpdates`, `sendMessage`, `getUploadUrl`, `getConfig`, `sendTyping`, `apiPostFetch`, `apiGetFetch`, `buildBaseInfo` |\n| `dist/src/api/types.js` | `WeixinMessage`, `MessageItem`, `CDNMedia`, `TextItem`, `ImageItem`, `VoiceItem`, `FileItem`, `VideoItem` 及各 Req/Resp 类型、枚举常量 |\n| `dist/src/cdn/cdn-upload.js` | `uploadBufferToCdn` |\n| `dist/src/cdn/pic-decrypt.js` | `downloadAndDecryptBuffer`, `downloadPlainCdnBuffer` |\n| `dist/src/cdn/aes-ecb.js` | `aesEcbPaddedSize` |\n\n---\n\n## 对官方包的依赖与假设\n\n以下列出本项目依赖的官方包行为。如果这些行为变化，可能导致 break。\n\n### 会 break 的情况\n\n| 依赖 | 说明 |\n|---|---|\n| **深路径导入** | 包没有 `exports` 字段，本项目通过 `@tencent-weixin/openclaw-weixin/dist/src/...` 导入。如果包重构目录结构，所有 import 路径失效。 |\n| **`uploadBufferToCdn` 签名** | 参数 `{ buf, uploadFullUrl, uploadParam, filekey, cdnBaseUrl, label, aeskey }`，返回 `{ downloadParam }`。如果参数名或返回值变化，`media.ts` 需要更新。 |\n| **`downloadAndDecryptBuffer` 签名** | 参数 `(encryptedQueryParam, aesKeyBase64, cdnBaseUrl, label, fullUrl?)`。如果参数顺序或 `aes_key` 解析逻辑变化，`cdn-proxy` 需要更新。 |\n| **`aes_key` 编码格式** | 消息中 `aes_key` 必须是 `Buffer.from(hex).toString(\"base64\")`。如果官方改为直接用原始字节 base64，所有已发消息的解密会失败。 |\n| **`SendMessageReq` 包装格式** | 必须用 `{ msg: { ... } }` 包装，flat 字段会被忽略。如果 API 改为接受 flat 格式，发送消息会静默失败。 |\n| **iLink API 端点路径** | 如 `ilink/bot/sendmessage`。如果端点变化，所有 API 调用返回 404。 |\n| **CDN 响应头 `x-encrypted-param`** | `uploadBufferToCdn` 从此头读取下载参数。如果 CDN 改用其他头或响应体，上传后无法获取下载引用。 |\n| **openclaw peer dependency** | 包依赖 `openclaw` 的 `plugin-sdk/*` 模块。本项目通过 postinstall stub 代替。如果包新增导入的 openclaw 模块，stub 需要同步更新。 |\n| **类型定义** | `scripts/sync-weixin-types.mjs` 从包源码自动生成 `types.ts`。如果包的类型结构变化，构建时 TypeScript 会报错。 |\n\n### 不太会 break 的行为\n\n| 行为 | 说明 |\n|---|---|\n| iLink API 的认证方式 | Bearer token + 固定 headers，协议层面稳定 |\n| CDN 加密算法 | AES-128-ECB，标准算法不会变 |\n| QR 登录流程 | 状态机（wait→scaned→confirmed），协议层面稳定 |\n| `parseAesKey` 双格式支持 | 向后兼容，两种格式都会继续支持 |\n| `buildCdnDownloadUrl` 逻辑 | `cdnBaseUrl/download?encrypted_query_param=...`，URL 模板稳定 |\n\n## 部署\n\n### 1. 创建 Turso 数据库\n\n```bash\ncurl -sSfL https://get.tur.so/install.sh | bash\nturso auth login\nturso db create wx-bot\nturso db show wx-bot --url       # → TURSO_DATABASE_URL\nturso db tokens create wx-bot    # → TURSO_AUTH_TOKEN\n```\n\n### 2. 部署到 Vercel\n\n1. 导入 GitHub 仓库 `est/wx-bot`\n2. 设置环境变量：\n\n| 变量 | 说明 |\n|---|---|\n| `TURSO_DATABASE_URL` | `libsql://wx-bot-xxx.turso.io` |\n| `TURSO_AUTH_TOKEN` | Turso 认证 token |\n| `DEPLOY_HOOK_URL` | (可选) Vercel Deploy Hook URL，用于 Cron 自动更新 |\n\n3. 初始化数据库：`npm run db:push`\n\n### 3. 自动更新\n\n`package-lock.json` 不提交到 git。Vercel Cron 每小时触发 Deploy Hook → 重新构建 → `npm install` 解析最新 `^2.x`。无需手动改代码。NpmBanner 在页面底部显示当前安装版本和 npm 最新版本。\n\n## 本地开发\n\n```bash\ncp .env.example .env\n# 填写 TURSO_DATABASE_URL=file:local.db, TURSO_AUTH_TOKEN（本地可留空）\nnpm install\nnpm run db:push\nnpm run dev\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fest%2Fwx-bot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fest%2Fwx-bot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fest%2Fwx-bot/lists"}