@ecat/weixin-bot-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +77 -0
- package/dist/api/api.d.ts +47 -0
- package/dist/api/api.js +233 -0
- package/dist/api/api.js.map +1 -0
- package/dist/api/config-cache.d.ts +18 -0
- package/dist/api/config-cache.js +64 -0
- package/dist/api/config-cache.js.map +1 -0
- package/dist/api/session-guard.d.ts +15 -0
- package/dist/api/session-guard.js +49 -0
- package/dist/api/session-guard.js.map +1 -0
- package/dist/api/types.d.ts +201 -0
- package/dist/api/types.js +35 -0
- package/dist/api/types.js.map +1 -0
- package/dist/auth/accounts.d.ts +30 -0
- package/dist/auth/accounts.js +158 -0
- package/dist/auth/accounts.js.map +1 -0
- package/dist/auth/login-qr.d.ts +31 -0
- package/dist/auth/login-qr.js +235 -0
- package/dist/auth/login-qr.js.map +1 -0
- package/dist/cdn/aes-ecb.d.ts +6 -0
- package/dist/cdn/aes-ecb.js +19 -0
- package/dist/cdn/aes-ecb.js.map +1 -0
- package/dist/cdn/cdn-upload.d.ts +17 -0
- package/dist/cdn/cdn-upload.js +73 -0
- package/dist/cdn/cdn-upload.js.map +1 -0
- package/dist/cdn/cdn-url.d.ts +13 -0
- package/dist/cdn/cdn-url.js +14 -0
- package/dist/cdn/cdn-url.js.map +1 -0
- package/dist/cdn/pic-decrypt.d.ts +9 -0
- package/dist/cdn/pic-decrypt.js +89 -0
- package/dist/cdn/pic-decrypt.js.map +1 -0
- package/dist/cdn/upload.d.ts +42 -0
- package/dist/cdn/upload.js +106 -0
- package/dist/cdn/upload.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +127 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/config-schema.d.ts +16 -0
- package/dist/config/config-schema.js +17 -0
- package/dist/config/config-schema.js.map +1 -0
- package/dist/media/media-download.d.ts +18 -0
- package/dist/media/media-download.js +95 -0
- package/dist/media/media-download.js.map +1 -0
- package/dist/media/mime.d.ts +6 -0
- package/dist/media/mime.js +73 -0
- package/dist/media/mime.js.map +1 -0
- package/dist/media/silk-transcode.d.ts +8 -0
- package/dist/media/silk-transcode.js +64 -0
- package/dist/media/silk-transcode.js.map +1 -0
- package/dist/messaging/debug-mode.d.ts +9 -0
- package/dist/messaging/debug-mode.js +63 -0
- package/dist/messaging/debug-mode.js.map +1 -0
- package/dist/messaging/inbound.d.ts +69 -0
- package/dist/messaging/inbound.js +201 -0
- package/dist/messaging/inbound.js.map +1 -0
- package/dist/messaging/send-media.d.ts +21 -0
- package/dist/messaging/send-media.js +54 -0
- package/dist/messaging/send-media.js.map +1 -0
- package/dist/messaging/send.d.ts +70 -0
- package/dist/messaging/send.js +203 -0
- package/dist/messaging/send.js.map +1 -0
- package/dist/monitor/monitor.d.ts +12 -0
- package/dist/monitor/monitor.js +145 -0
- package/dist/monitor/monitor.js.map +1 -0
- package/dist/storage/state-dir.d.ts +2 -0
- package/dist/storage/state-dir.js +8 -0
- package/dist/storage/state-dir.js.map +1 -0
- package/dist/storage/sync-buf.d.ts +20 -0
- package/dist/storage/sync-buf.js +64 -0
- package/dist/storage/sync-buf.js.map +1 -0
- package/dist/util/logger.d.ts +14 -0
- package/dist/util/logger.js +119 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/random.d.ts +10 -0
- package/dist/util/random.js +16 -0
- package/dist/util/random.js.map +1 -0
- package/dist/util/redact.d.ts +20 -0
- package/dist/util/redact.js +54 -0
- package/dist/util/redact.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Tencent is pleased to support the open source community by making openclaw-weixin available.
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2026 Tencent. All rights reserved.
|
|
4
|
+
|
|
5
|
+
openclaw-weixin is licensed under the MIT.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Terms of the MIT:
|
|
9
|
+
--------------------------------------------------------------------
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
11
|
+
a copy of this software and associated documentation files (the
|
|
12
|
+
"Software"), to deal in the Software without restriction, including
|
|
13
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
14
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
15
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
16
|
+
the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be
|
|
19
|
+
included in all copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
22
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
23
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
24
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
25
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
26
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
27
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# weixin-bot-cli
|
|
2
|
+
|
|
3
|
+
这是一个轻量级、独立的微信命令行工具(CLI)。它支持通过终端二维码登录微信、实时接收微信消息,并通过命令行直接发送基础的文本消息。
|
|
4
|
+
|
|
5
|
+
该项目基于官方 iLink 微信协议,直接获取配置并将收到的消息稳定地输出到标准终端。它完全独立运行,**不依赖**庞大复杂的插件系统或任何 AI 框架代理层,非常适合用于编写自定义自动化脚本或进行终端调试。
|
|
6
|
+
|
|
7
|
+
## 功能特性
|
|
8
|
+
|
|
9
|
+
- **二维码认证登录**:安全地在终端获取和扫描二维码来完成微信会话登录。
|
|
10
|
+
- **消息监听**:内置长轮询(Long-polling)机制实时监听收件箱,并打印结构化的消息日志。
|
|
11
|
+
- **发送消息**:直接通过命令行 `send` 指令向指定微信联系人发送纯文本消息。
|
|
12
|
+
- **原生媒体支持**:代码库底层保留了媒体处理的支持(详见 `src/media`,`src/cdn`),基于 TypeScript 的内部 API 设计允许开发者后续轻松扩展发送图片、视频和文件等功能。
|
|
13
|
+
- **独立免数据库存储**:告别重量级数据库依赖,所有账号凭证和会话状态均默认以纯文本和 JSON 的形式保存在物理文件系统 `~/.weixin-bot-cli` 下。
|
|
14
|
+
- **TypeScript 构建**:现代化的强类型代码库,编译为开箱即用的 ES Modules。
|
|
15
|
+
|
|
16
|
+
## 环境要求
|
|
17
|
+
|
|
18
|
+
- Node.js >= 22
|
|
19
|
+
- npm / pnpm
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## 使用方法
|
|
24
|
+
|
|
25
|
+
账号配置文件和状态数据的默认存放路径为用户的 `~/.weixin-bot-cli`。如果需要修改数据目录,你可以通过 `-h` 或 `--home` 参数进行覆盖,同样也支持设置 `WEIXIN_BOT_HOME` 环境变量。
|
|
26
|
+
|
|
27
|
+
### 1. 登录
|
|
28
|
+
|
|
29
|
+
初始化二维码扫码登录流程。
|
|
30
|
+
它会在终端生成登录二维码。请使用手机微信扫描屏幕上的二维码完成授权。
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @ecat/weixin-bot-cli login
|
|
34
|
+
|
|
35
|
+
# 可选拓展:将账号数据存放在自定义路径下
|
|
36
|
+
npx @ecat/weixin-bot-cli --home D:/my-data login
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. 启动监听
|
|
40
|
+
|
|
41
|
+
登录成功后,即可启动长轮询监听模式。该命令将挂起并持续运行,只要你的微信账号收到新消息,就会实时解析并将发件人和正文打印到控制台中。
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @ecat/weixin-bot-cli start
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. 发送消息
|
|
48
|
+
|
|
49
|
+
你可以在不中断 CLI 运行的情况下(或开个新终端),通过命令行的形式给特定的用户发送纯文本消息。(注:必须携带接收方的 微信 User ID)
|
|
50
|
+
发送命令前请确保已经运行过 login 命令。
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @ecat/weixin-bot-cli send <接收方的_user_id> "Hello,这是来自命令行的测试消息!"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 项目目录架构
|
|
57
|
+
|
|
58
|
+
- `src/cli.ts` - 核心命令入口,定义了 Commander 路由与其绑定的命令事件。
|
|
59
|
+
- `src/auth/` - 认证相关操作包,包括二维码获取、扫描轮询,以及本地持久化凭证文件写入逻辑(`accounts.ts`)。
|
|
60
|
+
- `src/api/` - 底层通信实现,对 iLink API 进行了直接封装(包括 `getupdates`, `sendmessage`, `getconfig` 等)。
|
|
61
|
+
- `src/monitor/` - 核心长轮询工作器 Worker 处理循环逻辑。
|
|
62
|
+
- `src/messaging/` - 消息发送模块,暴露可以独立发文本、处理内部 Markdown 以及组装 WeChat 发送上下文的基础 API。
|
|
63
|
+
|
|
64
|
+
## 开发
|
|
65
|
+
|
|
66
|
+
建议将仓库克隆至本地,编译后运行即可。
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 安装依赖
|
|
70
|
+
pnpm install
|
|
71
|
+
|
|
72
|
+
# 编译项目
|
|
73
|
+
pnpm run build
|
|
74
|
+
|
|
75
|
+
# 运行
|
|
76
|
+
node dist/cli.js
|
|
77
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BaseInfo, GetUploadUrlReq, GetUploadUrlResp, GetUpdatesReq, GetUpdatesResp, SendMessageReq, SendTypingReq, GetConfigResp } from "./types.js";
|
|
2
|
+
export type WeixinApiOptions = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
token?: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Long-poll timeout for getUpdates (server may hold the request up to this). */
|
|
7
|
+
longPollTimeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
/** Build the `base_info` payload included in every API request. */
|
|
10
|
+
export declare function buildBaseInfo(): BaseInfo;
|
|
11
|
+
/**
|
|
12
|
+
* GET fetch wrapper: send a GET request to a Weixin API endpoint with timeout + abort.
|
|
13
|
+
* Query parameters should already be encoded in `endpoint`.
|
|
14
|
+
* Returns the raw response text on success; throws on HTTP error or timeout.
|
|
15
|
+
*/
|
|
16
|
+
export declare function apiGetFetch(params: {
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
endpoint: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
label: string;
|
|
21
|
+
}): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Long-poll getUpdates. Server should hold the request until new messages or timeout.
|
|
24
|
+
*
|
|
25
|
+
* On client-side timeout (no server response within timeoutMs), returns an empty response
|
|
26
|
+
* with ret=0 so the caller can simply retry. This is normal for long-poll.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getUpdates(params: GetUpdatesReq & {
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
token?: string;
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
}): Promise<GetUpdatesResp>;
|
|
33
|
+
/** Get a pre-signed CDN upload URL for a file. */
|
|
34
|
+
export declare function getUploadUrl(params: GetUploadUrlReq & WeixinApiOptions): Promise<GetUploadUrlResp>;
|
|
35
|
+
/** Send a single message downstream. */
|
|
36
|
+
export declare function sendMessage(params: WeixinApiOptions & {
|
|
37
|
+
body: SendMessageReq;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/** Fetch bot config (includes typing_ticket) for a given user. */
|
|
40
|
+
export declare function getConfig(params: WeixinApiOptions & {
|
|
41
|
+
ilinkUserId: string;
|
|
42
|
+
contextToken?: string;
|
|
43
|
+
}): Promise<GetConfigResp>;
|
|
44
|
+
/** Send a typing indicator to a user. */
|
|
45
|
+
export declare function sendTyping(params: WeixinApiOptions & {
|
|
46
|
+
body: SendTypingReq;
|
|
47
|
+
}): Promise<void>;
|
package/dist/api/api.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { logger } from "../util/logger.js";
|
|
6
|
+
import { redactBody, redactUrl } from "../util/redact.js";
|
|
7
|
+
function readPackageJson() {
|
|
8
|
+
try {
|
|
9
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkgPath = path.resolve(dir, "..", "..", "package.json");
|
|
11
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const pkg = readPackageJson();
|
|
18
|
+
const CHANNEL_VERSION = pkg.version ?? "unknown";
|
|
19
|
+
/** iLink-App-Id: 直接读取 package.json 顶层 ilink_appid 字段。 */
|
|
20
|
+
const ILINK_APP_ID = pkg.ilink_appid ?? "";
|
|
21
|
+
/**
|
|
22
|
+
* iLink-App-ClientVersion: uint32 encoded as 0x00MMNNPP
|
|
23
|
+
* High 8 bits fixed to 0; remaining bits: major<<16 | minor<<8 | patch.
|
|
24
|
+
* e.g. "1.0.11" -> 0x0001000B = 65547
|
|
25
|
+
*/
|
|
26
|
+
function buildClientVersion(version) {
|
|
27
|
+
const parts = version.split(".").map((p) => parseInt(p, 10));
|
|
28
|
+
const major = parts[0] ?? 0;
|
|
29
|
+
const minor = parts[1] ?? 0;
|
|
30
|
+
const patch = parts[2] ?? 0;
|
|
31
|
+
return ((major & 0xff) << 16) | ((minor & 0xff) << 8) | (patch & 0xff);
|
|
32
|
+
}
|
|
33
|
+
const ILINK_APP_CLIENT_VERSION = buildClientVersion(pkg.version ?? "0.0.0");
|
|
34
|
+
/** Build the `base_info` payload included in every API request. */
|
|
35
|
+
export function buildBaseInfo() {
|
|
36
|
+
return { channel_version: CHANNEL_VERSION };
|
|
37
|
+
}
|
|
38
|
+
/** Default timeout for long-poll getUpdates requests. */
|
|
39
|
+
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
40
|
+
/** Default timeout for regular API requests (sendMessage, getUploadUrl). */
|
|
41
|
+
const DEFAULT_API_TIMEOUT_MS = 15_000;
|
|
42
|
+
/** Default timeout for lightweight API requests (getConfig, sendTyping). */
|
|
43
|
+
const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
|
|
44
|
+
function ensureTrailingSlash(url) {
|
|
45
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
46
|
+
}
|
|
47
|
+
/** X-WECHAT-UIN header: random uint32 -> decimal string -> base64. */
|
|
48
|
+
function randomWechatUin() {
|
|
49
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
50
|
+
return Buffer.from(String(uint32), "utf-8").toString("base64");
|
|
51
|
+
}
|
|
52
|
+
/** Build headers shared by both GET and POST requests. */
|
|
53
|
+
function buildCommonHeaders() {
|
|
54
|
+
const headers = {
|
|
55
|
+
"iLink-App-Id": ILINK_APP_ID,
|
|
56
|
+
"iLink-App-ClientVersion": String(ILINK_APP_CLIENT_VERSION),
|
|
57
|
+
};
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
60
|
+
function buildHeaders(opts) {
|
|
61
|
+
const headers = {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
AuthorizationType: "ilink_bot_token",
|
|
64
|
+
"Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
|
|
65
|
+
"X-WECHAT-UIN": randomWechatUin(),
|
|
66
|
+
...buildCommonHeaders(),
|
|
67
|
+
};
|
|
68
|
+
if (opts.token?.trim()) {
|
|
69
|
+
headers.Authorization = `Bearer ${opts.token.trim()}`;
|
|
70
|
+
}
|
|
71
|
+
logger.debug(`requestHeaders: ${JSON.stringify({ ...headers, Authorization: headers.Authorization ? "Bearer ***" : undefined })}`);
|
|
72
|
+
return headers;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* GET fetch wrapper: send a GET request to a Weixin API endpoint with timeout + abort.
|
|
76
|
+
* Query parameters should already be encoded in `endpoint`.
|
|
77
|
+
* Returns the raw response text on success; throws on HTTP error or timeout.
|
|
78
|
+
*/
|
|
79
|
+
export async function apiGetFetch(params) {
|
|
80
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
81
|
+
const url = new URL(params.endpoint, base);
|
|
82
|
+
const hdrs = buildCommonHeaders();
|
|
83
|
+
logger.debug(`GET ${redactUrl(url.toString())}`);
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const t = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(url.toString(), {
|
|
88
|
+
method: "GET",
|
|
89
|
+
headers: hdrs,
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
clearTimeout(t);
|
|
93
|
+
const rawText = await res.text();
|
|
94
|
+
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
97
|
+
}
|
|
98
|
+
return rawText;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
clearTimeout(t);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Common fetch wrapper: POST JSON to a Weixin API endpoint with timeout + abort.
|
|
107
|
+
* Returns the raw response text on success; throws on HTTP error or timeout.
|
|
108
|
+
*/
|
|
109
|
+
async function apiPostFetch(params) {
|
|
110
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
111
|
+
const url = new URL(params.endpoint, base);
|
|
112
|
+
const hdrs = buildHeaders({ token: params.token, body: params.body });
|
|
113
|
+
logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const t = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch(url.toString(), {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: hdrs,
|
|
120
|
+
body: params.body,
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
clearTimeout(t);
|
|
124
|
+
const rawText = await res.text();
|
|
125
|
+
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
128
|
+
}
|
|
129
|
+
return rawText;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
clearTimeout(t);
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Long-poll getUpdates. Server should hold the request until new messages or timeout.
|
|
138
|
+
*
|
|
139
|
+
* On client-side timeout (no server response within timeoutMs), returns an empty response
|
|
140
|
+
* with ret=0 so the caller can simply retry. This is normal for long-poll.
|
|
141
|
+
*/
|
|
142
|
+
export async function getUpdates(params) {
|
|
143
|
+
const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
144
|
+
try {
|
|
145
|
+
const rawText = await apiPostFetch({
|
|
146
|
+
baseUrl: params.baseUrl,
|
|
147
|
+
endpoint: "ilink/bot/getupdates",
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
get_updates_buf: params.get_updates_buf ?? "",
|
|
150
|
+
base_info: buildBaseInfo(),
|
|
151
|
+
}),
|
|
152
|
+
token: params.token,
|
|
153
|
+
timeoutMs: timeout,
|
|
154
|
+
label: "getUpdates",
|
|
155
|
+
});
|
|
156
|
+
const resp = JSON.parse(rawText);
|
|
157
|
+
return resp;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// Long-poll timeout is normal; return empty response so caller can retry
|
|
161
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
162
|
+
logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
|
|
163
|
+
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Get a pre-signed CDN upload URL for a file. */
|
|
169
|
+
export async function getUploadUrl(params) {
|
|
170
|
+
const rawText = await apiPostFetch({
|
|
171
|
+
baseUrl: params.baseUrl,
|
|
172
|
+
endpoint: "ilink/bot/getuploadurl",
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
filekey: params.filekey,
|
|
175
|
+
media_type: params.media_type,
|
|
176
|
+
to_user_id: params.to_user_id,
|
|
177
|
+
rawsize: params.rawsize,
|
|
178
|
+
rawfilemd5: params.rawfilemd5,
|
|
179
|
+
filesize: params.filesize,
|
|
180
|
+
thumb_rawsize: params.thumb_rawsize,
|
|
181
|
+
thumb_rawfilemd5: params.thumb_rawfilemd5,
|
|
182
|
+
thumb_filesize: params.thumb_filesize,
|
|
183
|
+
no_need_thumb: params.no_need_thumb,
|
|
184
|
+
aeskey: params.aeskey,
|
|
185
|
+
base_info: buildBaseInfo(),
|
|
186
|
+
}),
|
|
187
|
+
token: params.token,
|
|
188
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
189
|
+
label: "getUploadUrl",
|
|
190
|
+
});
|
|
191
|
+
const resp = JSON.parse(rawText);
|
|
192
|
+
return resp;
|
|
193
|
+
}
|
|
194
|
+
/** Send a single message downstream. */
|
|
195
|
+
export async function sendMessage(params) {
|
|
196
|
+
await apiPostFetch({
|
|
197
|
+
baseUrl: params.baseUrl,
|
|
198
|
+
endpoint: "ilink/bot/sendmessage",
|
|
199
|
+
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
|
|
200
|
+
token: params.token,
|
|
201
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
202
|
+
label: "sendMessage",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** Fetch bot config (includes typing_ticket) for a given user. */
|
|
206
|
+
export async function getConfig(params) {
|
|
207
|
+
const rawText = await apiPostFetch({
|
|
208
|
+
baseUrl: params.baseUrl,
|
|
209
|
+
endpoint: "ilink/bot/getconfig",
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
ilink_user_id: params.ilinkUserId,
|
|
212
|
+
context_token: params.contextToken,
|
|
213
|
+
base_info: buildBaseInfo(),
|
|
214
|
+
}),
|
|
215
|
+
token: params.token,
|
|
216
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
|
|
217
|
+
label: "getConfig",
|
|
218
|
+
});
|
|
219
|
+
const resp = JSON.parse(rawText);
|
|
220
|
+
return resp;
|
|
221
|
+
}
|
|
222
|
+
/** Send a typing indicator to a user. */
|
|
223
|
+
export async function sendTyping(params) {
|
|
224
|
+
await apiPostFetch({
|
|
225
|
+
baseUrl: params.baseUrl,
|
|
226
|
+
endpoint: "ilink/bot/sendtyping",
|
|
227
|
+
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
|
|
228
|
+
token: params.token,
|
|
229
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
|
|
230
|
+
label: "sendTyping",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/api/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Subset of getConfig fields that we actually need; add new fields here as needed. */
|
|
2
|
+
export interface CachedConfig {
|
|
3
|
+
typingTicket: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Per-user getConfig cache with periodic random refresh (within 24h) and
|
|
7
|
+
* exponential-backoff retry (up to 1h) on failure.
|
|
8
|
+
*/
|
|
9
|
+
export declare class WeixinConfigManager {
|
|
10
|
+
private apiOpts;
|
|
11
|
+
private log;
|
|
12
|
+
private cache;
|
|
13
|
+
constructor(apiOpts: {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
token?: string;
|
|
16
|
+
}, log: (msg: string) => void);
|
|
17
|
+
getForUser(userId: string, contextToken?: string): Promise<CachedConfig>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getConfig } from "./api.js";
|
|
2
|
+
const CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
const CONFIG_CACHE_INITIAL_RETRY_MS = 2_000;
|
|
4
|
+
const CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Per-user getConfig cache with periodic random refresh (within 24h) and
|
|
7
|
+
* exponential-backoff retry (up to 1h) on failure.
|
|
8
|
+
*/
|
|
9
|
+
export class WeixinConfigManager {
|
|
10
|
+
apiOpts;
|
|
11
|
+
log;
|
|
12
|
+
cache = new Map();
|
|
13
|
+
constructor(apiOpts, log) {
|
|
14
|
+
this.apiOpts = apiOpts;
|
|
15
|
+
this.log = log;
|
|
16
|
+
}
|
|
17
|
+
async getForUser(userId, contextToken) {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const entry = this.cache.get(userId);
|
|
20
|
+
const shouldFetch = !entry || now >= entry.nextFetchAt;
|
|
21
|
+
if (shouldFetch) {
|
|
22
|
+
let fetchOk = false;
|
|
23
|
+
try {
|
|
24
|
+
const resp = await getConfig({
|
|
25
|
+
baseUrl: this.apiOpts.baseUrl,
|
|
26
|
+
token: this.apiOpts.token,
|
|
27
|
+
ilinkUserId: userId,
|
|
28
|
+
contextToken,
|
|
29
|
+
});
|
|
30
|
+
if (resp.ret === 0) {
|
|
31
|
+
this.cache.set(userId, {
|
|
32
|
+
config: { typingTicket: resp.typing_ticket ?? "" },
|
|
33
|
+
everSucceeded: true,
|
|
34
|
+
nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
|
|
35
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
36
|
+
});
|
|
37
|
+
this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
|
|
38
|
+
fetchOk = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
|
|
43
|
+
}
|
|
44
|
+
if (!fetchOk) {
|
|
45
|
+
const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
|
|
46
|
+
const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
|
|
47
|
+
if (entry) {
|
|
48
|
+
entry.nextFetchAt = now + nextDelay;
|
|
49
|
+
entry.retryDelayMs = nextDelay;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.cache.set(userId, {
|
|
53
|
+
config: { typingTicket: "" },
|
|
54
|
+
everSucceeded: false,
|
|
55
|
+
nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
56
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return this.cache.get(userId)?.config ?? { typingTicket: "" };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=config-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-cache.js","sourceRoot":"","sources":["../../src/api/config-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Error code returned by the server when the bot session has expired. */
|
|
2
|
+
export declare const SESSION_EXPIRED_ERRCODE = -14;
|
|
3
|
+
/** Pause all inbound/outbound API calls for `accountId` for one hour. */
|
|
4
|
+
export declare function pauseSession(accountId: string): void;
|
|
5
|
+
/** Returns `true` when the bot is still within its one-hour cooldown window. */
|
|
6
|
+
export declare function isSessionPaused(accountId: string): boolean;
|
|
7
|
+
/** Milliseconds remaining until the pause expires (0 when not paused). */
|
|
8
|
+
export declare function getRemainingPauseMs(accountId: string): number;
|
|
9
|
+
/** Throw if the session is currently paused. Call before any API request. */
|
|
10
|
+
export declare function assertSessionActive(accountId: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Reset internal state — only for tests.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare function _resetForTest(): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { logger } from "../util/logger.js";
|
|
2
|
+
const SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
|
|
3
|
+
/** Error code returned by the server when the bot session has expired. */
|
|
4
|
+
export const SESSION_EXPIRED_ERRCODE = -14;
|
|
5
|
+
const pauseUntilMap = new Map();
|
|
6
|
+
/** Pause all inbound/outbound API calls for `accountId` for one hour. */
|
|
7
|
+
export function pauseSession(accountId) {
|
|
8
|
+
const until = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
9
|
+
pauseUntilMap.set(accountId, until);
|
|
10
|
+
logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
|
|
11
|
+
}
|
|
12
|
+
/** Returns `true` when the bot is still within its one-hour cooldown window. */
|
|
13
|
+
export function isSessionPaused(accountId) {
|
|
14
|
+
const until = pauseUntilMap.get(accountId);
|
|
15
|
+
if (until === undefined)
|
|
16
|
+
return false;
|
|
17
|
+
if (Date.now() >= until) {
|
|
18
|
+
pauseUntilMap.delete(accountId);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
/** Milliseconds remaining until the pause expires (0 when not paused). */
|
|
24
|
+
export function getRemainingPauseMs(accountId) {
|
|
25
|
+
const until = pauseUntilMap.get(accountId);
|
|
26
|
+
if (until === undefined)
|
|
27
|
+
return 0;
|
|
28
|
+
const remaining = until - Date.now();
|
|
29
|
+
if (remaining <= 0) {
|
|
30
|
+
pauseUntilMap.delete(accountId);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
return remaining;
|
|
34
|
+
}
|
|
35
|
+
/** Throw if the session is currently paused. Call before any API request. */
|
|
36
|
+
export function assertSessionActive(accountId) {
|
|
37
|
+
if (isSessionPaused(accountId)) {
|
|
38
|
+
const remainingMin = Math.ceil(getRemainingPauseMs(accountId) / 60_000);
|
|
39
|
+
throw new Error(`session paused for accountId=${accountId}, ${remainingMin} min remaining (errcode ${SESSION_EXPIRED_ERRCODE})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reset internal state — only for tests.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export function _resetForTest() {
|
|
47
|
+
pauseUntilMap.clear();
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=session-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-guard.js","sourceRoot":"","sources":["../../src/api/session-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,yBAAyB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEjD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,EAAE,CAAC;AAE3C,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEhD,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAC,SAAiB;IAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,yBAAyB,CAAC;IACrD,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACpC,MAAM,CAAC,IAAI,CACT,mCAAmC,SAAS,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,KAAK,yBAAyB,GAAG,IAAI,IAAI,CAC7H,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,KAAK,EAAE,CAAC;QACxB,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACnD,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACrC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QACnB,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAChC,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACnD,IAAI,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/B,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC;QACxE,MAAM,IAAI,KAAK,CACb,gCAAgC,SAAS,KAAK,YAAY,2BAA2B,uBAAuB,GAAG,CAChH,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,aAAa,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC"}
|