wx-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/README.en.md ADDED
@@ -0,0 +1,175 @@
1
+ # wxbot — WeChat AI Bot CLI
2
+
3
+ A command-line bot tool for the WeChat iLink API. Features QR-code login, message sending and receiving, SQLite-backed message history, and an Ink terminal dashboard (TUI).
4
+
5
+ The background daemon is managed by **launchd** (macOS) or **systemd** (Linux) — auto-start on boot, auto-restart on crash.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **QR-code login** — renders a QR code in the terminal; scan with WeChat to authorize
12
+ - **System service** — auto-registers as a launchd / systemd user service, no manual process management needed
13
+ - **Long-polling** — daemon continuously fetches new messages and writes them to SQLite (WAL mode)
14
+ - **Session quota** — up to 10 replies per context token; an automatic notice is sent on the 9th reply
15
+ - **Unix Socket IPC** — CLI and daemon communicate over a local socket with minimal latency
16
+ - **Ink TUI dashboard** — live message feed and service status, refreshed every 2 seconds
17
+
18
+ ---
19
+
20
+ ## Requirements
21
+
22
+ - Node.js >= 22
23
+ - macOS (launchd) or Linux (systemd)
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install -g wx-bot-cli
31
+ ```
32
+
33
+ Or build from source:
34
+
35
+ ```bash
36
+ git clone https://github.com/yourname/wx-bot-cli.git
37
+ cd wx-bot-cli
38
+ npm install
39
+ npm run build
40
+ npm link
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Quick Start
46
+
47
+ **1. Log in**
48
+
49
+ ```bash
50
+ wxbot login
51
+ ```
52
+
53
+ A QR code is rendered in the terminal. Scan it with WeChat and confirm. Once authorized, the background service is installed and started automatically.
54
+
55
+ **2. Open the dashboard**
56
+
57
+ ```bash
58
+ wxbot
59
+ ```
60
+
61
+ Shows the live message feed, active user, and service status.
62
+
63
+ **3. Send a message**
64
+
65
+ ```bash
66
+ wxbot send "Hello! How can I help you?"
67
+ ```
68
+
69
+ Sends to the currently active user (the most recent person who messaged the bot).
70
+
71
+ ---
72
+
73
+ ## Command Reference
74
+
75
+ | Command | Description |
76
+ |---|---|
77
+ | `wxbot` | Open the TUI dashboard (default) |
78
+ | `wxbot login` | QR-code login; install and start system service |
79
+ | `wxbot logout` | Stop service and clear session (message history preserved) |
80
+ | `wxbot send <text>` | Send a message to the active user |
81
+ | `wxbot list [-n <count>]` | Show recent messages (default: 20) |
82
+ | `wxbot status` | Show service running status |
83
+
84
+ ### wxbot list
85
+
86
+ ```bash
87
+ wxbot list # last 20 messages
88
+ wxbot list -n 50 # last 50 messages
89
+ ```
90
+
91
+ ### wxbot status — example output
92
+
93
+ ```
94
+ ● Service running
95
+ PID: 12345
96
+ Account: bot_abc123
97
+ Last poll: 2026-03-22T10:00:00.000Z
98
+ Active user: user_xyz
99
+ Total msgs: 128
100
+ Uptime: 15m30s
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Session Quota
106
+
107
+ Each user message carries a `context_token` representing an independent conversation. Each conversation allows up to **10 bot replies**:
108
+
109
+ - After the 9th reply, the bot automatically sends a notice asking the user to reply to start a new conversation
110
+ - Once the quota is exhausted, the bot waits for the user's next message to reset
111
+ - `wxbot send` shows a warning when 3 or fewer replies remain
112
+
113
+ ---
114
+
115
+ ## Data Files
116
+
117
+ All runtime files are stored under `~/.wxbot/`:
118
+
119
+ | Path | Purpose |
120
+ |---|---|
121
+ | `~/.wxbot/session.json` | Login session (chmod 600) |
122
+ | `~/.wxbot/messages.db` | Message database (SQLite WAL) |
123
+ | `~/.wxbot/wxbot.sock` | IPC Unix socket |
124
+ | `~/.wxbot/service.pid` | Daemon PID |
125
+ | `~/.wxbot/service-YYYY-MM-DD.log` | Daily service log |
126
+
127
+ ---
128
+
129
+ ## Architecture
130
+
131
+ ```
132
+ wxbot (CLI / TUI)
133
+
134
+ │ Unix Socket IPC (newline-delimited JSON)
135
+
136
+ wxbot _daemon (Daemon)
137
+ ├── Long-polls ilink/bot/getupdates
138
+ ├── Writes messages to SQLite (~/.wxbot/messages.db)
139
+ └── Tracks session state ServiceState (in-memory)
140
+ ```
141
+
142
+ ### Key Modules
143
+
144
+ | File | Responsibility |
145
+ |---|---|
146
+ | `bin.ts` | CLI entry point, Commander routing |
147
+ | `tui.tsx` | Ink TUI dashboard (React) |
148
+ | `service.ts` | Daemon main loop + IPC handler |
149
+ | `auth.ts` | QR-code login flow |
150
+ | `daemon.ts` | launchd plist / systemd unit generation |
151
+ | `ipc.ts` | Unix socket server + client |
152
+ | `api.ts` | iLink API HTTP wrappers |
153
+ | `db.ts` | SQLite operations (better-sqlite3) |
154
+
155
+ ---
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ npm run build # compile TypeScript to dist/
161
+ npm run typecheck # type-check without emitting files
162
+ npm test # run Vitest tests with coverage
163
+ ```
164
+
165
+ Run a single test file:
166
+
167
+ ```bash
168
+ npx vitest run src/auth.test.ts
169
+ ```
170
+
171
+ ---
172
+
173
+ ## License
174
+
175
+ [MIT](LICENSE)
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # wxbot — 微信 AI Bot CLI
2
+
3
+ [English](README.en.md)
4
+
5
+ 基于微信 iLink API 的命令行 Bot 工具,提供扫码登录、消息收发、SQLite 持久化存储,以及 Ink 终端看板(TUI)。
6
+
7
+ 后台守护进程由 **launchd**(macOS)或 **systemd**(Linux)托管,开机自启、崩溃自恢复。
8
+
9
+ ---
10
+
11
+ ## 功能特性
12
+
13
+ - **扫码登录** — 终端内渲染二维码,用微信扫描即可完成授权
14
+ - **系统服务** — 自动注册为 launchd / systemd 用户服务,无需手动守护
15
+ - **长轮询收消息** — 后台持续拉取新消息,写入 SQLite(WAL 模式)
16
+ - **会话额度管理** — 每个 context token 最多发送 10 条消息,第 9 条时自动发送提醒
17
+ - **Unix Socket IPC** — CLI 与 Daemon 通过本地 socket 通信,延迟极低
18
+ - **Ink TUI 看板** — 实时展示消息流与服务状态,每 2 秒刷新
19
+
20
+ ---
21
+
22
+ ## 环境要求
23
+
24
+ - Node.js >= 22
25
+ - macOS(launchd)或 Linux(systemd)
26
+
27
+ ---
28
+
29
+ ## 安装
30
+
31
+ ```bash
32
+ npm install -g wx-bot-cli
33
+ ```
34
+
35
+ 或从源码构建:
36
+
37
+ ```bash
38
+ git clone https://github.com/yourname/wx-bot-cli.git
39
+ cd wx-bot-cli
40
+ npm install
41
+ npm run build
42
+ npm link
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 快速开始
48
+
49
+ **1. 登录**
50
+
51
+ ```bash
52
+ wxbot login
53
+ ```
54
+
55
+ 终端会显示二维码,用微信扫码并确认授权。登录成功后,后台服务自动安装并启动。
56
+
57
+ **2. 打开看板**
58
+
59
+ ```bash
60
+ wxbot
61
+ ```
62
+
63
+ 实时展示收到的消息、当前活跃用户及服务状态。
64
+
65
+ **3. 发送消息**
66
+
67
+ ```bash
68
+ wxbot send "你好,有什么可以帮你?"
69
+ ```
70
+
71
+ 消息发送给当前活跃用户(最近一个给 Bot 发过消息的人)。
72
+
73
+ ---
74
+
75
+ ## 命令参考
76
+
77
+ | 命令 | 说明 |
78
+ |---|---|
79
+ | `wxbot` | 打开 TUI 看板(默认行为) |
80
+ | `wxbot login` | 扫码登录,安装并启动系统服务 |
81
+ | `wxbot logout` | 停止服务,清除会话(消息记录保留) |
82
+ | `wxbot send <text>` | 向当前活跃用户发送消息 |
83
+ | `wxbot list [-n <数量>]` | 查看最近消息,默认显示 20 条 |
84
+ | `wxbot status` | 查看服务运行状态 |
85
+
86
+ ### wxbot list
87
+
88
+ ```bash
89
+ wxbot list # 最近 20 条
90
+ wxbot list -n 50 # 最近 50 条
91
+ ```
92
+
93
+ ### wxbot status 输出示例
94
+
95
+ ```
96
+ ● 服务运行中
97
+ PID: 12345
98
+ 账号: bot_abc123
99
+ 上次轮询: 2026-03-22T10:00:00.000Z
100
+ 活跃用户: user_xyz
101
+ 消息总数: 128
102
+ 运行时长: 15m30s
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 会话额度机制
108
+
109
+ 每当用户发送一条消息时,会产生一个新的 `context_token`,对应一个独立会话。每个会话最多可回复 **10 条消息**:
110
+
111
+ - 第 9 条发送完毕后,Bot 自动向用户发送一条提醒("请回复以开启新会话")
112
+ - 第 10 条额度耗尽后,需等待用户回复才能继续
113
+ - `wxbot send` 会在剩余额度 <= 3 时显示警告
114
+
115
+ ---
116
+
117
+ ## 数据文件
118
+
119
+ 所有运行时文件存放在 `~/.wxbot/`:
120
+
121
+ | 路径 | 用途 |
122
+ |---|---|
123
+ | `~/.wxbot/session.json` | 登录会话(chmod 600) |
124
+ | `~/.wxbot/messages.db` | 消息数据库(SQLite WAL) |
125
+ | `~/.wxbot/wxbot.sock` | IPC Unix Socket |
126
+ | `~/.wxbot/service.pid` | 守护进程 PID |
127
+ | `~/.wxbot/service-YYYY-MM-DD.log` | 当日运行日志 |
128
+
129
+ ---
130
+
131
+ ## 架构
132
+
133
+ ```
134
+ wxbot (CLI/TUI)
135
+
136
+ │ Unix Socket IPC (JSON-over-newline)
137
+
138
+ wxbot _daemon (Daemon)
139
+ ├── 长轮询 ilink/bot/getupdates
140
+ ├── 消息写入 SQLite (~/.wxbot/messages.db)
141
+ └── 会话状态 ServiceState (内存)
142
+ ```
143
+
144
+ ### 核心模块
145
+
146
+ | 文件 | 职责 |
147
+ |---|---|
148
+ | `bin.ts` | CLI 入口,Commander 路由 |
149
+ | `tui.tsx` | Ink TUI 看板,React 组件 |
150
+ | `service.ts` | Daemon 主循环 + IPC 处理 |
151
+ | `auth.ts` | 扫码登录流程 |
152
+ | `daemon.ts` | launchd plist / systemd unit 生成与管理 |
153
+ | `ipc.ts` | Unix Socket 服务端 + 客户端 |
154
+ | `api.ts` | iLink API HTTP 封装 |
155
+ | `db.ts` | SQLite 操作(better-sqlite3) |
156
+
157
+ ---
158
+
159
+ ## 开发
160
+
161
+ ```bash
162
+ npm run build # TypeScript 编译到 dist/
163
+ npm run typecheck # 仅类型检查,不输出文件
164
+ npm test # Vitest 测试 + 覆盖率报告
165
+ ```
166
+
167
+ 单独运行某个测试文件:
168
+
169
+ ```bash
170
+ npx vitest run src/auth.test.ts
171
+ ```
172
+
173
+ ---
174
+
175
+ ## 许可证
176
+
177
+ [MIT](LICENSE)
package/dist/api.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ export declare const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
2
+ export declare const BOT_TYPE = "3";
3
+ export declare const LONG_POLL_TIMEOUT_MS = 35000;
4
+ export declare const API_TIMEOUT_MS = 15000;
5
+ export declare function buildHeaders(token?: string): Record<string, string>;
6
+ export declare function apiPost(params: {
7
+ baseUrl: string;
8
+ endpoint: string;
9
+ body: unknown;
10
+ token?: string;
11
+ timeoutMs: number;
12
+ }): Promise<unknown>;
13
+ export declare function apiGet(params: {
14
+ baseUrl: string;
15
+ endpoint: string;
16
+ token?: string;
17
+ extraHeaders?: Record<string, string>;
18
+ timeoutMs: number;
19
+ }): Promise<unknown>;
20
+ export declare function sendTextMessage(params: {
21
+ baseUrl: string;
22
+ token: string;
23
+ toUserId: string;
24
+ text: string;
25
+ contextToken: string;
26
+ }): Promise<void>;
27
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB,kCAAkC,CAAC;AAChE,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,cAAc,QAAS,CAAC;AAOrC,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQnE;AAaD,wBAAsB,OAAO,CAAC,MAAM,EAAE;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBnB;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBnB;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBhB"}
package/dist/api.js ADDED
@@ -0,0 +1,100 @@
1
+ import crypto from 'node:crypto';
2
+ export const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
3
+ export const BOT_TYPE = '3';
4
+ export const LONG_POLL_TIMEOUT_MS = 35_000;
5
+ export const API_TIMEOUT_MS = 15_000;
6
+ function randomWechatUin() {
7
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
8
+ return Buffer.from(String(uint32), 'utf-8').toString('base64');
9
+ }
10
+ export function buildHeaders(token) {
11
+ const h = {
12
+ 'Content-Type': 'application/json',
13
+ 'AuthorizationType': 'ilink_bot_token',
14
+ 'X-WECHAT-UIN': randomWechatUin(),
15
+ };
16
+ if (token?.trim())
17
+ h['Authorization'] = `Bearer ${token.trim()}`;
18
+ return h;
19
+ }
20
+ function ensureSlash(url) {
21
+ return url.endsWith('/') ? url : `${url}/`;
22
+ }
23
+ function isAbortError(err) {
24
+ if (!(err instanceof Error))
25
+ return false;
26
+ if (err.name === 'AbortError')
27
+ return true;
28
+ const cause = err.cause;
29
+ return cause?.name === 'AbortError' || false;
30
+ }
31
+ export async function apiPost(params) {
32
+ const url = new URL(params.endpoint, ensureSlash(params.baseUrl)).toString();
33
+ const bodyStr = JSON.stringify(params.body);
34
+ const ctrl = new AbortController();
35
+ const t = setTimeout(() => ctrl.abort(), params.timeoutMs);
36
+ try {
37
+ const res = await fetch(url, {
38
+ method: 'POST',
39
+ headers: { ...buildHeaders(params.token), 'Content-Length': String(Buffer.byteLength(bodyStr)) },
40
+ body: bodyStr,
41
+ signal: ctrl.signal,
42
+ });
43
+ clearTimeout(t);
44
+ const text = await res.text();
45
+ if (!res.ok)
46
+ throw new Error(`HTTP ${res.status}: ${text}`);
47
+ return JSON.parse(text);
48
+ }
49
+ catch (err) {
50
+ clearTimeout(t);
51
+ throw err;
52
+ }
53
+ }
54
+ export async function apiGet(params) {
55
+ const url = new URL(params.endpoint, ensureSlash(params.baseUrl)).toString();
56
+ const ctrl = new AbortController();
57
+ const t = setTimeout(() => ctrl.abort(), params.timeoutMs);
58
+ try {
59
+ const res = await fetch(url, {
60
+ method: 'GET',
61
+ headers: { ...buildHeaders(params.token), ...(params.extraHeaders ?? {}) },
62
+ signal: ctrl.signal,
63
+ });
64
+ clearTimeout(t);
65
+ if (res.status === 204 || res.headers.get('content-length') === '0')
66
+ return {};
67
+ const text = await res.text();
68
+ if (!res.ok)
69
+ throw new Error(`HTTP ${res.status}: ${text}`);
70
+ return JSON.parse(text);
71
+ }
72
+ catch (err) {
73
+ clearTimeout(t);
74
+ if (isAbortError(err))
75
+ return { status: 'wait' };
76
+ throw err;
77
+ }
78
+ }
79
+ export async function sendTextMessage(params) {
80
+ const clientId = `wxbot:${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
81
+ await apiPost({
82
+ baseUrl: params.baseUrl,
83
+ endpoint: 'ilink/bot/sendmessage',
84
+ body: {
85
+ msg: {
86
+ from_user_id: '',
87
+ to_user_id: params.toUserId,
88
+ client_id: clientId,
89
+ message_type: 2,
90
+ message_state: 2,
91
+ item_list: [{ type: 1, text_item: { text: params.text } }],
92
+ context_token: params.contextToken,
93
+ },
94
+ base_info: { channel_version: 'standalone' },
95
+ },
96
+ token: params.token,
97
+ timeoutMs: API_TIMEOUT_MS,
98
+ });
99
+ }
100
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { Session } from './types.js';
2
+ export declare function loadSession(sessionPath: string): Session | null;
3
+ export declare function saveSession(sessionPath: string, session: Session): void;
4
+ export declare function clearSession(sessionPath: string): void;
5
+ export declare function loginWithQr(baseUrl: string): Promise<Session>;
6
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAO/D;AAED,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAIvE;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmEnE"}
package/dist/auth.js ADDED
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ // @ts-ignore — no type declarations for qrcode-terminal
4
+ import qrterm from 'qrcode-terminal';
5
+ import { apiGet } from './api.js';
6
+ export function loadSession(sessionPath) {
7
+ try {
8
+ if (!fs.existsSync(sessionPath))
9
+ return null;
10
+ return JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function saveSession(sessionPath, session) {
17
+ fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
18
+ fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2), 'utf-8');
19
+ try {
20
+ fs.chmodSync(sessionPath, 0o600);
21
+ }
22
+ catch { /* best-effort */ }
23
+ }
24
+ export function clearSession(sessionPath) {
25
+ try {
26
+ fs.unlinkSync(sessionPath);
27
+ }
28
+ catch { /* ignore */ }
29
+ }
30
+ export async function loginWithQr(baseUrl) {
31
+ process.stdout.write('正在获取二维码...\n');
32
+ const qrResp = await apiGet({
33
+ baseUrl,
34
+ endpoint: `ilink/bot/get_bot_qrcode?bot_type=3`,
35
+ timeoutMs: 10_000,
36
+ });
37
+ if (!qrResp.qrcode || !qrResp.qrcode_img_content) {
38
+ throw new Error(`获取二维码失败: ${JSON.stringify(qrResp)}`);
39
+ }
40
+ process.stdout.write('\n请用微信扫描以下二维码:\n\n');
41
+ qrterm.generate(qrResp.qrcode_img_content, { small: true });
42
+ process.stdout.write('等待扫码确认...\n\n');
43
+ const deadline = Date.now() + 5 * 60_000;
44
+ let qrcode = qrResp.qrcode;
45
+ let qrcodeImg = qrResp.qrcode_img_content;
46
+ let refreshCount = 0;
47
+ while (Date.now() < deadline) {
48
+ const statusResp = await apiGet({
49
+ baseUrl,
50
+ endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
51
+ extraHeaders: { 'iLink-App-ClientVersion': '1' },
52
+ timeoutMs: 35_000,
53
+ });
54
+ const status = statusResp.status ?? 'wait';
55
+ if (status === 'scaned') {
56
+ process.stdout.write('\r👀 已扫码,请在微信中确认... \n');
57
+ }
58
+ else if (status === 'confirmed') {
59
+ if (!statusResp.ilink_bot_id)
60
+ throw new Error('登录成功但缺少 ilink_bot_id');
61
+ const session = {
62
+ token: statusResp.bot_token ?? '',
63
+ baseUrl: statusResp.baseurl || baseUrl,
64
+ accountId: statusResp.ilink_bot_id,
65
+ userId: statusResp.ilink_user_id,
66
+ savedAt: new Date().toISOString(),
67
+ };
68
+ process.stdout.write(`\n✅ 登录成功!accountId=${session.accountId}\n`);
69
+ return session;
70
+ }
71
+ else if (status === 'expired') {
72
+ refreshCount++;
73
+ if (refreshCount > 3)
74
+ throw new Error('二维码多次过期,请重新运行');
75
+ process.stdout.write(`\n⏳ 二维码已过期,正在刷新... (${refreshCount}/3)\n`);
76
+ const newQr = await apiGet({
77
+ baseUrl,
78
+ endpoint: `ilink/bot/get_bot_qrcode?bot_type=3`,
79
+ timeoutMs: 10_000,
80
+ });
81
+ if (!newQr.qrcode || !newQr.qrcode_img_content)
82
+ throw new Error('刷新二维码失败');
83
+ qrcode = newQr.qrcode;
84
+ qrcodeImg = newQr.qrcode_img_content;
85
+ process.stdout.write('\n新二维码已生成,请重新扫描:\n\n');
86
+ qrterm.generate(qrcodeImg, { small: true });
87
+ }
88
+ else {
89
+ process.stdout.write('.');
90
+ }
91
+ await new Promise((r) => setTimeout(r, 1000));
92
+ }
93
+ throw new Error('登录超时,请重新运行');
94
+ }
95
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}