mytglib 1.1.7 → 2.0.1
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.md +75 -18
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -102,8 +102,10 @@ try {
|
|
|
102
102
|
}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
iOS 默认使用 `ios.CURRENT_VERSION`
|
|
106
|
-
`appVersion`
|
|
105
|
+
iOS 默认使用 `ios.CURRENT_VERSION` 的最后一项。覆盖版本时仍需同时传入 `appVersion` 与
|
|
106
|
+
`overrideLayer`,以兼容现有调用方;`appVersion` 会原样用于 Telegram `initConnection.appVersion`,
|
|
107
|
+
`overrideLayer` 不会传给底层客户端。Node.js 的协议 layer 始终由 `@mtcute/node` 当前 codec 的
|
|
108
|
+
`tl.LAYER` 决定,当前为 layer 229。
|
|
107
109
|
|
|
108
110
|
```js
|
|
109
111
|
const iosCore = new CoreClient({
|
|
@@ -112,14 +114,15 @@ const iosCore = new CoreClient({
|
|
|
112
114
|
proxy: { type: "http", host: "127.0.0.1", port: 8080 },
|
|
113
115
|
sessionDirPath: resolve("telegram-sessions"),
|
|
114
116
|
langCode: "en",
|
|
115
|
-
appVersion: "
|
|
116
|
-
overrideLayer:
|
|
117
|
+
appVersion: "12.8.1 (33181) ",
|
|
118
|
+
overrideLayer: 227,
|
|
117
119
|
deviceTokenResolver: async () => ({ token: "AQIDBA==" }),
|
|
118
120
|
});
|
|
119
121
|
```
|
|
120
122
|
|
|
121
|
-
覆盖值仅适用于 iOS;`appVersion` 必须是非空字符串,`overrideLayer` 必须是正数 int32。
|
|
122
|
-
|
|
123
|
+
覆盖值仅适用于 iOS;`appVersion` 必须是非空字符串,`overrideLayer` 必须是正数 int32。Rust fork
|
|
124
|
+
同样只把 `app_version` 用于 `initConnection`,协议 layer 始终使用编译期 `tl::LAYER`。Node.js 和 Rust
|
|
125
|
+
新写入的 `override_layer` Metadata 均记录当前真实 codec layer(当前为 229),不记录历史选择值。
|
|
123
126
|
`langCode` 同样只支持 iOS,并且必须来自公开的 `iosLangPackLanguages`;省略时继续根据
|
|
124
127
|
手机号自动选择。手工覆盖只改变 Telegram `langCode`,`systemLangCode` 仍按手机号地区推断。
|
|
125
128
|
|
|
@@ -137,12 +140,14 @@ const response = await core.sendCode({
|
|
|
137
140
|
});
|
|
138
141
|
```
|
|
139
142
|
|
|
140
|
-
传入的五个能力字段必须是 boolean;Android 的 `allowAppHash` 以及 iOS 的 APNs `token`、`appSandbox` 仍由平台配置自动生成,不属于可覆盖字段。根据 [mtcute 0.
|
|
143
|
+
传入的五个能力字段必须是 boolean;Android 的 `allowAppHash` 以及 iOS 的 APNs `token`、`appSandbox` 仍由平台配置自动生成,不属于可覆盖字段。根据 [mtcute 0.32.0 `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.0 只会序列化非空的 `logoutTokens`,因此空数组不会设置可选 TL vector flag。
|
|
141
144
|
|
|
142
145
|
`deviceTokenResolver` 和 `recaptchaMobileTokenResolver` 必须返回包含非空字符串 `token` 字段的对象,对象可保留求解服务返回的 `errorMessage`、`message` 等附加字段。无有效 token 时会抛出 `TypeError`,其 `cause` 是 resolver 返回的完整结果;reCAPTCHA token 最长为 16,384 个字符。
|
|
143
146
|
|
|
144
147
|
reCAPTCHA 由客户端的全局 network middleware 处理,不限于 `auth.sendCode`。任意尚未包装且允许重放的 RPC 返回格式严格匹配 `403 RECAPTCHA_CHECK_<action>__<siteKey>` 时,middleware 会调用 `recaptchaMobileTokenResolver`,使用 `invokeWithReCaptcha` 包装原请求,并通过后续的 `networkMiddlewares.basic()` 链重试一次。`account.updatePasswordSettings` 和 `payments.assignAppStoreTransaction` 不会被该 middleware 自动重放;已有 `invokeWithReCaptcha` 请求或包装请求再次失败时也不会重复求解、再次包装或形成无限重试。middleware 自动生成的 `invokeWithReCaptcha` 日志记录错误文本、完整原请求、真实响应和错误。
|
|
145
148
|
|
|
149
|
+
Rust `grammers_core` 对初始 `auth.sendCode` 及其自动生成的 `invokeWithReCaptcha(auth.sendCode)` 额外设置完整 frame 写出后的 20 秒响应期限。连接类失败会先关闭当前 DC sender,再用同一序列化请求和同一 reCAPTCHA token 在新代理隧道上重放一次;明确的 Telegram RPC 错误、取消和第二次连接失败不会重放。该规则不改变 Node.js `@mtcute/node` 实现,也不适用于 `signIn`、`signUp` 或其他 RPC。
|
|
150
|
+
|
|
146
151
|
`auth.sentCodePaymentRequired` 不是 RPC 错误,而是 `auth.sendCode` 的合法返回。它表示由于所在国家或运营商的短信验证成本较高,官方客户端必须先完成 Telegram Premium 购买流程才能继续登录或注册。原始响应中的 `storeProduct`、`premiumDays`、`currency` 和 `amount` 描述商品与价格,`phoneCodeHash` 保留后续授权上下文,`supportEmailAddress` 和 `supportEmailSubject` 用于联系支持。
|
|
147
152
|
|
|
148
153
|
`sendCode()` 或 `verifyEmailCode()` 收到该响应时,会像保存 `phoneCodeHash` 一样把三个必需字段原子地挂载到 `core.inputStore`:`{ premiumDays, currency, amount }`。普通 `auth.sentCode`、`auth.sentCodeSuccess`、授权成功及成功销毁客户端时会清空该状态。
|
|
@@ -161,7 +166,7 @@ const updates = await core.submitCredentials({
|
|
|
161
166
|
|
|
162
167
|
`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()`。
|
|
163
168
|
|
|
164
|
-
`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.
|
|
169
|
+
`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.0 的包装对象上调用 `destroy()`,从而避免私有字段错误。无论成功或失败,都应在 `finally` 中调用 `core.destroy()`,永久关闭连接、定时器和存储资源。销毁成功后两个客户端引用都会恢复为 `null`,SQLite 文件路径仍保留在 `core.sessionFilePath` 上。
|
|
165
170
|
|
|
166
171
|
初始化 `.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()` 重试当前全部待写操作。
|
|
167
172
|
|
|
@@ -220,6 +225,13 @@ StringSession 优先读取 `session_str`,也兼容 `session_string`;两者
|
|
|
220
225
|
SQLite 与 StringSession 中的 endpoint 只用于交叉校验;实际创建 Pool 客户端时会按 JSON 对应的
|
|
221
226
|
官方 iOS 或 Android 平台,使用相同 DC ID 的内置官方 seed,不会连接上传文件指定的地址。
|
|
222
227
|
`device_token` 可以省略、设为 `null` 或留空;空值按未提供处理,不会写入连接参数。
|
|
228
|
+
导入会在写入账号池前主动检查账号状态,并且只接受 `UNFROZEN`:`FROZEN` 返回
|
|
229
|
+
`ACCOUNT_INVALID`,无法确认的 `UNKNOWN` 返回 `POOL_CONNECTION_FAILED`,两者都不会留下账号记录或
|
|
230
|
+
可调度槽位。只有 Telegram RPC 本身真实返回 `FROZEN_METHOD_INVALID` 或
|
|
231
|
+
`FROZEN_PARTICIPANT_MISSING` 时,才会原样保留其 `code = 420` 和错误文本;内部按认证失效一类
|
|
232
|
+
致命账号错误处置,不会把真实错误改写成 401。AppConfig RPC 的普通失败或超时才归为 `UNKNOWN`;
|
|
233
|
+
若该 RPC 真实返回现有致命账号错误或 `FLOOD_WAIT`,导入会保留原错误并拒绝落库,池内自检则分别
|
|
234
|
+
执行终态隔离或服务端时长冷却。
|
|
223
235
|
|
|
224
236
|
`minWarmSlots` 可省略,默认值为 `10`,可设为正的 JavaScript 安全整数,不再固定限制为 50;
|
|
225
237
|
`minWarmSlots * 5` 也必须是 JavaScript 安全整数。
|
|
@@ -271,6 +283,8 @@ console.log(response.status, response.waitTime, response.error);
|
|
|
271
283
|
返回最后一次已实际发生的暖槽错误;如果尚未发生任何暖槽错误就无可用槽位,则直接返回 Pool busy。
|
|
272
284
|
触发 `FLOOD_WAIT` 的账号会按 Telegram 返回的等待秒数退出暖槽并进入冷却,
|
|
273
285
|
Pool 随即从库存异步补入其他账号。
|
|
286
|
+
若暖槽返回冻结账号错误,Pool 会将该账号永久隔离并换用其他 `READY` 槽位;当前检测返回的错误仍保留
|
|
287
|
+
Telegram 原始 `code = 420` 和错误文本。
|
|
274
288
|
`PHONE_NUMBER_FLOOD` 是目标手机号级错误,不换槽重试,只保护目标手机号 5 分钟且不处罚账号。
|
|
275
289
|
明确检测结果缓存 5 分钟,最多保存 10,000 条;同一手机号的并发请求会合并为一次检测,
|
|
276
290
|
基础设施错误不缓存,也不提供强制刷新。
|
|
@@ -289,9 +303,13 @@ single-flight RPC。即使当前只有一个等待者,后台检测仍可完成
|
|
|
289
303
|
const accounts = await pool.listAccounts();
|
|
290
304
|
const detail = await pool.getAccount(account.accountId);
|
|
291
305
|
const testResult = await pool.testAccount(account.accountId);
|
|
306
|
+
const testReport = await pool.testAccounts({
|
|
307
|
+
abortSignal: new AbortController().signal,
|
|
308
|
+
});
|
|
292
309
|
const status = await pool.getStatus();
|
|
293
310
|
|
|
294
|
-
console.log(accounts, detail, testResult, status.runtime);
|
|
311
|
+
console.log(accounts, detail, testResult, testReport, status.runtime);
|
|
312
|
+
console.log(accounts.map(({ phone, cooldownUntil }) => ({ phone, cooldownUntil })));
|
|
295
313
|
|
|
296
314
|
// 删除会先停止派单,等待该账号当前任务结束,再断开连接并删除持久化记录。
|
|
297
315
|
const removed = await pool.removeAccount(account.accountId);
|
|
@@ -299,9 +317,34 @@ const removed = await pool.removeAccount(account.accountId);
|
|
|
299
317
|
|
|
300
318
|
账号运行态由 Pool 管理:可用账号处于 `STANDBY`、`RESERVED`、`CONNECTING`、`READY` 或
|
|
301
319
|
`BUSY`;触发 flood 的账号进入 `COOLDOWN`,认证失效等致命错误会进入 `QUARANTINED`。
|
|
302
|
-
`testAccount()`
|
|
303
|
-
|
|
304
|
-
|
|
320
|
+
`testAccount()` 是由管理员主动触发的账号健康自检,不会拿这个账号去检测某个外部手机号,也不会由
|
|
321
|
+
协调器定时自动执行。`QUARANTINED` 是终态;自检不会把已隔离账号恢复为 `ACTIVE` 或重新加入调度。
|
|
322
|
+
管理接口返回完整 `phone` 和 ISO 格式或 `null` 的 `cooldownUntil`,但不会包含 auth key、原始
|
|
323
|
+
Metadata、session、Telegram 冻结时间或申诉地址等内部探测字段。
|
|
324
|
+
|
|
325
|
+
`testAccounts()` 由使用者主动触发,会固定批次开始时的账号快照并严格串行调用单账号自检。同一个 Pool
|
|
326
|
+
同时只运行一个批次;已经 `BUSY` 的账号不会被等待或抢断,而是记为 `FAILED` 后继续。已隔离账号不联网,
|
|
327
|
+
直接记为 `QUARANTINED`;批次中的账号被删除或进入 `REMOVING` 时记为 `SKIPPED`。返回结构固定为:
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
{
|
|
331
|
+
total: 1,
|
|
332
|
+
healthy: 1,
|
|
333
|
+
quarantined: 0,
|
|
334
|
+
failed: 0,
|
|
335
|
+
skipped: 0,
|
|
336
|
+
results: [{
|
|
337
|
+
accountId: "account-id",
|
|
338
|
+
outcome: "HEALTHY",
|
|
339
|
+
account: { /* PoolAccount */ },
|
|
340
|
+
error: null,
|
|
341
|
+
}],
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`outcome` 只会是 `HEALTHY`、`QUARANTINED`、`FAILED` 或 `SKIPPED`;`account` 为当前
|
|
346
|
+
`PoolAccount` 或 `null`,`error` 为 `null` 或 `{ code: string | null, message: string }`。
|
|
347
|
+
取消信号会停止批次并直接拒绝当前调用,不会把取消记成账号失败或改变账号健康状态。
|
|
305
348
|
|
|
306
349
|
`getStatus()` 可用于健康检查和后台展示,包含生命周期、账号库存、各运行态槽位数、当前目标、
|
|
307
350
|
策略上限、资源上限、资源压力和建连并发等信息。若业务需要等待初始暖槽,可以在 API 对外接流量前
|
|
@@ -369,13 +412,17 @@ Rust 实现从 `grammers_core` 根模块导出 `create_phone_status_pool()`、`P
|
|
|
369
412
|
生命周期错误或内部协调错误才通过 `PhoneStatusPoolError` 返回,不应转换成业务 `BUSY`。
|
|
370
413
|
Rust Pool 同样在共享的 10 秒总期限内对 `FLOOD_WAIT` 和暖槽 RPC 其他错误最多换槽重试三次,
|
|
371
414
|
无 `READY` 槽位、期限耗尽或 `PHONE_NUMBER_FLOOD` 不会继续换槽。
|
|
415
|
+
Rust 与 Node.js 采用相同的冻结账号规则:导入时探测到 `FROZEN` 或 `UNKNOWN` 不落库,运行中发现冻结
|
|
416
|
+
则进入终态 `QUARANTINED`。AppConfig 主动识别冻结返回 `ACCOUNT_INVALID`;Telegram RPC 真实返回的
|
|
417
|
+
`FROZEN_*` 错误仍在错误 source 链中保留 `code = 420`,不会改写成 401。公共账号结构不暴露冻结
|
|
418
|
+
探测字段。AppConfig RPC 的致命账号错误与 `FLOOD_WAIT` 同样保留原始分类,不降级为 `UNKNOWN`。
|
|
372
419
|
Rust 字段名使用 snake_case,通过 serde 序列化时使用 camelCase。资源上限使用 CPU、内存和文件
|
|
373
420
|
描述符评估;Tokio 没有稳定的 event-loop 利用率指标,因此 Rust 状态不会伪造该项数据。
|
|
374
421
|
|
|
375
422
|
```rust,no_run
|
|
376
423
|
use grammers_core::{
|
|
377
424
|
CheckPhoneStatusInput, CloseOptions, ImportAccountInput, PhoneStatusCheckResponse,
|
|
378
|
-
PhoneStatusPoolError, PhoneStatusPoolOptions, create_phone_status_pool,
|
|
425
|
+
PhoneStatusPoolError, PhoneStatusPoolOptions, TestAccountsInput, create_phone_status_pool,
|
|
379
426
|
};
|
|
380
427
|
|
|
381
428
|
# async fn example() -> Result<(), PhoneStatusPoolError> {
|
|
@@ -410,12 +457,15 @@ use grammers_core::{
|
|
|
410
457
|
let accounts = pool.list_accounts().await?;
|
|
411
458
|
let detail = pool.get_account(&account.account_id).await?;
|
|
412
459
|
let tested = pool.test_account(&account.account_id).await?;
|
|
460
|
+
let test_report = pool.test_accounts(TestAccountsInput::default()).await?;
|
|
413
461
|
let status = pool.get_status().await?;
|
|
414
462
|
println!(
|
|
415
|
-
"accounts={} detail={} tested={} ready={}",
|
|
463
|
+
"phone={} accounts={} detail={} tested={} healthy={} ready={}",
|
|
464
|
+
account.phone,
|
|
416
465
|
accounts.len(),
|
|
417
466
|
detail.is_some(),
|
|
418
467
|
tested.is_some(),
|
|
468
|
+
test_report.healthy,
|
|
419
469
|
status.runtime.ready,
|
|
420
470
|
);
|
|
421
471
|
|
|
@@ -440,7 +490,7 @@ use grammers_core::{
|
|
|
440
490
|
使用相同的 Telethon/mtcute schema 自动识别、`session_file` stem 兼容和混合 schema 选择规则;
|
|
441
491
|
mtcute 缺失 `dc_main` 时按其原语义使用默认生产 DC2。SQLite 与 StringSession 中的
|
|
442
492
|
endpoint 只用于校验两个文件描述同一个会话;实际连接始终使用 grammers 内置的 Telegram DC
|
|
443
|
-
地址,上传文件不能指定 API 进程的 TCP 目标。`ImportAccountInput` 和
|
|
493
|
+
地址,上传文件不能指定 API 进程的 TCP 目标。`ImportAccountInput`、`TestAccountsInput` 和
|
|
444
494
|
`CheckPhoneStatusInput` 的 `abort_signal` 是仅运行时字段,使用 serde 反序列化输入时需要由 Rust
|
|
445
495
|
调用方另行赋值。
|
|
446
496
|
|
|
@@ -467,7 +517,10 @@ console.log(result);
|
|
|
467
517
|
```
|
|
468
518
|
|
|
469
519
|
JSON 必须包含与 `syncSessionJson()` 输出格式兼容的 `session_str`、`session_file`、`app_id`、`app_hash`
|
|
470
|
-
|
|
520
|
+
和客户端配置。导入已有授权 session 时,保存的 `app_version` 继续用于 `initConnection`;源文件保持
|
|
521
|
+
只读,账号池新生成的待持久化 Metadata 会把 iOS `override_layer` 规范为当前 `tl.LAYER`。该字段不会传给
|
|
522
|
+
底层客户端。重连不会重新授权,也不会自动重试
|
|
523
|
+
`auth.signUp` 或任何其他授权写操作。`session_file` 可以写完整文件名
|
|
471
524
|
`account.session` 或同名 stem `account`,但不得包含目录组件。`device_token` 可以省略、设为 `null`
|
|
472
525
|
或留空,空值按未提供处理。
|
|
473
526
|
方法会以只读方式检查 SQLite schema,自动识别 Telethon 的 `sessions` 表或 mtcute 的
|
|
@@ -498,7 +551,8 @@ Grammers、Telethon `sessions` 或 mtcute `key_value`/`auth_keys` 会话,并
|
|
|
498
551
|
旧导出 JSON 中同 stem 的 `.session` 名称,所有形式均不得包含目录组件。`app_id` 只需为正整数且
|
|
499
552
|
`app_hash` 非空,因此不破坏已有自定义凭据;`device_token` 缺失、为 `null` 或空白时按未提供处理。
|
|
500
553
|
Grammers 分支若提供非空 `device_token`,trim 后仍必须与内部 `session_metadata` 一致;省略或空值不会
|
|
501
|
-
从内部 Metadata
|
|
554
|
+
从内部 Metadata 自动注入连接参数。临时客户端沿用保存的 iOS `app_version`,协议 layer 使用当前
|
|
555
|
+
编译期 `tl::LAYER`(当前为 229),`override_layer` 不参与连接。
|
|
502
556
|
输入字段使用 snake_case,结果序列化时使用与 npm 一致的 camelCase:
|
|
503
557
|
|
|
504
558
|
```rust,no_run
|
|
@@ -613,7 +667,10 @@ console.log(result);
|
|
|
613
667
|
|
|
614
668
|
`vendor/grammers` 是为后续 Tauri/Rust 桌面端准备的内置 fork,其中项目自有的
|
|
615
669
|
`grammers-core` 承载 `src` 对应的 Rust 实现。它目前尚未接入本 npm 包的 JavaScript 运行路径,
|
|
616
|
-
也不会进入 npm
|
|
670
|
+
也不会进入 npm 发布包。Rust `CoreClient` 在 backend 连接初始化尚未完成、没有业务 transport frame
|
|
671
|
+
写出时,若初始化明确超时会自动重新创建一次 backend;该限定重试独立于 RPC 的 `max_retry_count`。
|
|
672
|
+
上游基线、本地覆盖层、
|
|
673
|
+
可重复的同步流程和验证范围见
|
|
617
674
|
[grammers fork 维护文档](docs/grammers-fork.md)。
|
|
618
675
|
|
|
619
676
|
## 测试
|