mytglib 2.0.7 → 2.1.4

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.
Files changed (3) hide show
  1. package/README.md +0 -781
  2. package/dist/index.js +1 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,782 +1 @@
1
1
  # mytglib
2
-
3
- `mytglib` 是对 `@mtcute/node` 登录流程的轻量封装,提供 Android/iOS 客户端配置、手机号与代理检查、验证码登录、结果消息解析和设备模型生成。
4
-
5
- ## 环境
6
-
7
- - Node.js 20、22、23、24、25 或 26(与 `better-sqlite3@12.11.1` 支持范围一致)
8
- - 执行登录或代理检查时,需要一个可连接 Telegram 主 DC 的 HTTP 或 SOCKS5 代理
9
-
10
- 安装当前仓库依赖:
11
-
12
- ```bash
13
- npm install
14
- ```
15
-
16
- ## 构建
17
-
18
- 生成供发布和包根入口使用的 ESM 产物:
19
-
20
- ```bash
21
- npm run build
22
- ```
23
-
24
- 构建结果位于 `dist/index.js`。Webpack 会打包本仓库源码,使用 Terser 压缩,并对标识符和字符串数组进行混淆;`@mtcute/node` 等运行时依赖保持为外部 ESM 依赖,不会重复打入产物。构建不生成 source map,发布包仅包含 `dist`、`README.md` 和 `package.json`。
25
-
26
- ## 离线示例
27
-
28
- 无需网络即可检查手机号解析、随机设备参数、验证码设置和消息映射:
29
-
30
- ```bash
31
- npm run example:inspect
32
- npm run example:inspect -- "+1 (202) 555-0123"
33
- ```
34
-
35
- 实现位于 `examples/inspect-config.js`。
36
-
37
- ## 初始化与登录
38
-
39
- 完整示例位于 `examples/login.js`。先查看所需环境变量:
40
-
41
- ```bash
42
- npm run example:login -- --help
43
- ```
44
-
45
- 以 HTTP 代理运行 Android 登录:
46
-
47
- ```bash
48
- TG_PHONE="+12025550123" \
49
- TG_PROXY_HOST="127.0.0.1" \
50
- TG_PROXY_PORT="8080" \
51
- TG_PROXY_TYPE="http" \
52
- TG_DEVICE_TOKEN="your-device-token" \
53
- npm run example:login
54
- ```
55
-
56
- 示例按服务端响应处理邮箱设置、邮箱验证码、手机验证码和新账号注册。reCAPTCHA 出现时会显示 `action` 与 `siteKey`,并等待输入外部移动端求解器返回的 token。
57
-
58
- ### RegHelp 发码验收
59
-
60
- `examples/reghelp-send-code.js` 默认读取 `examples/.env`,使用 `ios.API_CREDENTIALS`
61
- 第一项(当前 API ID `8`)和手机号 `+19156326494` 发起一次真实的 `auth.sendCode`。
62
- 可通过 `TELEGRAM_PHONE` 覆盖手机号,或同时通过 `TG_API_ID` 与 `TG_API_HASH` 选择数组中的其他凭据:
63
-
64
- ```bash
65
- npm run example:reghelp-send-code
66
- ```
67
-
68
- 该示例会完整输出 device token API、RegHelp 建任务与每次轮询、`invokeWithReCaptcha` 和最终
69
- `auth.sentCode` 响应,并输出中间件从 Telegram challenge 暴露的全部字段。其中包含 device token、
70
- rcmToken 和 `phoneCodeHash`。只应在可信的本地终端运行,且每次执行都会真实请求发送登录验证码。
71
-
72
- ## 基本 API
73
-
74
- 下面是常见的短信验证码登录路径;邮箱设置、新账号注册等完整分支见 `examples/login.js`。
75
-
76
- ```js
77
- import { resolve } from "node:path";
78
- import { CoreClient, getCallMessage } from "mytglib";
79
-
80
- const abortController = new AbortController();
81
- const core = new CoreClient({
82
- clientType: "android",
83
- phone: "+12025550123",
84
- proxy: { type: "http", host: "127.0.0.1", port: 8080 },
85
- sessionDirPath: resolve("telegram-sessions"),
86
- timeout: 30_000,
87
- maxRetryCount: 0,
88
- abortSignal: abortController.signal,
89
- deviceTokenResolver: async () => ({ token: "your-device-token" }),
90
- recaptchaMobileTokenResolver: async () => ({
91
- token: process.env.TG_RECAPTCHA_TOKEN,
92
- }),
93
- });
94
-
95
- try {
96
- await core.init();
97
-
98
- let response = await core.sendCode();
99
- console.log(getCallMessage(response));
100
-
101
- if (response._ === "auth.sentCode") {
102
- response = await core.verifyPhoneCode({ phoneCode: "12345" });
103
- console.log(getCallMessage(response));
104
- } else if (response._ === "auth.sentCodeSuccess") {
105
- response = response.authorization;
106
- }
107
-
108
- if (response._ !== "auth.authorization") {
109
- throw new Error(`Login did not complete: ${response._}`);
110
- }
111
-
112
- const config = await core.client.call({ _: "help.getConfig" });
113
- console.log(config.thisDc);
114
- } finally {
115
- await core.destroy();
116
- }
117
- ```
118
-
119
- Android 和 iOS 分别通过公开且深冻结的 `android.API_CREDENTIALS` 与
120
- `ios.API_CREDENTIALS` 列出可用的官方 API 凭据。Android 支持 ID 4、6,iOS 支持 ID 8、1;
121
- 数组第一项是省略自定义参数时的默认值,仍分别为 Android API ID 4 和 iOS API ID 8。需要使用
122
- 同平台的其他官方凭据时,保持 `clientType` 为 `"android"` 或 `"ios"`,并同时传入数组中同一项的
123
- `apiId` 与 `apiHash`:
124
-
125
- ```js
126
- const iosCore = new CoreClient({
127
- clientType: "ios",
128
- apiId: 1,
129
- apiHash: "b6b154c3707471f5339bd661645ed3d6",
130
- phone: "+12025550123",
131
- proxy: { type: "http", host: "127.0.0.1", port: 8080 },
132
- sessionDirPath: resolve("telegram-sessions"),
133
- deviceTokenResolver: async () => ({ token: "AQIDBA==" }),
134
- });
135
- ```
136
-
137
- `apiId` 与 `apiHash` 必须同时省略或同时提供;显式提供时必须精确匹配所选平台公开数组中的同一对,
138
- 不能交叉组合,也不会新增 `clientType`。选中的凭据会用于连接初始化和登录请求;首次授权新建 session
139
- Metadata 时也会保存该凭据,之后 `syncSessionJson()` 从这份注册快照导出。重新打开已有 session 时,
140
- 按既有契约保留首次写入的 API 凭据及其他初始化配置,不会用本次构造参数改写注册快照。
141
-
142
- iOS 默认使用 `ios.CURRENT_VERSION` 的最后一项。覆盖版本时仍需同时传入 `appVersion` 与
143
- `overrideLayer`,以兼容现有调用方;`appVersion` 会原样用于 Telegram `initConnection.appVersion`,
144
- `overrideLayer` 不会传给底层客户端。Node.js 的协议 layer 始终由 `@mtcute/node` 当前 codec 的
145
- `tl.LAYER` 决定,当前为 layer 229。
146
-
147
- ```js
148
- const iosCore = new CoreClient({
149
- clientType: "ios",
150
- phone: "+12025550123",
151
- proxy: { type: "http", host: "127.0.0.1", port: 8080 },
152
- sessionDirPath: resolve("telegram-sessions"),
153
- langCode: "en",
154
- appVersion: "12.8.1 (33181) ",
155
- overrideLayer: 227,
156
- deviceTokenResolver: async () => ({ token: "AQIDBA==" }),
157
- });
158
- ```
159
-
160
- 覆盖值仅适用于 iOS;`appVersion` 必须是非空字符串,`overrideLayer` 必须是正数 int32。Rust fork
161
- 同样只把 `app_version` 用于 `initConnection`,协议 layer 始终使用编译期 `tl::LAYER`。Node.js 和 Rust
162
- 新写入的 `override_layer` Metadata 均记录当前真实 codec layer(当前为 229),不记录历史选择值。
163
- `langCode` 同样只支持 iOS,并且必须来自公开的 `iosLangPackLanguages`;省略时继续根据
164
- 手机号自动选择。手工覆盖只改变 Telegram `langCode`,`systemLangCode` 仍按手机号地区推断。
165
-
166
- `sendCode()` 可选接收 `codeSettings` 对象,用于覆盖本次 `auth.sendCode` 请求的五个布尔能力字段。Android 保持原有默认值:`allowFirebase`、`allowFlashcall`、`allowMissedCall` 为 `true`,`currentNumber`、`unknownNumber` 为 `false`。
167
-
168
- iOS 默认显式传递 `allowFlashcall: true`、`allowFirebase: true` 和固定的 `logoutTokens: []`。调用方把 `allowFlashcall` 或 `allowFirebase` 设为 `false` 时仍会显式保留该值;`allowMissedCall`、`currentNumber`、`unknownNumber` 仅在设为 `true` 时传递,设为 `false` 或未提供时会从请求对象中省略。`logoutTokens` 不接受调用方覆盖,每次请求都会创建新的空数组。
169
-
170
- ```js
171
- const response = await core.sendCode({
172
- allowFirebase: false,
173
- allowFlashcall: true,
174
- allowMissedCall: false,
175
- currentNumber: false,
176
- unknownNumber: true,
177
- });
178
- ```
179
-
180
- 传入的五个能力字段必须是 boolean;Android 的 `allowAppHash` 以及 iOS 的 APNs `token`、`appSandbox` 仍由平台配置自动生成,不属于可覆盖字段。根据 [mtcute 0.32.1 `sendCode` API](https://ref.mtcute.dev/funcs/_mtcute_core.highlevel_methods.sendCode.html),future auth tokens 的参数类型是 `Uint8Array[]`;本库直接构造 raw `codeSettings`,因此对应的 `logoutTokens` 使用原生 JavaScript 数组,并在请求对象中固定保留空数组 `[]`,而不是 TL JSON 包装对象或字符串数组。mtcute 0.32.1 只会序列化非空的 `logoutTokens`,因此空数组不会设置可选 TL vector flag。
181
-
182
- `deviceTokenResolver` 和 `recaptchaMobileTokenResolver` 必须返回包含非空字符串 `token` 字段的对象,对象可保留求解服务返回的 `errorMessage`、`message` 等附加字段。无有效 token 时会抛出 `TypeError`,其 `cause` 是 resolver 返回的完整结果;reCAPTCHA token 最长为 16,384 个字符。
183
-
184
- reCAPTCHA 由客户端的全局 network middleware 处理,不限于 `auth.sendCode`。任意尚未包装且允许重放的 RPC 返回格式严格匹配 `403 RECAPTCHA_CHECK_<action>__<siteKey>` 时,middleware 会调用 `recaptchaMobileTokenResolver`,使用 `invokeWithReCaptcha` 包装原请求,并通过后续的 `networkMiddlewares.basic()` 链重试一次。`account.updatePasswordSettings` 和 `payments.assignAppStoreTransaction` 不会被该 middleware 自动重放;已有 `invokeWithReCaptcha` 请求或包装请求再次失败时也不会重复求解、再次包装或形成无限重试。middleware 自动生成的 `invokeWithReCaptcha` 日志记录错误文本、完整原请求、真实响应和错误。
185
-
186
- Rust `grammers_core` 对初始 `auth.sendCode` 及其自动生成的 `invokeWithReCaptcha(auth.sendCode)` 额外设置完整 frame 写出后的 20 秒响应期限。连接类失败会先关闭当前 DC sender,再用同一序列化请求和同一 reCAPTCHA token 在新代理隧道上重放一次;明确的 Telegram RPC 错误、取消和第二次连接失败不会重放。该规则不改变 Node.js `@mtcute/node` 实现,也不适用于 `signIn`、`signUp` 或其他 RPC。
187
-
188
- Rust `verify_phone_code()` / `register()`(包括对应 `CoreOperation`)的每个授权步骤共用 60 秒期限,包含开始/结束日志回调、重连、DC 迁移、核验、退避和授权元数据处理;日志回调也响应取消和期限。未派发业务请求的结构化连接初始化超时最多额外重试 3 次;仅 `auth.signUp` 的 `500 REG_ID_GENERATE_FAILED` 最多额外重试 3 次,退避均为 2、5、5 秒。注册重放前直接调用 `users.getUsers(InputUserSelf)`:已授权则保存成功,只有明确 `401 AUTH_KEY_UNREGISTERED` 才允许重放该 500。发送结果未知时只核验、不重放。恢复次数或期限耗尽、核验失败时返回 `CoreError::AuthorizationRecoveryRequired`,保留原始错误和重试记录,超时丢弃在途调用时也不会丢失这些记录;`signUp` 返回非成功授权响应同样归为授权未知;取消保持 `CoreError::Aborted`。恢复复用原 Session 和参数,禁止额外 reCAPTCHA 求解,不重新取号或发码。调用方应将最终未知结果隔离,不在外层另开自动重试循环。Node.js 行为不变。
189
-
190
- `auth.sentCodePaymentRequired` 不是 RPC 错误,而是 `auth.sendCode` 的合法返回。它表示由于所在国家或运营商的短信验证成本较高,官方客户端必须先完成 Telegram Premium 购买流程才能继续登录或注册。原始响应中的 `storeProduct`、`premiumDays`、`currency` 和 `amount` 描述商品与价格,`phoneCodeHash` 保留后续授权上下文,`supportEmailAddress` 和 `supportEmailSubject` 用于联系支持。
191
-
192
- `sendCode()` 或 `verifyEmailCode()` 收到该响应时,会像保存 `phoneCodeHash` 一样把三个必需字段原子地挂载到 `core.inputStore`:`{ premiumDays, currency, amount }`。普通 `auth.sentCode`、`auth.sentCodeSuccess`、授权成功及成功销毁客户端时会清空该状态。
193
-
194
- 使用 iOS 客户端通过 StoreKit 完成购买并取得 App Store receipt 后,使用 `submitCredentials({ receipt })` 调用 `payments.assignAppStoreTransaction`。`receipt` 必须是 base64 字符串,方法会严格解码为 Telegram `bytes`;`inputStore` 自动读取 `core.inputStore`,`restore` 固定为 `false`,方法不会从调用参数读取这些字段。未先收到 `auth.sentCodePaymentRequired`,或缓存字段无效时,方法会在发送 RPC 前抛出 `TypeError`。方法返回原始 Telegram `Updates`,并从 `updateSentPhoneCode.sentCode` 同步 `phoneCodeHash` 和 `inputStore`;若返回 `auth.sentCodeSuccess`,也会沿用现有登录通知逻辑。
195
-
196
- ```js
197
- const updates = await core.submitCredentials({
198
- receipt: appStoreReceiptBase64,
199
- });
200
- ```
201
-
202
- `payments.assignAppStoreTransaction` 及其购买流程仅供 Telegram 官方客户端使用。receipt 必须与 App Store 应用、商品和签名环境匹配;本库只封装 receipt 转换、TL 请求及返回状态处理,不负责发起 StoreKit 购买。
203
-
204
- `auth.sentCodeTypeFirebaseSms` 表示短信尚未发送。调用方必须先完成 Firebase 设备验证,再通过 `core.client.call()` 调用 `auth.requestFirebaseSms` 请求短信验证码;本库当前不封装 SafetyNet、Google Play Integrity 或 iOS push secret 的获取流程。`examples/login.js` 遇到该响应时会明确停止,避免在验证码尚未发送时进入输入流程。
205
-
206
- `timeout`、`maxRetryCount` 和 `abortSignal` 是所有 mytglib 登录 RPC 的统一调用参数。`timeout` 默认为 `30000`,必须是正安全整数毫秒;`maxRetryCount` 默认为 `0`,必须是非负安全整数;`abortSignal` 默认为 `undefined`,传入时必须是 `AbortSignal`。客户端创建后会通过原始 `TelegramClient.withParams()` 生成 `core.client`,登录流程和通过 `core.client.call()` 发出的请求都会使用这些参数。密码更新的最终 `account.updatePasswordSettings` 会把 `maxRetryCount` 强制为 `0`,避免 internal/flood retry middleware 重放;前置的只读 `account.getPassword` 仍使用客户端配置。它们只作用于 RPC,不处理 `connect()` 超时,也不会取消正在进行的 `connect()`。
207
-
208
- `sessionDirPath` 是必传的会话主目录。路径已存在时必须是目录;路径不存在时会在初始化过程中递归创建。`init()` 会按照 `<主目录>/<+手机号>/<+手机号>.session` 创建 SQLite 会话文件,然后依次解析设备 token 并连接客户端。连接成功后可通过 `core.sessionFilePath` 直接取得当前客户端使用的 SQLite 文件路径;连接前该属性为 `null`。登录相关公共异步方法均返回原始 Telegram TL 响应。`core.rawClient` 保存原始 `TelegramClient`,只负责 `connect()`、`notifyLoggedIn()` 和 `destroy()`;`core.client` 保存 `withParams()` 返回的 RPC Proxy。`core.destroy()` 始终销毁 `rawClient`,不会在 `@mtcute/node` 0.32.1 的包装对象上调用 `destroy()`,从而避免私有字段错误。无论成功或失败,都应在 `finally` 中调用 `core.destroy()`,永久关闭连接、定时器和存储资源。销毁成功后两个客户端引用都会恢复为 `null`,SQLite 文件路径仍保留在 `core.sessionFilePath` 上。
209
-
210
- 初始化 `.session` 时,库会在同一个 SQLite 数据库内创建单行 `session_metadata` 表,并在授权成功后写入账号 Metadata;不会自动创建 JSON 文件或第二个数据库连接。每次初始化都会让 `session_file` 与当前会话路径一致;初始化配置与首次授权时间作为注册快照保留,`reg_time` 取最终产生授权的注册或登录方法入口调用时间。重新授权只刷新用户名、姓名、Premium 状态及非空运行态字段。`first_name` 和 `last_name` 在注册或登录授权成功后自动从 Telegram 用户信息同步,缺失时写入 `NULL`。`completion_type` 只有 `sign_up`(实际执行 `auth.signUp` 创建账号)和 `login`(已有账号完成授权)两种;缺失字段写入 `NULL`。最新 `phone_code_hash`、已验证登录邮箱、成功使用的 reCAPTCHA token 和两步验证密码会随运行生命周期同步更新。数据库写入失败时会按 `50ms`、`150ms`、`300ms` 额外重试三次;Telegram RPC 已成功但 Metadata 最终仍失败时,可调用 `await core.retrySaveSessionMetadata()` 重试当前全部待写操作。
211
-
212
- 授权成功后、调用 `destroy()` 之前,可主动将数据库内的 Metadata 同步为与 `.session` 同目录、同名主干的 JSON:
213
-
214
- ```js
215
- const jsonFilePath = await core.syncSessionJson();
216
- ```
217
-
218
- `syncSessionJson()` 无参数并返回 JSON 文件路径。JSON 中的 `session_str` 为可与 Telethon StringSession v1 兼容的字符串,`reg_time` 按中国标准时间转为 `YYYY-MM-DD`;当前转换仅支持项目使用的 IPv4 主 DC。底层 `.session` 仍是 mtcute SQLite,不会被转换成 Telethon SQLite。输出文件包含 auth key 和明文两步验证密码,会通过临时文件原子替换;文件权限由运行环境的默认配置决定。
219
-
220
- ## 手机号状态账号池
221
-
222
- `createPhoneStatusPool()` 用于在常驻 Node.js API 进程内维护一组已经授权的 Telegram
223
- 账号。账号导入后由 Pool 自主预连接、派单、冷却、隔离、补槽和回收;管理员只负责导入、查看、
224
- 测试或删除账号,不需要手工启用、停用或扩缩容。
225
-
226
- Pool 使用消费者模式:外部请求只消费已经连接的 `READY` 槽位,不会在请求路径中临时连接
227
- Telegram。创建 Pool 和后续补槽均为异步操作;检测可以在内部 deadline 内等待正在连接、正在被其他
228
- 请求使用或正在异步补入的槽位,但不会同步建立临时账号连接。生产环境应在接入流量前完成基础暖槽,
229
- 并为重试配置独立的 `RETRY` 暖槽。
230
-
231
- ### 初始化与导入
232
-
233
- ```js
234
- import { resolve } from "node:path";
235
- import { createPhoneStatusPool } from "mytglib";
236
-
237
- const pool = await createPhoneStatusPool({
238
- databasePath: resolve(".data/phone-status-pool/pool.sqlite"),
239
- minWarmSlots: 10,
240
- retryWarmSlots: 200,
241
- maxConcurrentChecks: 256,
242
- });
243
-
244
- const account = await pool.importAccount({
245
- sessionFilePath: "/secure/imports/13099434947.session",
246
- sessionJsonFilePath: "/secure/imports/13099434947.json",
247
- });
248
-
249
- console.log(account.accountId, account.runtimeStatus);
250
- ```
251
-
252
- `databasePath` 必须是绝对文件路径,目录和文件权限由运行环境的默认配置决定。Pool 会把
253
- 账号连接所需的 auth key 和 JSON Metadata 明文持久化到 SQLite。`.session` 可以使用 Telethon
254
- 的 `sessions` schema 或当前 mtcute 的 `key_value`/`auth_keys` schema,并会在只读打开后
255
- 自动识别。两套 schema 同时存在时:两侧都有效且授权状态完全一致才接受;两侧都有效但内容冲突
256
- 则拒绝;只有一侧有效时使用有效侧;两侧都无有效授权时拒绝。会话文件与 JSON 必须属于同一个
257
- 有效账号,导入时会严格核对 auth key、DC、账号身份和手机号。Pool 只接受 Telegram 官方移动端
258
- 凭据:Android 必须精确匹配 `android.API_CREDENTIALS` 中的 ID 4 或 6 凭据对,iOS 必须精确匹配
259
- `ios.API_CREDENTIALS` 中的 ID 8 或 1 凭据对;TDesktop、自定义 API ID、交叉组合或平台与
260
- API 凭据不匹配的 JSON 会在建立 Telegram 连接前直接拒绝。当前不支持代理。
261
-
262
- 为兼容常见会话导出格式,JSON 的 `session_file` 只能写真实文件名
263
- `account.session` 或同名 stem `account`,不得携带目录组件;导入后会统一规整为真实文件名。
264
- StringSession 优先读取 `session_str`,也兼容 `session_string`;两者都缺失时会根据只读 SQLite
265
- 中的主 DC 和 auth key 生成规范 `session_str`。任何显式提供的 StringSession 都必须与 SQLite
266
- 中的主 DC、IPv4、端口和 auth key 完全一致,否则拒绝导入。
267
- SQLite 与 StringSession 中的 endpoint 只用于交叉校验;实际创建 Pool 客户端时会按 JSON 对应的
268
- 官方 iOS 或 Android 平台,使用相同 DC ID 的内置官方 seed,不会连接上传文件指定的地址。
269
- `device_token` 可以省略、设为 `null` 或留空;空值按未提供处理,不会写入连接参数。
270
- 导入会在写入账号池前主动检查账号状态,并且只接受 `UNFROZEN`:`FROZEN` 返回
271
- `ACCOUNT_INVALID`,无法确认的 `UNKNOWN` 返回 `POOL_CONNECTION_FAILED`,两者都不会留下账号记录或
272
- 可调度槽位。只有 Telegram RPC 本身真实返回 `FROZEN_METHOD_INVALID` 或
273
- `FROZEN_PARTICIPANT_MISSING` 时,才会原样保留其 `code = 420` 和错误文本;内部按认证失效一类
274
- 致命账号错误处置,不会把真实错误改写成 401。AppConfig RPC 的普通失败或超时才归为 `UNKNOWN`;
275
- 若该 RPC 真实返回现有致命账号错误或 `FLOOD_WAIT`,导入会保留原错误并拒绝落库,池内自检则分别
276
- 执行终态隔离或服务端时长冷却。
277
-
278
- `minWarmSlots` 可省略,默认值为 `10`,可设为正的 JavaScript 安全整数,不再固定限制为 50;
279
- `minWarmSlots * 5` 也必须是 JavaScript 安全整数。
280
- 它是正常负载下希望维持的最小预连接槽位数,不是导入账号数,也不是无条件建立的连接数。
281
- 例如导入 500 个账号且设置 `minWarmSlots: 100` 时,Pool 只会先预热目标槽位,
282
- 剩余账号作为持久化库存待命,不会把 500 个账号同时上线。
283
-
284
- `retryWarmSlots` 是专门服务账号级失败重试的独立暖槽数,默认值为 `0`,生产环境可按流量单独配置
285
- (首轮建议 `200`,不把比例写死在代码中)。首次检测只使用 `PRIMARY` 槽;账号错误、
286
- `FLOOD_WAIT` 或 RPC 超时等需要换账号时,只使用 `RETRY` 槽,普通 `PRIMARY` 槽不会被重试借用。
287
- `maxAccountsPerPhone` 限制一次手机号检测最多使用的不同账号数,默认值为 `8`,可配置上限为 `16`,
288
- 用于避免单个手机号占满重试区。
289
-
290
- Pool 的 PRIMARY 策略硬上限固定为 `minWarmSlots * 5`,加上 `retryWarmSlots` 后形成总策略上限,实际连接上限还会受进程 CPU、内存、文件描述符和
291
- 事件循环压力评估限制。忙碌槽位超过当前连接数一半时,Pool 会在资源允许的情况下异步提高目标槽位;
292
- 高峰过后,弹性槽位连续空闲 10 分钟才进入回收,并且每 30 秒最多回收一个,避免连接数瞬间震荡。
293
- 进入 `PRESSURED` 时,Pool 会先回收 PRIMARY 弹性槽,并保留当前资源上限能够承载的基础 PRIMARY
294
- 和独立 RETRY 槽;若上限不足,则优先保证 PRIMARY,按剩余容量降低 RETRY 数。
295
- 单次事件循环尖峰只触发 `PRESSURED`,连续 3 次成功采集的严重样本才进入软 `CRITICAL`,连续 2 次健康样本恢复;采样失败会打断连续计数。
296
- `resource.hardCritical` 区分内存/堆硬危险与事件循环软压力:软 `CRITICAL` 暂停建连、保留就绪主备槽并
297
- 以 10% 检测预算继续服务,不因 PRIMARY 未达到配置目标而回收已有 RETRY;暂停预热期间有效目标按实际存量显示。
298
- 硬危险立即拒绝新检测并逐步释放空闲连接,可低于配置的最小暖槽数。
299
- 压力回收每秒最多启动一个,不中断正在使用的槽。`resource.pressureReasons` 报告具体压力原因,
300
- `resource.capacityLimitingFactor` 单独报告连接容量瓶颈,避免把“目标未预热满”当成“资源未承压”。
301
- 监控采样失败时停止新建连接、缩小检测预算,连续 3 次失败后拒绝新检测;运行期单次采样最多等待 3 秒,
302
- 未完成的采样不会重复启动。`lastSuccessfulSampleAt` 用于识别陈旧数据。
303
-
304
- `maxConcurrentChecks` 默认 `256`,限制未完成的不同手机号共享检测任务(包含等待 READY 的任务)。
305
- `PRESSURED` 下预算为 50%,软 `CRITICAL` 为 10%,硬危险为 0;达到上限立即返回 `POOL_QUEUE_FULL`。
306
- 缓存命中和相同手机号的跟随者不新增共享检测名额。HTTP 层仍需独立限制总请求数与每用户并发,
307
- 防止大量跟随者占满 Web 进程。预热只排入最多两倍握手并发的连接任务,分别计算 PRIMARY/RETRY 缺口,主槽富余不会抵消重试槽缺口。
308
- READY 槽按风险/最近使用时间由索引优先队列选择,不再逐请求排序全部连接。
309
- 当总连接上限高于 `minWarmSlots` 时,Pool 会在这个上限内部为导入和待机账号自检保留 1 个临时
310
- 管理连接位;该预留不会突破 PRIMARY 策略上限与 `retryWarmSlots` 之和。若资源上限已经低到不高于
311
- 最小暖槽数,管理建连会返回 `POOL_BUSY`,优先保护正在提供服务的暖槽。
312
-
313
- ### 检测与返回值
314
-
315
- ```js
316
- const response = await pool.checkPhoneStatus({
317
- phone: "+12025550123",
318
- abortSignal: new AbortController().signal,
319
- deadlineAt: Date.now() + 13_000, // 可选:调用方等待的绝对期限
320
- });
321
-
322
- console.log(response.status, response.waitTime, response.error);
323
- ```
324
-
325
- 返回对象固定使用一套扁平结构:
326
-
327
- ```js
328
- {
329
- waitTime: 0,
330
- phone: "+12025550123",
331
- startedAt: "2026-08-13T00:00:00.000Z",
332
- endedAt: "2026-08-13T00:00:00.120Z",
333
- durationMs: 120,
334
- status: "PHONE_NUMBER_OCCUPIED",
335
- error: null,
336
- }
337
- ```
338
-
339
- `status` 只会是 `PHONE_NUMBER_OCCUPIED`、`PHONE_NUMBER_NO_OCCUPIED`、
340
- `PHONE_NUMBER_INVALID`、`PHONE_NUMBER_BANNED`、`FLOOD_WAIT` 或 `OTHER_ERROR`。
341
- `FLOOD_WAIT` 的等待秒数写入 `waitTime`;其他状态的 `waitTime` 为 `0`。
342
- 未映射到上述业务状态的检测异常会被捕获并以 `OTHER_ERROR` 正常返回,`error` 仅保留可用的 `message`、`name`、
343
- `code`、`text` 和 `seconds`。
344
-
345
- 外部 HTTP 接口的端到端目标是 15 秒;Pool 内部为输入、排队、重试和清理预留固定的 14 秒共享
346
- 检测 deadline。每次 Telegram 检测 RPC 单独使用最多约 3 秒的局部 timeout,且关闭 mtcute 自带的该请求
347
- 重试。第一次使用 `PRIMARY` 槽;账号级错误、`FLOOD_WAIT` 或 RPC 超时后,后续尝试只从已经
348
- `READY` 或正在异步补入的 `RETRY` 槽获取。后续尝试次数由剩余 `RETRY` 资源、单手机号账号上限和
349
- 14 秒 deadline 共同限制;资源允许时,仍可继续使用剩余重试暖槽兜底。
350
-
351
- 请求会在 deadline 内等待正在使用、正在连接或正在补槽的对应角色槽位,但不会在请求路径同步重启账号。
352
- 没有任何未使用的合格账号、重试暖槽耗尽或 deadline 到期时,返回最后一次已经实际发生的暖槽错误;
353
- 如果还没有发生暖槽错误,则返回 `OTHER_ERROR`,并通过 `error.code` 区分 `POOL_RETRY_TIMEOUT`、
354
- `POOL_QUEUE_FULL` 或 `POOL_RESOURCE_CRITICAL` 等 Pool 原因。触发 `FLOOD_WAIT` 的账号会按 Telegram
355
- 返回的等待秒数退出暖槽并进入冷却,Pool 随即从库存异步补入其他账号。
356
- 若暖槽返回冻结账号错误,Pool 会将该账号永久隔离并换用其他 `READY` 槽位;当前检测返回的错误仍保留
357
- Telegram 原始 `code = 420` 和错误文本。
358
- `PHONE_NUMBER_FLOOD` 是目标手机号级错误,当前请求不换槽重试、不处罚账号,也不缓存错误;后续同号请求可重新检测。
359
- 仅 `PHONE_NUMBER_OCCUPIED`、`PHONE_NUMBER_NO_OCCUPIED`、`PHONE_NUMBER_INVALID` 和
360
- `PHONE_NUMBER_BANNED` 的确定结果缓存 60 秒,最多保存 10,000 条;有效期从写入时计算,命中不续期。
361
- 本地格式校验失败仍即时返回 `PHONE_NUMBER_INVALID`,不发起 RPC,也不占用结果缓存。
362
- 同一手机号的并发请求会合并为一次检测;`FLOOD_WAIT`、`OTHER_ERROR` 等其他结果不缓存,
363
- 共享检测结束后后续请求可重新检测,也不提供强制刷新。
364
-
365
- `abortSignal` 只取消当前调用方等待,并以 `OTHER_ERROR` 返回取消原因;它不会中断已经启动的
366
- single-flight RPC。即使当前只有一个等待者,后台检测仍可完成并写入明确结果缓存。调用开始前已经
367
- 取消的 signal 会在读取缓存前返回,不会命中旧结果或启动新 RPC。
368
- `deadlineAt` 同样只限制当前调用方等待,不会缩短同号码其他调用者共享任务的 14 秒内部预算。
369
- 调用方离开后,内部任务仍占用 `maxConcurrentChecks` 名额直至完成。
370
- 普通检测的统计增量每秒合并写入 SQLite,并在候选选择和关闭时刷新;异常退出可能丢失最后
371
- 约一秒统计,账号冷却/隔离等调度状态仍立即持久化。统计写失败保留待写增量并报告后台错误。
372
- 管理查询直接叠加待写统计、不强制刷新;仓储新增不会因无关统计刷新失败出现“已落库却抛错”,删除和冷却/隔离状态落盘也不受统计刷新阻断。候选选择仍要求统计刷新成功以保持排序;完整导入流程中的必要写入失败时仍会回滚导入。
373
-
374
- 按照当前业务规则,`403 RECAPTCHA_CHECK_signup`(含 site key 后缀)直接判定为
375
- `PHONE_NUMBER_NO_OCCUPIED`。Pool 不求解、不重放 challenge;它会回收触发 challenge 的槽位并从库存
376
- 异步补槽。其他 reCAPTCHA action 仍按 `OTHER_ERROR` 返回。
377
-
378
- ### 管理与运行状态
379
-
380
- 大库存管理列表可调用 `await pool.listAccountsPage({ page: 1, pageSize: 20, phone: "", runtimeStatus: "READY" })`,
381
- 返回 `{ accounts, total, page, pageSize }`。`pageSize` 为 1–100,页码越界时回到最后一页,筛选后的账号投影不读取
382
- auth key 或 session metadata。原 `listAccounts()` 继续兼容。`getStatus()` 从轻量状态索引汇总,包含
383
- `resource`、`roles`、`queues` 和 `admission`,不读取全量会话。
384
-
385
- 本机 mock Telegram + 真实 SQLite 的容量测量见 [基准说明](docs/phone-status-pool-benchmark.md)。
386
- 该基准只能验证调度开销与预算上限,真实吞吐还需在目标硬件、真实网络延迟和账号限流条件下测量。
387
-
388
-
389
- ```js
390
- const accounts = await pool.listAccounts();
391
- const detail = await pool.getAccount(account.accountId);
392
- const testResult = await pool.testAccount(account.accountId);
393
- const testReport = await pool.testAccounts({
394
- abortSignal: new AbortController().signal,
395
- });
396
- const status = await pool.getStatus();
397
-
398
- console.log(accounts, detail, testResult, testReport, status.runtime);
399
- console.log(accounts.map(({ phone, cooldownUntil }) => ({ phone, cooldownUntil })));
400
-
401
- // 删除会先停止派单,等待该账号当前任务结束,再断开连接并删除持久化记录。
402
- const removed = await pool.removeAccount(account.accountId);
403
- ```
404
-
405
- 账号运行态由 Pool 管理:可用账号处于 `STANDBY`、`RESERVED`、`CONNECTING`、`READY` 或
406
- `BUSY`;触发 flood 的账号进入 `COOLDOWN`,认证失效等致命错误会进入 `QUARANTINED`。
407
- `testAccount()` 是由管理员主动触发的账号健康自检,不会拿这个账号去检测某个外部手机号,也不会由
408
- 协调器定时自动执行。`QUARANTINED` 是终态;自检不会把已隔离账号恢复为 `ACTIVE` 或重新加入调度。
409
- 单账号自检失败时会直接拒绝 Promise:Telegram RPC 错误保留具体的 `message`、数值 `code`、`text` 和
410
- `seconds` 字段;Pool 自身错误仍使用 `PhoneStatusPoolError` 的字符串 `code`。`testAccounts()` 继续将
411
- 单账号错误收敛到批量报告的 `{ code, message }`,不改变批量接口契约。
412
- 管理接口返回完整 `phone` 和 ISO 格式或 `null` 的 `cooldownUntil`,但不会包含 auth key、原始
413
- Metadata、session、Telegram 冻结时间或申诉地址等内部探测字段。
414
-
415
- `testAccounts()` 由使用者主动触发,会固定批次开始时的账号快照并严格串行调用单账号自检。同一个 Pool
416
- 同时只运行一个批次;已经 `BUSY` 的账号不会被等待或抢断,而是记为 `FAILED` 后继续。已隔离账号不联网,
417
- 直接记为 `QUARANTINED`;批次中的账号被删除或进入 `REMOVING` 时记为 `SKIPPED`。返回结构固定为:
418
-
419
- ```js
420
- {
421
- total: 1,
422
- healthy: 1,
423
- quarantined: 0,
424
- failed: 0,
425
- skipped: 0,
426
- results: [{
427
- accountId: "account-id",
428
- outcome: "HEALTHY",
429
- account: { /* PoolAccount */ },
430
- error: null,
431
- }],
432
- }
433
- ```
434
-
435
- `outcome` 只会是 `HEALTHY`、`QUARANTINED`、`FAILED` 或 `SKIPPED`;`account` 为当前
436
- `PoolAccount` 或 `null`,`error` 为 `null` 或 `{ code: string | null, message: string }`。
437
- 取消信号会停止批次并直接拒绝当前调用,不会把取消记成账号失败或改变账号健康状态。
438
-
439
- `getStatus()` 可用于健康检查和后台展示,包含生命周期、账号库存、各运行态槽位数、PRIMARY/RETRY
440
- 角色槽位、对应等待队列、当前目标、策略上限、资源上限、资源压力和建连并发等信息。若业务需要等待
441
- 初始暖槽,可以在 API 对外接流量前轮询 `status.runtime.ready`;检测方法只在共享 deadline 内等待已安排的
442
- 槽位,不会同步创建连接。
443
-
444
- ### 部署与关闭
445
-
446
- 一个 SQLite 号池数据库同时只能由一个 Pool/进程持有,重复打开会抛出
447
- `POOL_ALREADY_OWNED`。为避免多个进程同时接管同一数据库,Pool 不会自动删除陈旧 owner lock;
448
- 进程异常退出后再次启动会抛出 `POOL_STALE_LOCK`,必须先确认原 Pool 进程已经停止,再人工删除错误
449
- `details.lockPath` 指向的 `.lock` 文件。因此 Pool 应作为 API 进程级单例创建,而不是在每个路由或
450
- 每次请求中创建。
451
- 使用 PM2 时应采用单实例 `fork` 模式,不要使用 `cluster`:
452
-
453
- ```js
454
- export default {
455
- apps: [{
456
- name: "api",
457
- script: "server.js",
458
- exec_mode: "fork",
459
- instances: 1,
460
- }],
461
- };
462
- ```
463
-
464
- Next.js 必须让持有 Pool 的路由运行在 Node.js runtime,不能使用 Edge runtime;常驻连接和本地
465
- SQLite owner lock 也不适合无状态 serverless 部署。由于 `better-sqlite3` 是原生依赖,建议在
466
- `next.config.js` 中把库及相关运行时包保持为服务端外部依赖:
467
-
468
- ```js
469
- /** @type {import("next").NextConfig} */
470
- const nextConfig = {
471
- serverExternalPackages: ["mytglib", "@mtcute/node", "better-sqlite3"],
472
- };
473
-
474
- export default nextConfig;
475
- ```
476
-
477
- 对应 Route Handler 显式声明:
478
-
479
- ```js
480
- export const runtime = "nodejs";
481
- ```
482
-
483
- 进程退出时应停止接收新请求并安全关闭 Pool:
484
-
485
- ```js
486
- await pool.close({ drainTimeoutMs: 10_000 });
487
- ```
488
-
489
- `close()` 会先等待正在执行的管理和检测操作;超过 drain deadline 后取消未完成操作,再销毁所有
490
- Telegram 客户端、定时器、缓存、SQLite 连接和 owner lock。关闭后的 Pool 不能复用,应重新调用
491
- `createPhoneStatusPool()`。如果强制关闭宽限期结束时仍有底层客户端销毁任务,`close()` 返回
492
- `POOL_CLOSE_TIMEOUT` 并保持 SQLite repository 和 owner lock;后台销毁结束后可再次调用 `close()`
493
- 完成资源释放。
494
-
495
- ### Rust API
496
-
497
- 同一个 Pool 还支持通过 `userId` 查询大约注册年月,Node.js 使用 `getRegistrationDate()`,Rust 使用
498
- `get_registration_date()`;暖槽授权、DeviceCheck 材料配置、并发与返回契约见
499
- [注册年月查询](docs/registration-date.md)。
500
-
501
- Rust 实现从 `grammers_core` 根模块导出 `create_phone_status_pool()`、`PhoneStatusPool` 及相关
502
- 输入、响应和状态类型。调度、持久化格式和暖槽策略与 Node.js 实现一致。手机号检测统一返回
503
- 扁平的 `PhoneStatusCheckResponse`,字段为 `wait_time`、`phone`、`started_at`、`ended_at`、
504
- `duration_ms`、`status` 和 `error`;serde 序列化后与 Node.js 的 camelCase 对齐。没有可用资源、
505
- 排队超时或资源压力不会返回旧的 `BUSY` 变体,而是返回
506
- `status = PhoneStatusPoolResult::OtherError`,序列化值为 `OTHER_ERROR`,并在 `error.code` 中区分
507
- `POOL_RETRY_TIMEOUT`、`POOL_QUEUE_FULL` 或 `POOL_RESOURCE_CRITICAL`。创建和管理操作的
508
- 无效输入、调用取消、Pool 生命周期错误或内部协调错误仍通过 `PhoneStatusPoolError` 返回。
509
- `test_account()` 的单账号失败会把 Telegram RPC 的具体文本写入 `PhoneStatusPoolError` 的 `message()`;
510
- 需要与 Node.js 对齐的结构化字段时调用 `phone_status_error()`,可取得 `message`、RPC 数值 `code`、
511
- `text` 和 `seconds`。`PhoneStatusPoolError::code()` 仍保留 Pool 层分类,批量 `test_accounts()` 的错误
512
- 报告契约不变。
513
- Rust Pool 使用 14 秒共享检测 deadline,每次 Telegram 检测 RPC 最多约 3 秒;首次只用 `PRIMARY`
514
- 暖槽,账号级失败后的重试只用隔离的 `RETRY` 暖槽。后续尝试由剩余 `RETRY` 资源、
515
- `max_accounts_per_phone` 和 deadline 共同限制;`PHONE_NUMBER_FLOOD` 按手机号保护,
516
- 不换账号。
517
- Rust 与 Node.js 采用相同的冻结账号规则:导入时探测到 `FROZEN` 或 `UNKNOWN` 不落库,运行中发现冻结
518
- 则进入终态 `QUARANTINED`。AppConfig 主动识别冻结返回 `ACCOUNT_INVALID`;Telegram RPC 真实返回的
519
- `FROZEN_*` 错误仍在错误 source 链中保留 `code = 420`,不会改写成 401。公共账号结构不暴露冻结
520
- 探测字段。AppConfig RPC 的致命账号错误与 `FLOOD_WAIT` 同样保留原始分类,不降级为 `UNKNOWN`。
521
- Rust 字段名使用 snake_case,通过 serde 序列化时使用 camelCase。资源上限使用 CPU、内存和文件
522
- 描述符评估;Tokio 没有稳定的 event-loop 利用率指标,因此 Rust 状态不会伪造该项数据。
523
-
524
- ```rust,no_run
525
- use grammers_core::{
526
- CheckPhoneStatusInput, CloseOptions, ImportAccountInput, PhoneStatusPoolError,
527
- PhoneStatusPoolOptions, TestAccountsInput, create_phone_status_pool,
528
- };
529
-
530
- # async fn example() -> Result<(), PhoneStatusPoolError> {
531
- let mut options = PhoneStatusPoolOptions::new("/secure/phone-status-pool/pool.sqlite");
532
- options.min_warm_slots = 10;
533
- options.retry_warm_slots = 200;
534
- options.max_accounts_per_phone = 8;
535
- let pool = create_phone_status_pool(options).await?;
536
-
537
- // 即使任一管理或检测步骤失败,下面仍会执行安全关闭。
538
- let operation: Result<(), PhoneStatusPoolError> = async {
539
- let account = pool
540
- .import_account(ImportAccountInput::new(
541
- "/secure/imports/13099434947.session",
542
- "/secure/imports/13099434947.json",
543
- ))
544
- .await?;
545
-
546
- let response = pool
547
- .check_phone_status(CheckPhoneStatusInput::new("+12025550123"))
548
- .await?;
549
- println!(
550
- "status={:?} waitTime={} durationMs={} error={:?}",
551
- response.status, response.wait_time, response.duration_ms, response.error,
552
- );
553
-
554
- let accounts = pool.list_accounts().await?;
555
- let detail = pool.get_account(&account.account_id).await?;
556
- let tested = pool.test_account(&account.account_id).await?;
557
- let test_report = pool.test_accounts(TestAccountsInput::default()).await?;
558
- let status = pool.get_status().await?;
559
- println!(
560
- "phone={} accounts={} detail={} tested={} healthy={} ready={}",
561
- account.phone,
562
- accounts.len(),
563
- detail.is_some(),
564
- tested.is_some(),
565
- test_report.healthy,
566
- status.runtime.ready,
567
- );
568
-
569
- pool.remove_account(&account.account_id).await?;
570
- Ok(())
571
- }
572
- .await;
573
-
574
- let close_result = pool.close(CloseOptions::default()).await;
575
- operation?;
576
- close_result?;
577
- # Ok(())
578
- # }
579
- ```
580
-
581
- `PhoneStatusPoolOptions::new()` 要求绝对数据库路径,`min_warm_slots` 省略时为 `10`,
582
- `retry_warm_slots` 默认为 `0`,`max_accounts_per_phone` 默认为 `8` 且最大为 `16`。创建方法只
583
- 打开本地状态并启动异步预热,不等待 Telegram 暖槽连接完成;可在接入业务流量前轮询
584
- `pool.get_status().await?.runtime.ready`。同一个数据库仍只能由一个 Pool 实例持有,进程退出前应
585
- 始终调用 `pool.close(CloseOptions::default()).await`。如果强制关闭宽限期结束时仍有后台清理,
586
- `close()` 返回 `POOL_CLOSE_TIMEOUT` 并暂时保留 SQLite owner lock;后台任务退出后可再次调用
587
- `close()` 完成资源释放。导入的 session JSON 最大为 1 MiB。Rust Pool 与 Node.js
588
- 使用相同的 Telethon/mtcute schema 自动识别、`session_file` stem 兼容和混合 schema 选择规则;
589
- mtcute 缺失 `dc_main` 时按其原语义使用默认生产 DC2。SQLite 与 StringSession 中的
590
- endpoint 只用于校验两个文件描述同一个会话;实际连接始终使用 grammers 内置的 Telegram DC
591
- 地址,上传文件不能指定 API 进程的 TCP 目标。`ImportAccountInput`、`TestAccountsInput` 和
592
- `CheckPhoneStatusInput` 的 `abort_signal` 是仅运行时字段,使用 serde 反序列化输入时需要由 Rust
593
- 调用方另行赋值。
594
-
595
- ## 一次性手机号状态检查
596
-
597
- `checkPhoneStatus()` 是独立于注册流程的根导出方法。Telegram 没有为此提供纯查询接口;
598
- 本方法使用一个已授权账号的 Telethon 或 mtcute `.session` 文件和对应 JSON,根据
599
- [`account.sendChangePhoneCode`](https://core.telegram.org/method/account.sendChangePhoneCode)
600
- 的响应或 RPC 错误推断号码状态:
601
-
602
- ```js
603
- import { checkPhoneStatus } from "mytglib";
604
-
605
- const result = await checkPhoneStatus({
606
- phone: "+12025550123",
607
- sessionFilePath: "/secure/account/account.session",
608
- sessionJsonFilePath: "/secure/account/account.json",
609
- proxy: "socks5://user:password@127.0.0.1:1080",
610
- timeout_ms: 30_000,
611
- abort_signal: new AbortController().signal,
612
- });
613
-
614
- console.log(result);
615
- ```
616
-
617
- JSON 必须包含与 `syncSessionJson()` 输出格式兼容的 `session_str`、`session_file`、`app_id`、`app_hash`
618
- 和客户端配置。导入已有授权 session 时,保存的 `app_version` 继续用于 `initConnection`;源文件保持
619
- 只读,账号池新生成的待持久化 Metadata 会把 iOS `override_layer` 规范为当前 `tl.LAYER`。该字段不会传给
620
- 底层客户端。重连不会重新授权,也不会自动重试
621
- `auth.signUp` 或任何其他授权写操作。`session_file` 可以写完整文件名
622
- `account.session` 或同名 stem `account`,但不得包含目录组件。`device_token` 可以省略、设为 `null`
623
- 或留空,空值按未提供处理。
624
- 方法会以只读方式检查 SQLite schema,自动识别 Telethon 的 `sessions` 表或 mtcute 的
625
- `key_value` 与 `auth_keys` 表,校验其主 DC、IPv4、端口和 auth key 与 JSON 的规范
626
- `session_str` 一致,再把必要连接状态复制到 `MemoryStorage`;不会初始化对应存储实现、执行迁移或修改
627
- 原 SQLite 的表和主数据库内容。不过 SQLite 在 WAL 模式下即使只读打开也可能创建或更新 `-shm` 等
628
- sidecar,因此这不等同于“文件系统零写入”。混合 schema 同样遵循“两侧一致、冲突拒绝、仅一侧有效
629
- 则使用有效侧”的规则。SQLite 与 StringSession 中的 endpoint 只用于交叉校验;实际联网会按
630
- `lang_pack` 对应的官方平台,使用相同 DC ID 的内置官方 seed。用于请求的临时客户端不会注册
631
- reCAPTCHA middleware,也不会求解或主动重放 challenge;正常主 DC 路径只调用一次 `client.call()`,
632
- 不做应用层重试。mtcute 仍可能为 DC 迁移或 MTProto 连接恢复重发底层请求,这不是 `maxRetryCount`
633
- 控制的应用层重试。请求的 `settings` 固定为
634
- `{ _: "codeSettings", allowFlashcall: true, allowFirebase: false, logoutTokens: [] }`。
635
- `timeout_ms` 默认为 `30000`。会话数据库读取、连接和目标 RPC 等受控阶段各自使用完整的
636
- `timeout_ms` 上限,而不是共享整个方法的总 deadline;资源清理也使用独立的同值上限,并且不会因
637
- 调用方取消而跳过。SQLite 查询是同步 native 调用:查询开始前已取消会阻止读取,查询开始后 JavaScript
638
- timer 或 `abort_signal` 不能硬中断;SQLite 锁等待最多使用 `5000ms` busy timeout,查询返回时还会复核
639
- 阶段期限并拒绝过期结果。其他受控异步阶段会响应 `abort_signal`;请求重试次数和 flood 自动等待均固定
640
- 为 `0`。客户端无论成功或失败都会在返回前尝试有界销毁。
641
-
642
- Rust 对应入口是 `grammers_core` 根导出的 `check_phone_status()`。它按 SQLite schema 自动识别内部
643
- Grammers、Telethon `sessions` 或 mtcute `key_value`/`auth_keys` 会话,并校验 JSON 中显式提供的
644
- `session_str` 与主 DC、IPv4/端口和 auth key 一致。Grammers 会话继续以内部
645
- `session_metadata` 为权威;Telethon/mtcute 的 schema、DC 与 auth key 在同一个只读事务快照中读取。
646
- 三种来源都只把授权 key 与连接配置复制到 `MemorySession`,不会导入、迁移或写回源数据库;外部会话
647
- 记录的地址只用于文件间交叉校验,实际连接仍使用 Grammers 内置的可信 Telegram DC 地址。外部会话的
648
- `session_file` 可写真实文件名或省略 `.session`/`.grammers` 的同名 stem;内部 `.grammers` 会话还兼容
649
- 旧导出 JSON 中同 stem 的 `.session` 名称,所有形式均不得包含目录组件。`app_id` 只需为正整数且
650
- `app_hash` 非空,因此不破坏已有自定义凭据;`device_token` 缺失、为 `null` 或空白时按未提供处理。
651
- Grammers 分支若提供非空 `device_token`,trim 后仍必须与内部 `session_metadata` 一致;省略或空值不会
652
- 从内部 Metadata 自动注入连接参数。临时客户端沿用保存的 iOS `app_version`,协议 layer 使用当前
653
- 编译期 `tl::LAYER`(当前为 229),`override_layer` 不参与连接。
654
- 输入字段使用 snake_case,结果序列化时使用与 npm 一致的 camelCase:
655
-
656
- ```rust,no_run
657
- use grammers_core::{AbortSignal, PhoneStatusCheckInput, Proxy, check_phone_status};
658
-
659
- # async fn example() -> Result<(), Box<dyn std::error::Error>> {
660
- let abort_signal = AbortSignal::new();
661
- let mut input = PhoneStatusCheckInput::new(
662
- "+12025550123",
663
- "/secure/account/account.grammers",
664
- "/secure/account/account.json",
665
- );
666
- input.proxy = Some(Proxy::from_url(
667
- "socks5://user:password@127.0.0.1:1080",
668
- ).expect("valid proxy URL"));
669
- input.timeout_ms = 30_000;
670
- input.abort_signal = Some(abort_signal);
671
-
672
- let result = check_phone_status(input).await;
673
- println!("{}", serde_json::to_string_pretty(&result)?);
674
- # Ok(())
675
- # }
676
- ```
677
-
678
- Rust 的 `timeout_ms` 覆盖文件读取、只读会话加载、连接及目标 RPC;每个阶段各自使用完整上限,而非
679
- 共享整个方法的总 deadline。`abort_signal` 也覆盖清理前的这些异步阶段;sender cleanup 使用独立的
680
- 同值上限且不会因调用方取消而跳过,因此超时或取消后仍会先尝试有界资源清理再返回。该方法使用
681
- `NoRetries`,且不自动等待 `FLOOD_WAIT`。`abort_signal` 标记为 serde skip,若
682
- 输入来自 JSON,必须在反序列化后由 Rust 代码赋值。`proxy` 可使用 `Proxy::from_url()`,也可直接
683
- 构造 `Proxy`。
684
-
685
- 这不是无副作用的号码查询:未注册号码可能因此真实收到验证码。调用方必须只在获授权的账号和号码
686
- 范围内使用,并自行控制调用频率,避免触发发送滥用或 `FLOOD_WAIT`。
687
-
688
- 返回对象固定包含 `waitTime`、原样传入的 `phone`、ISO 时间字符串 `startedAt`/`endedAt`、
689
- `durationMs`、`status` 和 `error`。`status` 可能为 `PHONE_NUMBER_OCCUPIED`、
690
- `PHONE_NUMBER_NO_OCCUPIED`、`PHONE_NUMBER_INVALID`、`PHONE_NUMBER_BANNED`、`FLOOD_WAIT`
691
- 或 `OTHER_ERROR`。`RECAPTCHA_CHECK_signup` 不会触发求解或再次请求,而是按当前业务规则返回
692
- `PHONE_NUMBER_NO_OCCUPIED`;`FLOOD_WAIT` 的秒数写入 `waitTime`;其他异常同样不会向外抛出,而是在 `error`
693
- 中保留可用的 `message`、`name`、`code`、`text` 和 `seconds`。sender cleanup 失败始终返回
694
- `OTHER_ERROR` 且 `waitTime` 为 `0`;若目标 RPC 同时失败,合并后的 message 会包含 cleanup 原因,并保留
695
- 可用的 RPC 关键字段。
696
-
697
- 授权成功后可调用 Telegram 官方的 [`account.getAuthorizations`](https://core.telegram.org/method/account.getAuthorizations)
698
- 获取当前账号的全部登录会话,并把响应保存到同目录的 `<session stem>.authorizations.json`:
699
-
700
- ```js
701
- const authorizationsJsonPath = await core.syncAuthorizationsJson();
702
- ```
703
-
704
- `syncAuthorizationsJson()` 无参数并返回 JSON 文件路径;Rust 对应入口为
705
- `CoreClient::sync_authorizations_json()`。文件保留 mtcute 的 camelCase 响应结构(根节点为
706
- `account.authorizations`),仅按官方 [`Authorization`](https://core.telegram.org/constructor/authorization)
707
- 构造器的 `date_created`(会话创建时间)语义,把 `dateCreated` 的 Unix 秒时间戳按 UTC 转为补零的
708
- `YYYY-MM` 字符串;为避免 JavaScript `Long` 的 JSON 对象结构和数字精度差异,`hash` 统一写为
709
- 十进制字符串,其他授权字段保持原始响应值。该方法要求客户端已初始化并完成授权,应在
710
- `destroy()` 前调用。
711
-
712
- `sendCode()`、`changePassword()`、`setNewPassword()`、`confirmPasswordEmail()`、`sendEmailCode()`、`verifyEmailCode()`、`verifyPhoneCode()`、`register()` 和 `submitCredentials()` 会直接向 `onLog` 发出 `started`、`completed` 或 `failed` 事件。日志不做脱敏,记录真实输入、响应和序列化后的错误;调用方需要自行控制日志文件的访问权限和生命周期。
713
-
714
- ## 两步验证密码
715
-
716
- Telegram 的两步验证密码流程先调用
717
- [`account.getPassword`](https://core.telegram.org/method/account.getPassword)
718
- 取得 SRP 参数,再调用
719
- [`account.updatePasswordSettings`](https://core.telegram.org/method/account.updatePasswordSettings)
720
- 更新密码。本库复用 `@mtcute/node` 的 SRP 与密码哈希实现,但会先按官方要求校验 2048-bit 安全素数、生成元和 `srp_B`,并把不足 256 字节的合法 `srp_B` 左侧补零后再计算 proof。
721
-
722
- 已有密码时使用 `changePassword()`:
723
-
724
- ```js
725
- await core.changePassword({
726
- currentPassword: "old password",
727
- newPassword: "new password",
728
- hint: "new hint",
729
- });
730
- ```
731
-
732
- 尚未启用密码时改用 `setNewPassword()`:
733
-
734
- ```js
735
- await core.setNewPassword({
736
- newPassword: "first password",
737
- });
738
- ```
739
-
740
- 两个方法都返回 `Promise<void>`,密码字符串不会被裁剪,且会随真实输入写入操作日志。`changePassword()` 省略 `hint` 时发送空字符串;`setNewPassword()` 省略 `hint` 时默认使用 `ATGHUB`,并且默认不设置恢复邮箱。需要恢复邮箱时显式传入 `email`;服务端返回 `EMAIL_UNCONFIRMED_%d` 后使用:
741
-
742
- ```js
743
- await core.confirmPasswordEmail({ emailCode: "123456" });
744
- ```
745
-
746
- 确认成功后才会把密码写入 Metadata 的 `two_fa` 字段。该字段按需求保存明文密码,请妥善保护 `.session` 文件。最终密码更新不会进入 `maxRetryCount` 控制的 internal/flood 自动重试,也不会因 reCAPTCHA challenge 被 middleware 重放。若账户已有 Telegram Passport 数据,方法会在提交前拒绝改密,避免未重加密 Passport secret 导致数据失联。
747
-
748
- ## 代理检查
749
-
750
- ```js
751
- import { checkProxy } from "mytglib";
752
-
753
- const result = await checkProxy({
754
- clientType: "android",
755
- proxy: "socks5://user:password@127.0.0.1:1080",
756
- timeoutMs: 12_000,
757
- });
758
-
759
- console.log(result);
760
- ```
761
-
762
- `checkProxy` 会并行检查所选平台的全部五个 Telegram 主 DC,并关闭每个已建立的连接。
763
-
764
- ## Rust fork
765
-
766
- `vendor/grammers` 是为后续 Tauri/Rust 桌面端准备的内置 fork,其中项目自有的
767
- `grammers-core` 承载 `src` 对应的 Rust 实现。它目前尚未接入本 npm 包的 JavaScript 运行路径,
768
- 也不会进入 npm 发布包。crate 根模块公开 `ApiCredentials`、`android::API_CREDENTIALS` 和
769
- `ios::API_CREDENTIALS`;`CoreClientConfig.api_id` 与 `api_hash` 同时省略时使用对应数组第一项,同时
770
- 提供时必须精确匹配同平台凭据对。Rust `CoreClient` 在 backend 连接初始化尚未完成、没有业务
771
- transport frame 写出时,若初始化明确超时会自动重新创建一次 backend;该限定重试独立于 RPC 的
772
- `max_retry_count`。
773
- 上游基线、本地覆盖层、
774
- 可重复的同步流程和验证范围见
775
- [grammers fork 维护文档](docs/grammers-fork.md)。
776
-
777
- ## 测试
778
-
779
- ```bash
780
- npm test
781
- npm run test:coverage
782
- ```