mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
feat(adapters): add WhatsApp linked-device support (#573)
Add a WhatsApp adapter backed by Baileys linked-device auth, plus desktop QR binding UI, server config endpoints, sidecar startup wiring, tests, and documentation. Constraint: Uses WhatsApp Web linked-device auth, not Meta WhatsApp Business Cloud API. Tested: - cd adapters && bun run check:adapters - bun test src/server/__tests__/adapters.test.ts - cd desktop && bun run check:desktop - bun run check:native - bun run check:persistence-upgrade - bun run check:docs Not-tested: - Live WhatsApp QR pairing, because no WhatsApp account/device was exercised here. - bun run check:server, because the existing src/server/__tests__/conversations.test.ts timeout still fails independently. Confidence: medium Scope-risk: moderate
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
- `docs/im/index.md`
|
||||
- `docs/im/wechat.md`
|
||||
- `docs/im/dingtalk.md`
|
||||
- `docs/im/whatsapp.md`
|
||||
- `docs/im/telegram.md`
|
||||
- `docs/im/feishu.md`
|
||||
|
||||
@@ -40,6 +41,8 @@ bun run feishu
|
||||
bun run wechat
|
||||
# 或
|
||||
bun run dingtalk
|
||||
# 或
|
||||
bun run whatsapp
|
||||
```
|
||||
|
||||
## 开发
|
||||
@@ -54,6 +57,7 @@ bun test telegram/
|
||||
bun test feishu/
|
||||
bun test wechat/
|
||||
bun test dingtalk/
|
||||
bun test whatsapp/
|
||||
```
|
||||
|
||||
### 目录结构
|
||||
@@ -73,6 +77,10 @@ adapters/
|
||||
├── dingtalk/
|
||||
│ ├── helpers.ts # 钉钉 Stream 消息解析与会话键
|
||||
│ └── index.ts # 钉钉扫码绑定 / Stream 文本聊天 Adapter
|
||||
├── whatsapp/
|
||||
│ ├── session.ts # Baileys socket / auth state 封装
|
||||
│ ├── protocol.ts # WhatsApp QR 登录 / 解绑协议封装
|
||||
│ └── index.ts # WhatsApp Web 私聊 Adapter
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
@@ -86,6 +94,7 @@ adapters/
|
||||
|
||||
- 飞书: 图片(jpg/png/gif/webp/heic)、文档(doc/xls/ppt/pdf 等)、post 富文本里的 img/file 元素
|
||||
- Telegram: photo、document、video、audio、voice
|
||||
- WhatsApp: image、document、video、audio、sticker
|
||||
|
||||
下载落地到 `~/.claude/im-downloads/{platform}/{sessionId}/`,24 小时后自动 GC(`.part` 孤文件 10 分钟超时)。大小限制:单张图 ≤10 MB、单个文件 ≤30 MB,超限直接拒收并在 IM 里提示。
|
||||
|
||||
@@ -95,6 +104,7 @@ Agent 流式文本里的 markdown 图片引用 `` 会被 `
|
||||
|
||||
- 飞书: `im.message.create(msg_type='image')` 单发(card 内嵌是后续优化)
|
||||
- Telegram: `bot.api.sendPhoto(InputFile)` 单发
|
||||
- WhatsApp: Baileys `sendMessage({ image })` 单发
|
||||
|
||||
非图片类出站(Agent 产的 pdf/zip 等)暂不支持。
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"name": "claude-code-im-adapters",
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "^1.60.0",
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"dingtalk-stream": "2.1.4",
|
||||
"grammy": "^1.42.0",
|
||||
"ws": "^8.18.0",
|
||||
@@ -21,10 +22,80 @@
|
||||
"protobufjs": "^7.5.5",
|
||||
},
|
||||
"packages": {
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "https://registry.npmmirror.com/@borewit/text-codec/-/text-codec-0.2.2.tgz", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
|
||||
|
||||
"@cacheable/memory": ["@cacheable/memory@2.0.9", "https://registry.npmmirror.com/@cacheable/memory/-/memory-2.0.9.tgz", { "dependencies": { "@cacheable/utils": "^2.4.1", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA=="],
|
||||
|
||||
"@cacheable/node-cache": ["@cacheable/node-cache@1.7.6", "https://registry.npmmirror.com/@cacheable/node-cache/-/node-cache-1.7.6.tgz", { "dependencies": { "cacheable": "^2.3.1", "hookified": "^1.14.0", "keyv": "^5.5.5" } }, "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A=="],
|
||||
|
||||
"@cacheable/utils": ["@cacheable/utils@2.4.1", "https://registry.npmmirror.com/@cacheable/utils/-/utils-2.4.1.tgz", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="],
|
||||
|
||||
"@grammyjs/types": ["@grammyjs/types@3.26.0", "https://registry.npmmirror.com/@grammyjs/types/-/types-3.26.0.tgz", {}, "sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A=="],
|
||||
|
||||
"@hapi/boom": ["@hapi/boom@9.1.4", "https://registry.npmmirror.com/@hapi/boom/-/boom-9.1.4.tgz", { "dependencies": { "@hapi/hoek": "9.x.x" } }, "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw=="],
|
||||
|
||||
"@hapi/hoek": ["@hapi/hoek@9.3.0", "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@keyv/bigmap": ["@keyv/bigmap@1.3.1", "https://registry.npmmirror.com/@keyv/bigmap/-/bigmap-1.3.1.tgz", { "dependencies": { "hashery": "^1.4.0", "hookified": "^1.15.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ=="],
|
||||
|
||||
"@keyv/serialize": ["@keyv/serialize@1.1.1", "https://registry.npmmirror.com/@keyv/serialize/-/serialize-1.1.1.tgz", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="],
|
||||
|
||||
"@larksuiteoapi/node-sdk": ["@larksuiteoapi/node-sdk@1.60.0", "https://registry.npmmirror.com/@larksuiteoapi/node-sdk/-/node-sdk-1.60.0.tgz", { "dependencies": { "axios": "~1.13.3", "lodash.identity": "^3.0.0", "lodash.merge": "^4.6.2", "lodash.pickby": "^4.6.0", "protobufjs": "^7.2.6", "qs": "^6.14.2", "ws": "^8.19.0" } }, "sha512-MS1eXx7K6HHIyIcCBkJLb21okoa8ZatUGQWZaCCUePm6a37RWFmT6ZKlKvHxAanSX26wNuNlwP0RhgscsE+T6g=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "https://registry.npmmirror.com/@pinojs/redact/-/redact-0.4.0.tgz", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
@@ -45,28 +116,46 @@
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.1.tgz", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "https://registry.npmmirror.com/@tokenizer/token/-/token-0.3.0.tgz", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.2", "https://registry.npmmirror.com/@types/node/-/node-25.5.2.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@whiskeysockets/baileys": ["@whiskeysockets/baileys@7.0.0-rc.9", "https://registry.npmmirror.com/@whiskeysockets/baileys/-/baileys-7.0.0-rc.9.tgz", { "dependencies": { "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", "lru-cache": "^11.1.0", "music-metadata": "^11.7.0", "p-queue": "^9.0.0", "pino": "^9.6", "protobufjs": "^7.2.4", "ws": "^8.13.0" }, "peerDependencies": { "audio-decode": "^2.1.3", "jimp": "^1.6.0", "link-preview-js": "^3.0.0", "sharp": "*" }, "optionalPeers": ["audio-decode", "jimp", "link-preview-js"] }, "sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"async-mutex": ["async-mutex@0.5.0", "https://registry.npmmirror.com/async-mutex/-/async-mutex-0.5.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"atomic-sleep": ["atomic-sleep@1.0.0", "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||
|
||||
"axios": ["axios@1.13.6", "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.11.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"cacheable": ["cacheable@2.3.5", "https://registry.npmmirror.com/cacheable/-/cacheable-2.3.5.tgz", { "dependencies": { "@cacheable/memory": "^2.0.8", "@cacheable/utils": "^2.4.1", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" } }, "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"content-type": ["content-type@2.0.0", "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||
|
||||
"curve25519-js": ["curve25519-js@0.0.4", "https://registry.npmmirror.com/curve25519-js/-/curve25519-js-0.0.4.tgz", {}, "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dingtalk-stream": ["dingtalk-stream@2.1.4", "https://registry.npmmirror.com/dingtalk-stream/-/dingtalk-stream-2.1.4.tgz", { "dependencies": { "axios": "^1.4.0", "debug": "^4.3.4", "ws": "^8.13.0" } }, "sha512-rgQbXLGWfASuB9onFcqXTnRSj4ZotimhBOnzrB4kS19AaU9lshXiuofs1GAYcKh5uzPWCAuEs3tMtiadTQWP4A=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
@@ -81,6 +170,10 @@
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"file-type": ["file-type@21.3.4", "https://registry.npmmirror.com/file-type/-/file-type-21.3.4.tgz", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.16.0", "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
@@ -99,8 +192,18 @@
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hashery": ["hashery@1.5.1", "https://registry.npmmirror.com/hashery/-/hashery-1.5.1.tgz", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hookified": ["hookified@1.15.1", "https://registry.npmmirror.com/hookified/-/hookified-1.15.1.tgz", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"keyv": ["keyv@5.6.0", "https://registry.npmmirror.com/keyv/-/keyv-5.6.0.tgz", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
|
||||
|
||||
"libsignal": ["libsignal@github:whiskeysockets/libsignal-node#bcea72d", { "dependencies": { "curve25519-js": "^0.0.4", "protobufjs": "^7.5.5" } }, "WhiskeySockets-libsignal-node-bcea72d", "sha512-zKDaKfYGwQ/ALHPan2W2KZq/6njngElVNcqQQmQy0rl3fZbaqjiIchbPaJyCVwkTwHx1+s3Li1U/tf+f+AbCQA=="],
|
||||
|
||||
"lodash.identity": ["lodash.identity@3.0.0", "https://registry.npmmirror.com/lodash.identity/-/lodash.identity-3.0.0.tgz", {}, "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
@@ -109,24 +212,56 @@
|
||||
|
||||
"long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.5.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@2.0.0", "https://registry.npmmirror.com/media-typer/-/media-typer-2.0.0.tgz", {}, "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"music-metadata": ["music-metadata@11.13.0", "https://registry.npmmirror.com/music-metadata/-/music-metadata-11.13.0.tgz", { "dependencies": { "@borewit/text-codec": "^0.2.2", "@tokenizer/token": "^0.3.0", "content-type": "^2.0.0", "debug": "^4.4.3", "file-type": "^21.3.4", "media-typer": "^2.0.0", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0", "win-guid": "^0.2.1" } }, "sha512-uXRaov9dfjSpQufXIU7sMxVZnh+FilCQv2mXn+K5EJ/decP3dTWrgvPYa5r6MtRbieNSCE708Da4J0u1UGfQIw=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||
|
||||
"p-queue": ["p-queue@9.3.0", "https://registry.npmmirror.com/p-queue/-/p-queue-9.3.0.tgz", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang=="],
|
||||
|
||||
"p-timeout": ["p-timeout@7.0.1", "https://registry.npmmirror.com/p-timeout/-/p-timeout-7.0.1.tgz", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
|
||||
|
||||
"pino": ["pino@9.14.0", "https://registry.npmmirror.com/pino/-/pino-9.14.0.tgz", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
|
||||
|
||||
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
|
||||
|
||||
"pino-std-serializers": ["pino-std-serializers@7.1.0", "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
||||
|
||||
"process-warning": ["process-warning@5.0.0", "https://registry.npmmirror.com/process-warning/-/process-warning-5.0.0.tgz", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.5.6", "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.6.tgz", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"qified": ["qified@0.10.1", "https://registry.npmmirror.com/qified/-/qified-0.10.1.tgz", { "dependencies": { "hookified": "^2.1.1" } }, "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "https://registry.npmmirror.com/qs/-/qs-6.15.0.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||
|
||||
"real-require": ["real-require@0.2.0", "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"semver": ["semver@7.8.3", "https://registry.npmmirror.com/semver/-/semver-7.8.3.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
@@ -135,16 +270,34 @@
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.1", "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.1.tgz", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.5", "https://registry.npmmirror.com/strtok3/-/strtok3-10.3.5.tgz", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
|
||||
|
||||
"thread-stream": ["thread-stream@3.2.0", "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.2.0.tgz", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "https://registry.npmmirror.com/token-types/-/token-types-6.1.2.tgz", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"win-guid": ["win-guid@0.2.1", "https://registry.npmmirror.com/win-guid/-/win-guid-0.2.1.tgz", {}, "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A=="],
|
||||
|
||||
"ws": ["ws@8.20.0", "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
||||
|
||||
"@protobufjs/fetch/@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||
|
||||
"qified/hookified": ["hookified@2.2.0", "https://registry.npmmirror.com/hookified/-/hookified-2.2.0.tgz", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,10 @@ describe('adapter config defaults', () => {
|
||||
expect(config.feishu.defaultWorkDir).toBe(fs.realpathSync(workDir))
|
||||
expect(config.wechat.defaultWorkDir).toBe(fs.realpathSync(workDir))
|
||||
expect(config.dingtalk.defaultWorkDir).toBe(fs.realpathSync(workDir))
|
||||
expect(config.whatsapp.defaultWorkDir).toBe(fs.realpathSync(workDir))
|
||||
expect(getConfiguredWorkDir(config, config.wechat)).toBe(fs.realpathSync(workDir))
|
||||
expect(getConfiguredWorkDir(config, config.dingtalk)).toBe(fs.realpathSync(workDir))
|
||||
expect(getConfiguredWorkDir(config, config.whatsapp)).toBe(fs.realpathSync(workDir))
|
||||
} finally {
|
||||
fs.rmSync(configDir, { recursive: true, force: true })
|
||||
fs.rmSync(workDir, { recursive: true, force: true })
|
||||
@@ -57,8 +59,10 @@ describe('adapter config defaults', () => {
|
||||
|
||||
expect(getConfiguredWorkDir(config, config.wechat)).toBe(defaultProjectDir)
|
||||
expect(getConfiguredWorkDir(config, config.dingtalk)).toBe(defaultProjectDir)
|
||||
expect(getConfiguredWorkDir(config, config.whatsapp)).toBe(defaultProjectDir)
|
||||
expect(config.wechat.defaultWorkDir).toBe(fs.realpathSync(workDir))
|
||||
expect(config.dingtalk.defaultWorkDir).toBe(fs.realpathSync(workDir))
|
||||
expect(config.whatsapp.defaultWorkDir).toBe(fs.realpathSync(workDir))
|
||||
} finally {
|
||||
fs.rmSync(configDir, { recursive: true, force: true })
|
||||
fs.rmSync(defaultProjectDir, { recursive: true, force: true })
|
||||
@@ -79,6 +83,7 @@ describe('adapter config defaults', () => {
|
||||
|
||||
expect(getConfiguredWorkDir(config, config.wechat)).toBe(fs.realpathSync(defaultProjectDir))
|
||||
expect(getConfiguredWorkDir(config, config.dingtalk)).toBe(fs.realpathSync(defaultProjectDir))
|
||||
expect(getConfiguredWorkDir(config, config.whatsapp)).toBe(fs.realpathSync(defaultProjectDir))
|
||||
} finally {
|
||||
fs.rmSync(configDir, { recursive: true, force: true })
|
||||
fs.rmSync(defaultProjectDir, { recursive: true, force: true })
|
||||
@@ -102,6 +107,27 @@ describe('adapter config defaults', () => {
|
||||
fs.rmSync(configDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('loads WhatsApp auth dir from file or env', () => {
|
||||
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-config-'))
|
||||
const originalWhatsAppAuthDir = process.env.WHATSAPP_AUTH_DIR
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(configDir, 'adapters.json'),
|
||||
JSON.stringify({ whatsapp: { authDir: '~/custom-whatsapp-auth', accountJid: '15551234567@s.whatsapp.net' } }),
|
||||
)
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
delete process.env.WHATSAPP_AUTH_DIR
|
||||
expect(loadConfig().whatsapp.authDir).toBe(path.join(os.homedir(), 'custom-whatsapp-auth'))
|
||||
expect(loadConfig().whatsapp.accountJid).toBe('15551234567@s.whatsapp.net')
|
||||
|
||||
process.env.WHATSAPP_AUTH_DIR = path.join(configDir, 'wa-auth')
|
||||
expect(loadConfig().whatsapp.authDir).toBe(path.join(configDir, 'wa-auth'))
|
||||
} finally {
|
||||
restoreEnv('WHATSAPP_AUTH_DIR', originalWhatsAppAuthDir)
|
||||
fs.rmSync(configDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function restoreEnv(key: string, value: string | undefined): void {
|
||||
|
||||
@@ -19,4 +19,13 @@ describe('pairing platform support', () => {
|
||||
},
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('checks WhatsApp paired users with the same shared access rule', () => {
|
||||
expect(isPaired('whatsapp', '15551234567@s.whatsapp.net', {
|
||||
whatsapp: {
|
||||
pairedUsers: [{ userId: '15551234567@s.whatsapp.net', displayName: 'WhatsApp User', pairedAt: Date.now() }],
|
||||
allowedUsers: [],
|
||||
},
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { AttachmentRef } from '../ws-bridge.js'
|
||||
export type { AttachmentRef }
|
||||
|
||||
/** Platform tag — used for local staging subdir and telemetry. */
|
||||
export type ImPlatform = 'feishu' | 'telegram' | 'wechat' | 'dingtalk'
|
||||
export type ImPlatform = 'feishu' | 'telegram' | 'wechat' | 'dingtalk' | 'whatsapp'
|
||||
|
||||
/** Result of downloading an IM resource into the local stage dir. */
|
||||
export interface LocalAttachment {
|
||||
|
||||
@@ -58,6 +58,14 @@ export type DingtalkConfig = {
|
||||
permissionCardTemplateId: string
|
||||
}
|
||||
|
||||
export type WhatsAppConfig = {
|
||||
accountJid: string
|
||||
authDir: string
|
||||
allowedUsers: string[]
|
||||
pairedUsers: PairedUser[]
|
||||
defaultWorkDir: string
|
||||
}
|
||||
|
||||
export type AdapterConfig = {
|
||||
serverUrl: string
|
||||
defaultProjectDir: string
|
||||
@@ -66,6 +74,7 @@ export type AdapterConfig = {
|
||||
feishu: FeishuConfig
|
||||
wechat: WechatConfig
|
||||
dingtalk: DingtalkConfig
|
||||
whatsapp: WhatsAppConfig
|
||||
}
|
||||
|
||||
export type AdapterPlatformConfig =
|
||||
@@ -73,6 +82,7 @@ export type AdapterPlatformConfig =
|
||||
| FeishuConfig
|
||||
| WechatConfig
|
||||
| DingtalkConfig
|
||||
| WhatsAppConfig
|
||||
|
||||
function getConfigPath(): string {
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
||||
@@ -96,8 +106,12 @@ export function loadConfig(): AdapterConfig {
|
||||
const fs_ = file.feishu ?? {}
|
||||
const wc = file.wechat ?? {}
|
||||
const dt = file.dingtalk ?? {}
|
||||
const wa = file.whatsapp ?? {}
|
||||
const pairing = file.pairing ?? {}
|
||||
const fallbackWorkDir = resolveUserDefaultWorkDir()
|
||||
const whatsappAuthDir = resolveConfiguredPath(
|
||||
process.env.WHATSAPP_AUTH_DIR || wa.authDir || defaultWhatsAppAuthDir(),
|
||||
)
|
||||
|
||||
return {
|
||||
serverUrl: process.env.ADAPTER_SERVER_URL || file.serverUrl || 'ws://127.0.0.1:3456',
|
||||
@@ -141,6 +155,13 @@ export function loadConfig(): AdapterConfig {
|
||||
endpoint: process.env.DINGTALK_STREAM_ENDPOINT || dt.endpoint || 'https://api.dingtalk.com',
|
||||
permissionCardTemplateId: process.env.DINGTALK_PERMISSION_CARD_TEMPLATE_ID || dt.permissionCardTemplateId || '',
|
||||
},
|
||||
whatsapp: {
|
||||
accountJid: process.env.WHATSAPP_ACCOUNT_JID || wa.accountJid || '',
|
||||
authDir: whatsappAuthDir,
|
||||
allowedUsers: wa.allowedUsers ?? [],
|
||||
pairedUsers: wa.pairedUsers ?? [],
|
||||
defaultWorkDir: wa.defaultWorkDir || fallbackWorkDir,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,3 +203,19 @@ function resolveExistingDirectory(value: string | undefined): string | null {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function defaultWhatsAppAuthDir(): string {
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
||||
return path.join(configDir, 'whatsapp-auth', 'default')
|
||||
}
|
||||
|
||||
function resolveConfiguredPath(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return defaultWhatsAppAuthDir()
|
||||
const expanded = trimmed === '~'
|
||||
? os.homedir()
|
||||
: trimmed.startsWith('~/')
|
||||
? path.join(os.homedir(), trimmed.slice(2))
|
||||
: trimmed
|
||||
return path.resolve(expanded)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as crypto from 'node:crypto'
|
||||
import type { PairedUser, PairingState } from './config.js'
|
||||
|
||||
const SAFE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' // 排除 0/O/1/I/L
|
||||
export type ImPlatform = 'telegram' | 'feishu' | 'wechat' | 'dingtalk'
|
||||
export type ImPlatform = 'telegram' | 'feishu' | 'wechat' | 'dingtalk' | 'whatsapp'
|
||||
|
||||
// 速率限制:每个 userId 在 RATE_LIMIT_WINDOW_MS 内最多 RATE_LIMIT_MAX_ATTEMPTS 次失败尝试
|
||||
const RATE_LIMIT_WINDOW_MS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
@@ -8,15 +8,18 @@
|
||||
"feishu": "bun run feishu/index.ts",
|
||||
"wechat": "bun run wechat/index.ts",
|
||||
"dingtalk": "bun run dingtalk/index.ts",
|
||||
"whatsapp": "bun run whatsapp/index.ts",
|
||||
"test": "bun test",
|
||||
"test:common": "bun test common/",
|
||||
"test:telegram": "bun test telegram/",
|
||||
"test:feishu": "bun test feishu/",
|
||||
"test:wechat": "bun test wechat/",
|
||||
"test:dingtalk": "bun test dingtalk/"
|
||||
"test:dingtalk": "bun test dingtalk/",
|
||||
"test:whatsapp": "bun test whatsapp/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "^1.60.0",
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"dingtalk-stream": "2.1.4",
|
||||
"grammy": "^1.42.0",
|
||||
"ws": "^8.18.0"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import {
|
||||
buildWhatsAppThinkingPreview,
|
||||
formatWhatsAppOutboundText,
|
||||
splitWhatsAppText,
|
||||
} from '../format.js'
|
||||
|
||||
describe('WhatsApp message formatting', () => {
|
||||
it('converts markdown tables to mobile-friendly bullets', () => {
|
||||
const markdown = [
|
||||
'| Feature | Status |',
|
||||
'| --- | --- |',
|
||||
'| WhatsApp | Ready |',
|
||||
].join('\n')
|
||||
|
||||
expect(formatWhatsAppOutboundText(markdown)).toBe([
|
||||
'WhatsApp',
|
||||
'• Status: Ready',
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('splits long text within the WhatsApp chunk limit', () => {
|
||||
const chunks = splitWhatsAppText('a'.repeat(8100), 4000)
|
||||
|
||||
expect(chunks).toHaveLength(3)
|
||||
expect(chunks.every((chunk) => chunk.length <= 4000)).toBe(true)
|
||||
})
|
||||
|
||||
it('accumulates thinking preview text without losing the full value', () => {
|
||||
const first = buildWhatsAppThinkingPreview('', 'The user')
|
||||
const second = buildWhatsAppThinkingPreview(first.fullText, ' asks for WhatsApp')
|
||||
|
||||
expect(second.fullText).toBe('The user asks for WhatsApp')
|
||||
expect(second.messageText).toContain('The user asks for WhatsApp')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { convertMarkdownTablesToBullets, splitMessage } from '../common/format.js'
|
||||
|
||||
const DEFAULT_THINKING_PREVIEW_LIMIT = 800
|
||||
|
||||
export function formatWhatsAppOutboundText(text: string): string {
|
||||
return convertMarkdownTablesToBullets(text).trim()
|
||||
}
|
||||
|
||||
export function buildWhatsAppThinkingPreview(
|
||||
currentText: string,
|
||||
deltaText: string,
|
||||
previewLimit = DEFAULT_THINKING_PREVIEW_LIMIT,
|
||||
): { fullText: string; messageText: string } {
|
||||
const fullText = currentText + deltaText
|
||||
const preview = fullText.slice(0, Math.max(0, previewLimit)).trimStart()
|
||||
return {
|
||||
fullText,
|
||||
messageText: preview ? `思考中...\n${preview}` : '思考中...',
|
||||
}
|
||||
}
|
||||
|
||||
export function splitWhatsAppText(text: string, limit: number): string[] {
|
||||
return splitMessage(formatWhatsAppOutboundText(text), limit).filter((chunk) => chunk.trim())
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
/**
|
||||
* WhatsApp Adapter for Claude Code Desktop
|
||||
*
|
||||
* Uses WhatsApp Web via Baileys. Link the account from Desktop Settings first,
|
||||
* then run: bun run whatsapp
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import {
|
||||
normalizeMessageContent,
|
||||
type proto,
|
||||
} from '@whiskeysockets/baileys'
|
||||
import { WsBridge, type ServerMessage, type AttachmentRef } from '../common/ws-bridge.js'
|
||||
import { MessageDedup } from '../common/message-dedup.js'
|
||||
import { enqueue } from '../common/chat-queue.js'
|
||||
import { getConfiguredWorkDir, loadConfig } from '../common/config.js'
|
||||
import {
|
||||
formatImHelp,
|
||||
formatImStatus,
|
||||
formatPermissionRequest,
|
||||
} from '../common/format.js'
|
||||
import {
|
||||
formatPermissionDecisionStatus,
|
||||
formatPermissionInstructions,
|
||||
parsePermissionCommand,
|
||||
type PermissionDecision,
|
||||
} from '../common/permission.js'
|
||||
import { SessionStore } from '../common/session-store.js'
|
||||
import { AdapterHttpClient } from '../common/http-client.js'
|
||||
import { restoreStoredSessionBinding } from '../common/session-recovery.js'
|
||||
import { isAllowedUser, tryPair } from '../common/pairing.js'
|
||||
import { AttachmentStore } from '../common/attachment/attachment-store.js'
|
||||
import { checkAttachmentLimit } from '../common/attachment/attachment-limits.js'
|
||||
import { ImageBlockWatcher } from '../common/attachment/image-block-watcher.js'
|
||||
import type { PendingUpload } from '../common/attachment/attachment-types.js'
|
||||
import {
|
||||
closeWhatsAppSocket,
|
||||
createWhatsAppSocket,
|
||||
hasWhatsAppAuth,
|
||||
isWhatsAppLoggedOut,
|
||||
type WhatsAppSocket,
|
||||
} from './session.js'
|
||||
import { WhatsAppMediaService } from './media.js'
|
||||
import {
|
||||
buildWhatsAppThinkingPreview,
|
||||
formatWhatsAppOutboundText,
|
||||
splitWhatsAppText,
|
||||
} from './format.js'
|
||||
|
||||
const WHATSAPP_TEXT_LIMIT = 4000
|
||||
const RECONNECT_BASE_MS = 1000
|
||||
const RECONNECT_MAX_MS = 30_000
|
||||
|
||||
const config = loadConfig()
|
||||
const authDir = config.whatsapp.authDir
|
||||
if (!hasWhatsAppAuth(authDir)) {
|
||||
console.error('[WhatsApp] No linked WhatsApp account found. Bind with QR in Desktop Settings first.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const bridge = new WsBridge(config.serverUrl, 'whatsapp')
|
||||
const dedup = new MessageDedup()
|
||||
const sessionStore = new SessionStore()
|
||||
const defaultWorkDir = getConfiguredWorkDir(config, config.whatsapp)
|
||||
const httpClient = new AdapterHttpClient(config.serverUrl, { allowedProjectRoots: [defaultWorkDir] })
|
||||
const attachmentStore = new AttachmentStore()
|
||||
attachmentStore.gc().catch((err) => {
|
||||
console.warn('[WhatsApp] AttachmentStore.gc failed:', err instanceof Error ? err.message : err)
|
||||
})
|
||||
|
||||
let sock: WhatsAppSocket
|
||||
let media: WhatsAppMediaService
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let reconnectAttempts = 0
|
||||
let shuttingDown = false
|
||||
|
||||
const accumulatedText = new Map<string, string>()
|
||||
const accumulatedThinkingText = new Map<string, string>()
|
||||
const thinkingNotices = new Set<string>()
|
||||
const pendingProjectSelection = new Map<string, boolean>()
|
||||
const runtimeStates = new Map<string, ChatRuntimeState>()
|
||||
const pendingPermissions = new Map<string, Set<string>>()
|
||||
const imageWatchers = new Map<string, ImageBlockWatcher>()
|
||||
|
||||
type ChatRuntimeState = {
|
||||
state: 'idle' | 'thinking' | 'streaming' | 'tool_executing' | 'permission_pending'
|
||||
verb?: string
|
||||
model?: string
|
||||
pendingPermissionCount: number
|
||||
}
|
||||
|
||||
function getRuntimeState(chatId: string): ChatRuntimeState {
|
||||
let state = runtimeStates.get(chatId)
|
||||
if (!state) {
|
||||
state = { state: 'idle', pendingPermissionCount: 0 }
|
||||
runtimeStates.set(chatId, state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function getImageWatcher(chatId: string): ImageBlockWatcher {
|
||||
let watcher = imageWatchers.get(chatId)
|
||||
if (!watcher) {
|
||||
watcher = new ImageBlockWatcher()
|
||||
imageWatchers.set(chatId, watcher)
|
||||
}
|
||||
return watcher
|
||||
}
|
||||
|
||||
function clearTransientChatState(chatId: string): void {
|
||||
accumulatedText.delete(chatId)
|
||||
accumulatedThinkingText.delete(chatId)
|
||||
thinkingNotices.delete(chatId)
|
||||
const runtime = getRuntimeState(chatId)
|
||||
runtime.state = 'idle'
|
||||
runtime.verb = undefined
|
||||
runtime.pendingPermissionCount = 0
|
||||
pendingPermissions.delete(chatId)
|
||||
imageWatchers.delete(chatId)
|
||||
}
|
||||
|
||||
async function sendWhatsAppText(jid: string, text: string): Promise<void> {
|
||||
const chunks = splitWhatsAppText(text, WHATSAPP_TEXT_LIMIT)
|
||||
for (const chunk of chunks) {
|
||||
await sock.sendMessage(jid, { text: chunk })
|
||||
}
|
||||
}
|
||||
|
||||
async function sendHelp(jid: string): Promise<void> {
|
||||
await sendWhatsAppText(jid, `Claude Code WhatsApp 已就绪。\n\n${formatImHelp()}`)
|
||||
}
|
||||
|
||||
async function handlePermissionDecision(chatId: string, decision: PermissionDecision): Promise<void> {
|
||||
const pending = pendingPermissions.get(chatId)
|
||||
if (!pending?.has(decision.requestId)) {
|
||||
await sendWhatsAppText(chatId, `未找到待确认的权限请求:${decision.requestId}`)
|
||||
return
|
||||
}
|
||||
|
||||
const sent = bridge.sendPermissionResponse(chatId, decision.requestId, decision.allowed, decision.rule)
|
||||
if (sent) {
|
||||
pending.delete(decision.requestId)
|
||||
const runtime = getRuntimeState(chatId)
|
||||
runtime.pendingPermissionCount = Math.max(0, runtime.pendingPermissionCount - 1)
|
||||
}
|
||||
await sendWhatsAppText(
|
||||
chatId,
|
||||
sent ? `${formatPermissionDecisionStatus(decision)}。` : '权限响应发送失败,请检查会话状态。',
|
||||
)
|
||||
}
|
||||
|
||||
async function ensureExistingSession(chatId: string): Promise<{ sessionId: string; workDir: string } | null> {
|
||||
return await restoreStoredSessionBinding({
|
||||
chatId,
|
||||
bridge,
|
||||
sessionStore,
|
||||
httpClient,
|
||||
onServerMessage: (msg) => handleServerMessage(chatId, msg),
|
||||
logPrefix: '[WhatsApp]',
|
||||
clearTransientState: () => clearTransientChatState(chatId),
|
||||
})
|
||||
}
|
||||
|
||||
async function buildStatusText(chatId: string): Promise<string> {
|
||||
const stored = await ensureExistingSession(chatId)
|
||||
if (!stored) return formatImStatus(null)
|
||||
|
||||
const runtime = getRuntimeState(chatId)
|
||||
let projectName = path.basename(stored.workDir) || stored.workDir
|
||||
let branch: string | null = null
|
||||
|
||||
try {
|
||||
const gitInfo = await httpClient.getGitInfo(stored.sessionId)
|
||||
projectName = gitInfo.repoName || path.basename(gitInfo.workDir) || projectName
|
||||
branch = gitInfo.branch
|
||||
} catch {
|
||||
// best-effort status
|
||||
}
|
||||
|
||||
return formatImStatus({
|
||||
sessionId: stored.sessionId,
|
||||
projectName,
|
||||
branch,
|
||||
model: runtime.model,
|
||||
state: runtime.state,
|
||||
verb: runtime.verb,
|
||||
pendingPermissionCount: runtime.pendingPermissionCount,
|
||||
})
|
||||
}
|
||||
|
||||
async function ensureSession(chatId: string): Promise<boolean> {
|
||||
const stored = await ensureExistingSession(chatId)
|
||||
if (stored) return true
|
||||
|
||||
const workDir = defaultWorkDir
|
||||
if (workDir) {
|
||||
return await createSessionForChat(chatId, workDir)
|
||||
}
|
||||
|
||||
await showProjectPicker(chatId)
|
||||
return false
|
||||
}
|
||||
|
||||
async function createSessionForChat(chatId: string, workDir: string): Promise<boolean> {
|
||||
try {
|
||||
bridge.resetSession(chatId)
|
||||
const sessionId = await httpClient.createSession(workDir)
|
||||
sessionStore.set(chatId, sessionId, workDir)
|
||||
bridge.connectSession(chatId, sessionId)
|
||||
bridge.onServerMessage(chatId, (msg) => handleServerMessage(chatId, msg))
|
||||
const opened = await bridge.waitForOpen(chatId)
|
||||
if (!opened) {
|
||||
await sendWhatsAppText(chatId, '连接服务器超时,请重试。')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
await sendWhatsAppText(chatId, `无法创建会话: ${err instanceof Error ? err.message : String(err)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function showProjectPicker(chatId: string): Promise<void> {
|
||||
try {
|
||||
const projects = await httpClient.listRecentProjects()
|
||||
if (projects.length === 0) {
|
||||
await sendWhatsAppText(
|
||||
chatId,
|
||||
`没有找到最近的项目。发送 /new 会使用默认工作目录:${defaultWorkDir}\n也可以发送 /new /path/to/project 指定项目。`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const lines = projects.slice(0, 10).map((p, i) =>
|
||||
`${i + 1}. ${p.projectName}${p.branch ? ` (${p.branch})` : ''}\n ${p.realPath}`,
|
||||
)
|
||||
pendingProjectSelection.set(chatId, true)
|
||||
await sendWhatsAppText(
|
||||
chatId,
|
||||
`选择项目(回复编号):\n\n${lines.join('\n\n')}\n\n下次可直接 /new <编号、名称或绝对路径> 快速新建会话`,
|
||||
)
|
||||
} catch (err) {
|
||||
await sendWhatsAppText(chatId, `无法获取项目列表: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function dispatchOutboundMedia(chatId: string, pending: PendingUpload): Promise<void> {
|
||||
try {
|
||||
let buffer: Buffer
|
||||
let mime = 'image/png'
|
||||
switch (pending.source.kind) {
|
||||
case 'base64':
|
||||
buffer = Buffer.from(pending.source.data, 'base64')
|
||||
mime = pending.source.mime
|
||||
break
|
||||
case 'path':
|
||||
buffer = await fs.readFile(pending.source.path)
|
||||
mime = pending.source.mime ?? 'image/png'
|
||||
break
|
||||
case 'url': {
|
||||
const resp = await fetch(pending.source.url)
|
||||
if (!resp.ok) throw new Error(`fetch ${pending.source.url} -> ${resp.status}`)
|
||||
buffer = Buffer.from(await resp.arrayBuffer())
|
||||
mime = pending.source.mime ?? resp.headers.get('content-type') ?? 'image/png'
|
||||
break
|
||||
}
|
||||
}
|
||||
const check = checkAttachmentLimit('image', buffer.length, mime)
|
||||
if (!check.ok) {
|
||||
console.warn('[WhatsApp] Outbound image rejected:', check.hint)
|
||||
return
|
||||
}
|
||||
await media.sendMedia(chatId, buffer, mime, pending.alt)
|
||||
} catch (err) {
|
||||
console.error('[WhatsApp] dispatchOutboundMedia failed:', err instanceof Error ? err.message : err)
|
||||
}
|
||||
}
|
||||
|
||||
async function flushAccumulatedText(chatId: string): Promise<void> {
|
||||
const text = accumulatedText.get(chatId)
|
||||
if (!text?.trim()) {
|
||||
accumulatedText.delete(chatId)
|
||||
return
|
||||
}
|
||||
await sendWhatsAppText(chatId, formatWhatsAppOutboundText(text))
|
||||
accumulatedText.delete(chatId)
|
||||
}
|
||||
|
||||
async function handleServerMessage(chatId: string, msg: ServerMessage): Promise<void> {
|
||||
const runtime = getRuntimeState(chatId)
|
||||
|
||||
switch (msg.type) {
|
||||
case 'connected':
|
||||
break
|
||||
|
||||
case 'status':
|
||||
runtime.state = msg.state
|
||||
runtime.verb = typeof msg.verb === 'string' ? msg.verb : undefined
|
||||
if (msg.state === 'thinking' && !thinkingNotices.has(chatId)) {
|
||||
thinkingNotices.add(chatId)
|
||||
await sendWhatsAppText(chatId, '思考中...')
|
||||
}
|
||||
break
|
||||
|
||||
case 'content_start':
|
||||
if (msg.blockType === 'text') {
|
||||
accumulatedThinkingText.delete(chatId)
|
||||
}
|
||||
break
|
||||
|
||||
case 'content_delta':
|
||||
if (msg.text) {
|
||||
accumulatedThinkingText.delete(chatId)
|
||||
accumulatedText.set(chatId, (accumulatedText.get(chatId) ?? '') + msg.text)
|
||||
const newUploads = getImageWatcher(chatId).feed(msg.text)
|
||||
for (const pending of newUploads) {
|
||||
void dispatchOutboundMedia(chatId, pending)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'thinking': {
|
||||
const update = buildWhatsAppThinkingPreview(
|
||||
accumulatedThinkingText.get(chatId) ?? '',
|
||||
msg.text ?? '',
|
||||
)
|
||||
accumulatedThinkingText.set(chatId, update.fullText)
|
||||
break
|
||||
}
|
||||
|
||||
case 'permission_request': {
|
||||
runtime.pendingPermissionCount += 1
|
||||
runtime.state = 'permission_pending'
|
||||
const pending = pendingPermissions.get(chatId) ?? new Set<string>()
|
||||
pending.add(msg.requestId)
|
||||
pendingPermissions.set(chatId, pending)
|
||||
const text = `${formatPermissionRequest(msg.toolName, msg.input, msg.requestId)}\n\n${formatPermissionInstructions(msg.requestId)}`
|
||||
await sendWhatsAppText(chatId, text)
|
||||
break
|
||||
}
|
||||
|
||||
case 'message_complete':
|
||||
runtime.state = 'idle'
|
||||
runtime.verb = undefined
|
||||
thinkingNotices.delete(chatId)
|
||||
accumulatedThinkingText.delete(chatId)
|
||||
await flushAccumulatedText(chatId)
|
||||
break
|
||||
|
||||
case 'error':
|
||||
runtime.state = 'idle'
|
||||
runtime.verb = undefined
|
||||
thinkingNotices.delete(chatId)
|
||||
accumulatedThinkingText.delete(chatId)
|
||||
if (msg.message && /Invalid.*signature.*thinking/i.test(msg.message)) {
|
||||
const stored = sessionStore.get(chatId)
|
||||
const workDir = stored?.workDir || defaultWorkDir
|
||||
if (workDir) {
|
||||
await sendWhatsAppText(chatId, '会话上下文已失效,正在自动重建...')
|
||||
clearTransientChatState(chatId)
|
||||
bridge.resetSession(chatId)
|
||||
sessionStore.delete(chatId)
|
||||
const ok = await createSessionForChat(chatId, workDir)
|
||||
await sendWhatsAppText(chatId, ok ? '已重建会话,请重新发送消息。' : '重建会话失败,请发送 /new 手动新建。')
|
||||
} else {
|
||||
await sendWhatsAppText(chatId, '会话上下文已失效,请发送 /new 新建会话。')
|
||||
}
|
||||
} else {
|
||||
await sendWhatsAppText(chatId, `错误: ${msg.message}`)
|
||||
}
|
||||
break
|
||||
|
||||
case 'system_notification':
|
||||
if (msg.subtype === 'init' && msg.data && typeof msg.data === 'object') {
|
||||
const model = (msg.data as Record<string, unknown>).model
|
||||
if (typeof model === 'string' && model.trim()) {
|
||||
runtime.model = model
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function startNewSession(chatId: string, query?: string): Promise<void> {
|
||||
bridge.resetSession(chatId)
|
||||
sessionStore.delete(chatId)
|
||||
accumulatedText.delete(chatId)
|
||||
accumulatedThinkingText.delete(chatId)
|
||||
thinkingNotices.delete(chatId)
|
||||
pendingProjectSelection.delete(chatId)
|
||||
pendingPermissions.delete(chatId)
|
||||
runtimeStates.delete(chatId)
|
||||
imageWatchers.delete(chatId)
|
||||
|
||||
if (query) {
|
||||
try {
|
||||
const { project, ambiguous } = await httpClient.matchProject(query)
|
||||
if (project) {
|
||||
const ok = await createSessionForChat(chatId, project.realPath)
|
||||
if (ok) {
|
||||
await sendWhatsAppText(chatId, `已新建会话:${project.projectName}${project.branch ? ` (${project.branch})` : ''}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (ambiguous) {
|
||||
const list = ambiguous.map((p, i) => `${i + 1}. ${p.projectName} - ${p.realPath}`).join('\n')
|
||||
await sendWhatsAppText(chatId, `匹配到多个项目,请更精确:\n\n${list}`)
|
||||
return
|
||||
}
|
||||
await sendWhatsAppText(chatId, `未找到匹配 "${query}" 的项目。发送 /projects 查看完整列表。`)
|
||||
} catch (err) {
|
||||
await sendWhatsAppText(chatId, `错误: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
} else {
|
||||
const workDir = defaultWorkDir
|
||||
if (workDir) {
|
||||
const ok = await createSessionForChat(chatId, workDir)
|
||||
if (ok) await sendWhatsAppText(chatId, '已新建会话,可以开始对话了。')
|
||||
} else {
|
||||
await showProjectPicker(chatId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function routeUserMessage(
|
||||
chatId: string,
|
||||
userId: string,
|
||||
displayName: string,
|
||||
text: string,
|
||||
attachments: AttachmentRef[],
|
||||
): Promise<void> {
|
||||
if (!isAllowedUser('whatsapp', userId)) {
|
||||
const success = attachments.length === 0
|
||||
? tryPair(text.trim(), { userId, displayName }, 'whatsapp')
|
||||
: false
|
||||
await sendWhatsAppText(
|
||||
chatId,
|
||||
success
|
||||
? '配对成功!现在可以开始聊天了。\n\n发送消息即可与 Claude 对话。'
|
||||
: '未授权。请在 Claude Code 桌面端生成配对码后发送给我。',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
enqueue(chatId, async () => {
|
||||
const command = text.trim()
|
||||
if (command === '/start' || command === '/help') {
|
||||
await sendHelp(chatId)
|
||||
return
|
||||
}
|
||||
if (command.startsWith('/new')) {
|
||||
await startNewSession(chatId, command.slice('/new'.length).trim() || undefined)
|
||||
return
|
||||
}
|
||||
if (command === '/projects') {
|
||||
await showProjectPicker(chatId)
|
||||
return
|
||||
}
|
||||
if (command === '/stop') {
|
||||
const stored = await ensureExistingSession(chatId)
|
||||
if (!stored) {
|
||||
await sendWhatsAppText(chatId, formatImStatus(null))
|
||||
return
|
||||
}
|
||||
bridge.sendStopGeneration(chatId)
|
||||
await sendWhatsAppText(chatId, '已发送停止信号。')
|
||||
return
|
||||
}
|
||||
if (command === '/status') {
|
||||
await sendWhatsAppText(chatId, await buildStatusText(chatId))
|
||||
return
|
||||
}
|
||||
if (command === '/clear') {
|
||||
const stored = await ensureExistingSession(chatId)
|
||||
if (!stored) {
|
||||
await sendWhatsAppText(chatId, formatImStatus(null))
|
||||
return
|
||||
}
|
||||
clearTransientChatState(chatId)
|
||||
const sent = bridge.sendUserMessage(chatId, '/clear')
|
||||
await sendWhatsAppText(chatId, sent ? '已清空当前会话上下文。' : '无法发送 /clear,请先发送 /new 重新连接会话。')
|
||||
return
|
||||
}
|
||||
|
||||
const permissionDecision = attachments.length === 0
|
||||
? parsePermissionCommand(text, pendingPermissions.get(chatId))
|
||||
: null
|
||||
if (permissionDecision) {
|
||||
await handlePermissionDecision(chatId, permissionDecision)
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingProjectSelection.has(chatId)) {
|
||||
if (text.trim()) await startNewSession(chatId, text.trim())
|
||||
return
|
||||
}
|
||||
|
||||
const ready = await ensureSession(chatId)
|
||||
if (!ready) return
|
||||
const effective = text || (attachments.length > 0 ? '(用户发送了附件)' : '')
|
||||
if (!effective && attachments.length === 0) return
|
||||
const sent = bridge.sendUserMessage(chatId, effective, attachments.length ? attachments : undefined)
|
||||
if (!sent) {
|
||||
await sendWhatsAppText(chatId, '消息发送失败,连接可能已断开。请发送 /new 重新开始。')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function collectAttachments(
|
||||
message: proto.IWebMessageInfo,
|
||||
chatId: string,
|
||||
): Promise<{ attachments: AttachmentRef[]; rejections: string[] }> {
|
||||
const sessionId = sessionStore.get(chatId)?.sessionId ?? chatId
|
||||
const attachments: AttachmentRef[] = []
|
||||
const rejections: string[] = []
|
||||
try {
|
||||
const local = await media.downloadMessageMedia(message, sessionId)
|
||||
if (!local) return { attachments, rejections }
|
||||
const check = checkAttachmentLimit(local.kind, local.size, local.mimeType)
|
||||
if (!check.ok) {
|
||||
rejections.push(check.hint)
|
||||
return { attachments, rejections }
|
||||
}
|
||||
if (local.kind === 'image') {
|
||||
attachments.push({
|
||||
type: 'image',
|
||||
name: local.name,
|
||||
data: local.buffer.toString('base64'),
|
||||
mimeType: local.mimeType,
|
||||
})
|
||||
} else {
|
||||
attachments.push({
|
||||
type: 'file',
|
||||
name: local.name,
|
||||
path: local.path,
|
||||
mimeType: local.mimeType,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[WhatsApp] download media failed:', err instanceof Error ? err.message : err)
|
||||
rejections.push('附件下载失败,请稍后重试。')
|
||||
}
|
||||
return { attachments, rejections }
|
||||
}
|
||||
|
||||
function extractText(raw: proto.IMessage | undefined): string {
|
||||
const msg = normalizeMessageContent(raw)
|
||||
if (!msg) return ''
|
||||
return msg.conversation
|
||||
?? msg.extendedTextMessage?.text
|
||||
?? msg.imageMessage?.caption
|
||||
?? msg.videoMessage?.caption
|
||||
?? msg.documentMessage?.caption
|
||||
?? ''
|
||||
}
|
||||
|
||||
function isDirectChat(jid: string): boolean {
|
||||
return jid.endsWith('@s.whatsapp.net') || jid.endsWith('@lid')
|
||||
}
|
||||
|
||||
async function handleIncomingMessage(message: proto.IWebMessageInfo): Promise<void> {
|
||||
const key = message.key
|
||||
if (!key) return
|
||||
const chatId = key.remoteJid
|
||||
const messageId = key.id
|
||||
if (!chatId || !messageId || key.fromMe) return
|
||||
if (chatId === 'status@broadcast' || chatId.endsWith('@broadcast') || !isDirectChat(chatId)) return
|
||||
if (!dedup.tryRecord(`${chatId}:${messageId}`)) return
|
||||
|
||||
const text = extractText(message.message as proto.IMessage | undefined)
|
||||
const { attachments, rejections } = await collectAttachments(message, chatId)
|
||||
for (const rejection of rejections) {
|
||||
await sendWhatsAppText(chatId, rejection).catch(() => {})
|
||||
}
|
||||
if (!text.trim() && attachments.length === 0) return
|
||||
|
||||
await sock.readMessages([{ remoteJid: chatId, id: messageId, fromMe: false }]).catch(() => {})
|
||||
await sock.sendPresenceUpdate('composing', chatId).catch(() => {})
|
||||
await routeUserMessage(
|
||||
chatId,
|
||||
chatId,
|
||||
message.pushName || chatId,
|
||||
text,
|
||||
attachments,
|
||||
)
|
||||
}
|
||||
|
||||
async function startSocket(): Promise<void> {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
sock = await createWhatsAppSocket({ authDir })
|
||||
media = new WhatsAppMediaService(sock, attachmentStore)
|
||||
|
||||
sock.ev.on('messages.upsert', ({ type, messages }) => {
|
||||
if (type !== 'notify') return
|
||||
for (const message of messages) {
|
||||
void handleIncomingMessage(message)
|
||||
}
|
||||
})
|
||||
|
||||
sock.ev.on('connection.update', (update) => {
|
||||
if (update.connection === 'open') {
|
||||
reconnectAttempts = 0
|
||||
const connectedId = sock.user?.id ?? config.whatsapp.accountJid ?? 'unknown'
|
||||
console.log(`[WhatsApp] Connected as ${connectedId}`)
|
||||
return
|
||||
}
|
||||
if (update.connection !== 'close' || shuttingDown) return
|
||||
if (isWhatsAppLoggedOut(update.lastDisconnect?.error)) {
|
||||
console.error('[WhatsApp] Account logged out. Rebind with QR in Desktop Settings.')
|
||||
process.exit(1)
|
||||
}
|
||||
scheduleReconnect()
|
||||
})
|
||||
}
|
||||
|
||||
function scheduleReconnect(): void {
|
||||
if (reconnectTimer) return
|
||||
const delay = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** reconnectAttempts)
|
||||
reconnectAttempts += 1
|
||||
console.warn(`[WhatsApp] Connection closed. Reconnecting in ${delay}ms...`)
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null
|
||||
startSocket().catch((err) => {
|
||||
console.error('[WhatsApp] Reconnect failed:', err instanceof Error ? err.message : err)
|
||||
scheduleReconnect()
|
||||
})
|
||||
}, delay)
|
||||
}
|
||||
|
||||
console.log('[WhatsApp] Starting adapter...')
|
||||
console.log(`[WhatsApp] Server: ${config.serverUrl}`)
|
||||
console.log(`[WhatsApp] Auth dir: ${authDir}`)
|
||||
console.log(`[WhatsApp] Allowed users: ${config.whatsapp.allowedUsers.length === 0 ? 'paired users only' : config.whatsapp.allowedUsers.join(', ')}`)
|
||||
|
||||
await startSocket()
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('[WhatsApp] Shutting down...')
|
||||
shuttingDown = true
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||
closeWhatsAppSocket(sock, 'SIGINT')
|
||||
bridge.destroy()
|
||||
dedup.destroy()
|
||||
process.exit(0)
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import * as path from 'node:path'
|
||||
import {
|
||||
downloadMediaMessage,
|
||||
normalizeMessageContent,
|
||||
type WAMessage,
|
||||
type proto,
|
||||
} from '@whiskeysockets/baileys'
|
||||
import type { WhatsAppSocket } from './session.js'
|
||||
import { AttachmentStore } from '../common/attachment/attachment-store.js'
|
||||
import type { LocalAttachment } from '../common/attachment/attachment-types.js'
|
||||
|
||||
function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | undefined {
|
||||
return normalizeMessageContent(message)
|
||||
}
|
||||
|
||||
function resolveMediaMime(message: proto.IMessage): string | undefined {
|
||||
return message.imageMessage?.mimetype
|
||||
?? message.videoMessage?.mimetype
|
||||
?? message.documentMessage?.mimetype
|
||||
?? message.audioMessage?.mimetype
|
||||
?? message.stickerMessage?.mimetype
|
||||
?? (message.audioMessage ? 'audio/ogg; codecs=opus' : undefined)
|
||||
?? (message.imageMessage ? 'image/jpeg' : undefined)
|
||||
?? (message.videoMessage ? 'video/mp4' : undefined)
|
||||
?? (message.stickerMessage ? 'image/webp' : undefined)
|
||||
}
|
||||
|
||||
function resolveMediaName(message: proto.IMessage, mimeType: string): string {
|
||||
const explicit = message.documentMessage?.fileName
|
||||
if (explicit?.trim()) return path.basename(explicit)
|
||||
if (message.imageMessage) return `whatsapp-image.${extensionForMime(mimeType, 'jpg')}`
|
||||
if (message.videoMessage) return `whatsapp-video.${extensionForMime(mimeType, 'mp4')}`
|
||||
if (message.audioMessage) return `whatsapp-audio.${extensionForMime(mimeType, 'ogg')}`
|
||||
if (message.stickerMessage) return `whatsapp-sticker.${extensionForMime(mimeType, 'webp')}`
|
||||
return `whatsapp-file.${extensionForMime(mimeType, 'bin')}`
|
||||
}
|
||||
|
||||
function extensionForMime(mimeType: string, fallback: string): string {
|
||||
const normalized = mimeType.split(';')[0]!.trim().toLowerCase()
|
||||
switch (normalized) {
|
||||
case 'image/jpeg':
|
||||
return 'jpg'
|
||||
case 'image/png':
|
||||
return 'png'
|
||||
case 'image/gif':
|
||||
return 'gif'
|
||||
case 'image/webp':
|
||||
return 'webp'
|
||||
case 'video/mp4':
|
||||
return 'mp4'
|
||||
case 'audio/ogg':
|
||||
return 'ogg'
|
||||
case 'audio/mpeg':
|
||||
return 'mp3'
|
||||
case 'application/pdf':
|
||||
return 'pdf'
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export class WhatsAppMediaService {
|
||||
constructor(
|
||||
private sock: WhatsAppSocket,
|
||||
private store: AttachmentStore,
|
||||
) {}
|
||||
|
||||
async downloadMessageMedia(message: proto.IWebMessageInfo, sessionId: string): Promise<LocalAttachment | null> {
|
||||
const content = unwrapMessage(message.message as proto.IMessage | undefined)
|
||||
if (!content) return null
|
||||
const hasMedia = Boolean(
|
||||
content.imageMessage ||
|
||||
content.videoMessage ||
|
||||
content.documentMessage ||
|
||||
content.audioMessage ||
|
||||
content.stickerMessage,
|
||||
)
|
||||
if (!hasMedia) return null
|
||||
|
||||
const mimeType = resolveMediaMime(content) ?? 'application/octet-stream'
|
||||
const buffer = await downloadMediaMessage(
|
||||
message as WAMessage,
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
reuploadRequest: this.sock.updateMediaMessage,
|
||||
logger: this.sock.logger,
|
||||
},
|
||||
) as Buffer
|
||||
const kind = mimeType.startsWith('image/') ? 'image' : 'file'
|
||||
const name = resolveMediaName(content, mimeType)
|
||||
const target = this.store.resolvePath('whatsapp', sessionId, name)
|
||||
await this.store.write(target, buffer)
|
||||
return {
|
||||
kind,
|
||||
name,
|
||||
path: target,
|
||||
size: buffer.length,
|
||||
mimeType,
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
|
||||
async sendMedia(jid: string, buffer: Buffer, mimeType: string, caption?: string, fileName?: string): Promise<void> {
|
||||
if (mimeType.startsWith('image/')) {
|
||||
await this.sock.sendMessage(jid, { image: buffer, caption, mimetype: mimeType })
|
||||
return
|
||||
}
|
||||
if (mimeType.startsWith('video/')) {
|
||||
await this.sock.sendMessage(jid, { video: buffer, caption, mimetype: mimeType })
|
||||
return
|
||||
}
|
||||
if (mimeType.startsWith('audio/')) {
|
||||
const effective = mimeType === 'audio/ogg' ? 'audio/ogg; codecs=opus' : mimeType
|
||||
await this.sock.sendMessage(jid, { audio: buffer, ptt: true, mimetype: effective })
|
||||
return
|
||||
}
|
||||
await this.sock.sendMessage(jid, {
|
||||
document: buffer,
|
||||
fileName: fileName || 'file',
|
||||
caption,
|
||||
mimetype: mimeType || 'application/octet-stream',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import * as crypto from 'node:crypto'
|
||||
import * as path from 'node:path'
|
||||
import {
|
||||
clearWhatsAppAuth,
|
||||
closeWhatsAppSocket,
|
||||
createWhatsAppSocket,
|
||||
isWhatsAppLoggedOut,
|
||||
waitForWhatsAppCredsSave,
|
||||
type WhatsAppSocket,
|
||||
} from './session.js'
|
||||
|
||||
export type WhatsAppLoginStartResult = {
|
||||
sessionKey: string
|
||||
qr?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type WhatsAppLoginPollResult =
|
||||
| {
|
||||
connected: true
|
||||
accountJid: string
|
||||
authDir: string
|
||||
message: string
|
||||
}
|
||||
| {
|
||||
connected: false
|
||||
status: 'waiting' | 'expired' | 'error'
|
||||
qr?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
type LoginSession = {
|
||||
authDir: string
|
||||
sock: WhatsAppSocket
|
||||
qr?: string
|
||||
connected: boolean
|
||||
accountJid?: string
|
||||
error?: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
const LOGIN_TTL_MS = 2 * 60 * 1000
|
||||
const WAIT_FOR_QR_MS = 20_000
|
||||
const sessions = new Map<string, LoginSession>()
|
||||
|
||||
export async function startWhatsAppLoginWithQr(options: {
|
||||
authDir: string
|
||||
force?: boolean
|
||||
}): Promise<WhatsAppLoginStartResult> {
|
||||
cleanupExpiredSessions()
|
||||
const authDir = path.resolve(options.authDir)
|
||||
if (options.force) {
|
||||
closeSessionsForAuthDir(authDir)
|
||||
clearWhatsAppAuth(authDir)
|
||||
}
|
||||
|
||||
const sessionKey = crypto.randomUUID()
|
||||
const session: LoginSession = {
|
||||
authDir,
|
||||
sock: undefined as unknown as WhatsAppSocket,
|
||||
connected: false,
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
const qrPromise = waitForQr(session)
|
||||
const sock = await createWhatsAppSocket({
|
||||
authDir,
|
||||
onQr: (qr) => {
|
||||
session.qr = qr
|
||||
},
|
||||
})
|
||||
session.sock = sock
|
||||
sessions.set(sessionKey, session)
|
||||
|
||||
sock.ev.on('connection.update', async (update) => {
|
||||
if (update.qr) {
|
||||
session.qr = update.qr
|
||||
}
|
||||
if (update.connection === 'open') {
|
||||
await waitForWhatsAppCredsSave(authDir)
|
||||
session.connected = true
|
||||
session.accountJid = sock.user?.id ?? ''
|
||||
}
|
||||
if (update.connection === 'close') {
|
||||
if (isWhatsAppLoggedOut(update.lastDisconnect?.error)) {
|
||||
session.error = 'WhatsApp session logged out. Please scan again.'
|
||||
} else if (!session.connected) {
|
||||
session.error = 'WhatsApp login connection closed. Please retry.'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await qrPromise
|
||||
return {
|
||||
sessionKey,
|
||||
qr: session.qr,
|
||||
message: session.qr
|
||||
? 'Scan this QR in WhatsApp > Linked devices.'
|
||||
: 'Waiting for WhatsApp QR code...',
|
||||
}
|
||||
}
|
||||
|
||||
export async function pollWhatsAppLoginWithQr(options: {
|
||||
sessionKey: string
|
||||
}): Promise<WhatsAppLoginPollResult> {
|
||||
cleanupExpiredSessions()
|
||||
const session = sessions.get(options.sessionKey)
|
||||
if (!session) {
|
||||
return {
|
||||
connected: false,
|
||||
status: 'expired',
|
||||
message: 'WhatsApp login session expired. Generate a new QR code.',
|
||||
}
|
||||
}
|
||||
|
||||
if (session.connected) {
|
||||
await waitForWhatsAppCredsSave(session.authDir)
|
||||
const accountJid = session.accountJid || session.sock.user?.id || ''
|
||||
closeWhatsAppSocket(session.sock, 'WhatsApp login complete')
|
||||
sessions.delete(options.sessionKey)
|
||||
return {
|
||||
connected: true,
|
||||
accountJid,
|
||||
authDir: session.authDir,
|
||||
message: 'WhatsApp linked successfully.',
|
||||
}
|
||||
}
|
||||
|
||||
if (session.error) {
|
||||
closeWhatsAppSocket(session.sock, 'WhatsApp login error')
|
||||
sessions.delete(options.sessionKey)
|
||||
return {
|
||||
connected: false,
|
||||
status: 'error',
|
||||
message: session.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connected: false,
|
||||
status: 'waiting',
|
||||
qr: session.qr,
|
||||
message: session.qr
|
||||
? 'Waiting for WhatsApp scan confirmation...'
|
||||
: 'Waiting for WhatsApp QR code...',
|
||||
}
|
||||
}
|
||||
|
||||
export async function logoutWhatsAppAuth(authDir: string): Promise<void> {
|
||||
closeSessionsForAuthDir(path.resolve(authDir))
|
||||
clearWhatsAppAuth(authDir)
|
||||
}
|
||||
|
||||
function waitForQr(session: LoginSession): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const started = Date.now()
|
||||
const timer = setInterval(() => {
|
||||
if (session.qr || session.connected || Date.now() - started > WAIT_FOR_QR_MS) {
|
||||
clearInterval(timer)
|
||||
resolve()
|
||||
}
|
||||
}, 250)
|
||||
})
|
||||
}
|
||||
|
||||
function cleanupExpiredSessions(): void {
|
||||
const now = Date.now()
|
||||
for (const [sessionKey, session] of sessions) {
|
||||
if (now - session.createdAt <= LOGIN_TTL_MS) continue
|
||||
closeWhatsAppSocket(session.sock, 'WhatsApp login expired')
|
||||
sessions.delete(sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
function closeSessionsForAuthDir(authDir: string): void {
|
||||
for (const [sessionKey, session] of sessions) {
|
||||
if (path.resolve(session.authDir) !== authDir) continue
|
||||
closeWhatsAppSocket(session.sock, 'WhatsApp login superseded')
|
||||
sessions.delete(sessionKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import {
|
||||
DisconnectReason,
|
||||
fetchLatestBaileysVersion,
|
||||
makeCacheableSignalKeyStore,
|
||||
makeWASocket,
|
||||
useMultiFileAuthState,
|
||||
} from '@whiskeysockets/baileys'
|
||||
|
||||
export type WhatsAppSocket = ReturnType<typeof makeWASocket>
|
||||
|
||||
const CREDS_FILE = 'creds.json'
|
||||
const CREDS_BACKUP_FILE = 'creds.json.bak'
|
||||
const LOGGED_OUT_STATUS = DisconnectReason?.loggedOut ?? 401
|
||||
|
||||
const credsSaveQueues = new Map<string, Promise<void>>()
|
||||
|
||||
export function hasWhatsAppAuth(authDir: string): boolean {
|
||||
return fs.existsSync(resolveCredsPath(authDir))
|
||||
}
|
||||
|
||||
export function clearWhatsAppAuth(authDir: string): void {
|
||||
fs.rmSync(authDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
export function getWhatsAppDisconnectStatus(error: unknown): number | undefined {
|
||||
const candidate = error as {
|
||||
output?: { statusCode?: unknown }
|
||||
statusCode?: unknown
|
||||
} | null
|
||||
const status = candidate?.output?.statusCode ?? candidate?.statusCode
|
||||
return typeof status === 'number' ? status : undefined
|
||||
}
|
||||
|
||||
export function isWhatsAppLoggedOut(error: unknown): boolean {
|
||||
return getWhatsAppDisconnectStatus(error) === LOGGED_OUT_STATUS
|
||||
}
|
||||
|
||||
export async function createWhatsAppSocket(options: {
|
||||
authDir: string
|
||||
onQr?: (qr: string) => void
|
||||
verbose?: boolean
|
||||
}): Promise<WhatsAppSocket> {
|
||||
const authDir = path.resolve(options.authDir)
|
||||
fs.mkdirSync(authDir, { recursive: true, mode: 0o700 })
|
||||
maybeRestoreCredsFromBackup(authDir)
|
||||
|
||||
const logger = makeBaileysLogger(options.verbose ? 'info' : 'silent')
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authDir)
|
||||
const { version } = await fetchLatestBaileysVersion()
|
||||
const sock = makeWASocket({
|
||||
auth: {
|
||||
creds: state.creds,
|
||||
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
||||
},
|
||||
version,
|
||||
logger,
|
||||
printQRInTerminal: false,
|
||||
browser: ['cc-haha', 'desktop', '1.0'],
|
||||
syncFullHistory: false,
|
||||
markOnlineOnConnect: false,
|
||||
})
|
||||
|
||||
sock.ev.on('creds.update', () => enqueueSaveCreds(authDir, saveCreds))
|
||||
if (options.onQr) {
|
||||
sock.ev.on('connection.update', (update) => {
|
||||
if (update.qr) options.onQr?.(update.qr)
|
||||
})
|
||||
}
|
||||
const ws = sock.ws as unknown as { on?: (event: string, handler: (err: Error) => void) => void }
|
||||
ws.on?.('error', (err) => {
|
||||
console.error('[WhatsApp] WebSocket error:', err.message)
|
||||
})
|
||||
|
||||
return sock
|
||||
}
|
||||
|
||||
export function closeWhatsAppSocket(sock: WhatsAppSocket, reason = 'closed'): void {
|
||||
try {
|
||||
const closable = sock as unknown as { end?: (error?: Error) => void }
|
||||
closable.end?.(new Error(reason))
|
||||
} catch {
|
||||
// best-effort shutdown
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForWhatsAppCredsSave(authDir: string): Promise<void> {
|
||||
await (credsSaveQueues.get(path.resolve(authDir)) ?? Promise.resolve())
|
||||
}
|
||||
|
||||
function resolveCredsPath(authDir: string): string {
|
||||
return path.join(path.resolve(authDir), CREDS_FILE)
|
||||
}
|
||||
|
||||
function resolveCredsBackupPath(authDir: string): string {
|
||||
return path.join(path.resolve(authDir), CREDS_BACKUP_FILE)
|
||||
}
|
||||
|
||||
function maybeRestoreCredsFromBackup(authDir: string): void {
|
||||
const credsPath = resolveCredsPath(authDir)
|
||||
const backupPath = resolveCredsBackupPath(authDir)
|
||||
if (isValidJsonFile(credsPath) || !isValidJsonFile(backupPath)) return
|
||||
fs.copyFileSync(backupPath, credsPath)
|
||||
fs.chmodSync(credsPath, 0o600)
|
||||
}
|
||||
|
||||
function enqueueSaveCreds(authDir: string, saveCreds: () => Promise<void> | void): void {
|
||||
const resolved = path.resolve(authDir)
|
||||
const prev = credsSaveQueues.get(resolved) ?? Promise.resolve()
|
||||
const next = prev
|
||||
.then(() => safeSaveCreds(resolved, saveCreds))
|
||||
.catch((err) => {
|
||||
console.warn('[WhatsApp] Failed to save credentials:', err instanceof Error ? err.message : err)
|
||||
})
|
||||
.finally(() => {
|
||||
if (credsSaveQueues.get(resolved) === next) {
|
||||
credsSaveQueues.delete(resolved)
|
||||
}
|
||||
})
|
||||
credsSaveQueues.set(resolved, next)
|
||||
}
|
||||
|
||||
async function safeSaveCreds(authDir: string, saveCreds: () => Promise<void> | void): Promise<void> {
|
||||
backupCreds(authDir)
|
||||
await Promise.resolve(saveCreds())
|
||||
fs.chmodSync(resolveCredsPath(authDir), 0o600)
|
||||
}
|
||||
|
||||
function backupCreds(authDir: string): void {
|
||||
const credsPath = resolveCredsPath(authDir)
|
||||
if (!isValidJsonFile(credsPath)) return
|
||||
try {
|
||||
const backupPath = resolveCredsBackupPath(authDir)
|
||||
fs.copyFileSync(credsPath, backupPath)
|
||||
fs.chmodSync(backupPath, 0o600)
|
||||
} catch {
|
||||
// keep the previous backup
|
||||
}
|
||||
}
|
||||
|
||||
function isValidJsonFile(filePath: string): boolean {
|
||||
try {
|
||||
JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function makeBaileysLogger(level: 'silent' | 'info'): any {
|
||||
const noop = () => {}
|
||||
const sink = level === 'silent'
|
||||
? { trace: noop, debug: noop, info: noop, warn: noop, error: noop, fatal: noop }
|
||||
: {
|
||||
trace: console.debug.bind(console),
|
||||
debug: console.debug.bind(console),
|
||||
info: console.info.bind(console),
|
||||
warn: console.warn.bind(console),
|
||||
error: console.error.bind(console),
|
||||
fatal: console.error.bind(console),
|
||||
}
|
||||
return {
|
||||
level,
|
||||
child: () => makeBaileysLogger(level),
|
||||
...sink,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user