@zhin.js/adapter-icqq 1.1.28 → 1.1.29
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/CHANGELOG.md +32 -0
- package/README.md +59 -9
- package/adapters/icqq/index.js +3 -0
- package/adapters/icqq/index.ts +3 -0
- package/commands/icqq/login/code/index.js +41 -16
- package/commands/icqq/login/code/index.ts +37 -13
- package/commands/icqq/login/confirm/index.js +15 -9
- package/commands/icqq/login/confirm/index.ts +15 -6
- package/commands/icqq/login/definition.js +57 -3
- package/commands/icqq/login/definition.ts +62 -3
- package/commands/icqq/login/qr/index.js +14 -16
- package/commands/icqq/login/qr/index.ts +17 -15
- package/commands/icqq/login/status/index.js +32 -13
- package/commands/icqq/login/status/index.ts +30 -12
- package/lib/endpoint.d.ts +8 -1
- package/lib/endpoint.js +170 -41
- package/lib/i18n.d.ts +1 -1
- package/lib/i18n.js +283 -15
- package/lib/icqq-login-assist.d.ts +113 -20
- package/lib/icqq-login-assist.js +468 -97
- package/lib/icqq-login-control.d.ts +4 -1
- package/lib/icqq-login-descriptors.d.ts +48 -0
- package/lib/icqq-login-descriptors.js +187 -0
- package/lib/icqq-login-errors.d.ts +42 -0
- package/lib/icqq-login-errors.js +77 -0
- package/lib/sdk-logger.d.ts +14 -0
- package/lib/sdk-logger.js +44 -0
- package/package.json +19 -19
- package/src/endpoint.ts +167 -48
- package/src/i18n.ts +283 -15
- package/src/icqq-login-assist.ts +533 -114
- package/src/icqq-login-control.ts +4 -1
- package/src/icqq-login-descriptors.ts +200 -0
- package/src/icqq-login-errors.ts +112 -0
- package/src/sdk-logger.ts +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @zhin.js/adapter-icqq
|
|
2
2
|
|
|
3
|
+
## 1.1.29
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f69feaf: 设备锁登录不再循环:等待验证期间不自动重新登录(重登会作废正在验证的链接),超过等待时间未处理才按退避重试;收到设备锁时不再自动发送短信,打开链接完成验证后提交 `ok` 继续,需要短信时提交 `sms` 再提交验证码,短信发送失败会重新提示。
|
|
8
|
+
- f2fa3d7: ICQQ 改用登录辅助 v2 的登录流程(ADR 0058 P2):每次登录一条流程(连接 → 挑战 → 验证中 → 在线 / 失败),声明 `login: { mode: 'interactive', relogin: true, refreshQr: true }` 并实现「重新登录」「刷新二维码」钩子。扫码时自动跟踪扫码状态(已扫码提示、确认后自动继续、过期自动换码两次);滑块带托管验证码辅助页(`media.captcha`)和手动提取 ticket 的分步说明,NT 登录提交 `ticket,randstr`、旧版只提交 ticket;设备锁提供「我已在手机完成验证」和「改用短信验证码」(6 位验证码、60 秒后可重新发送,发送失败报 `rate_limited` / `unknown` 并留在设备锁);身份验证提供设备 JSON(敏感,只在活动挑战上)、验证链接二维码和只在 `accounts.qq.com/login/attack` 运行的静态辅助脚本。`system.login.error` 按一张表映射到错误码,验证码错误留在原挑战;需要人处理的失败不再自动重新登录,只有被踢下线、网络错误等不需要验证的失败保留退避重试并记入时间线。QQ 命令 `icqq login confirm / code / status / qr` 按挑战作答(设备锁 `code sms`、短信 `code resend`),`code` 支持带 QQ 号。core:流程挑战的兼容待办 `payload.message` 带上简短说明,`payload.url` 在没有 `media.link` 时取第一个链接块。
|
|
9
|
+
|
|
10
|
+
adapter:`AdapterIndex.loginControl` 的登录钩子(重新登录、刷新挑战)在账号启动中(卡在登录挑战上)也能调用,新增 `acquireLogin` 租约;已停止的账号和被替换的旧 incarnation 仍然拒绝。
|
|
11
|
+
|
|
12
|
+
验证链接带会话标识:icqq 日志只记录挑战类型和链接主机名;core 的 `withoutLoginMedia`(`endpoint.login.flow` 事件与历史)去掉滑块 / 设备锁 / 身份验证 / link_verify 挑战的 `media.link`、`media.captcha` 和链接块,并标 `linkOmitted: true`(console-protocol 增加该字段)。
|
|
13
|
+
|
|
14
|
+
复审修正:NT 滑块答错后重新登录拿新滑块(NT / 旧版格式按挑战记录,不再重新判断);「ticket 已过期」映射为 `challenge_expired`;`system.receive` 的登录挑战副事件只带 `{ kind, host }`;包装 icqq SDK 日志,去掉链接查询串和手机号(SDK 直接写控制台的终端二维码和 237 分支 `console.log` 无法拦截,已在 README 说明);自动重新登录 8 次用尽后流程失败并通知;扫码时 SDK 的 `internal.error.qrcode` 转为结果;登录钩子随租约中止;token 不兼容提示删除 token 文件;`icqq login code` 在多账号时不回显参数;短信冷却跨重新登录保留。core:纯字符串等于某个无输入操作的 id 时选择该操作(旧 Console 输入 `sms` 仍可用)。
|
|
15
|
+
|
|
16
|
+
- b2aae3c: 登录流程事件与历史不再带验证链接:`withoutLoginMedia` 去掉滑块 / 设备锁 / 授权 / 链接验证挑战的 `media.link`、`media.captcha` 与链接块(它们带 sid),标记 `media.linkOmitted: true`,需要时用 `login.flow.get` 读取;二维码挑战不变。icqq 登录辅助日志只记录挑战类型与链接主机名。
|
|
17
|
+
- Updated dependencies [f74b4a5]
|
|
18
|
+
- Updated dependencies [ce8edf5]
|
|
19
|
+
- Updated dependencies [f2fa3d7]
|
|
20
|
+
- Updated dependencies [eba2566]
|
|
21
|
+
- Updated dependencies [ac72706]
|
|
22
|
+
- Updated dependencies [8f237bd]
|
|
23
|
+
- Updated dependencies [b2aae3c]
|
|
24
|
+
- Updated dependencies [3c14aeb]
|
|
25
|
+
- @zhin.js/core@1.7.0
|
|
26
|
+
- @zhin.js/adapter@1.1.34
|
|
27
|
+
- @zhin.js/adapter-onebot-events@0.0.8
|
|
28
|
+
- @zhin.js/page@1.1.19
|
|
29
|
+
- @zhin.js/agent-feature@1.1.18
|
|
30
|
+
- @zhin.js/feature-kit@1.1.17
|
|
31
|
+
- @zhin.js/permission@1.2.1
|
|
32
|
+
- @zhin.js/skill@1.1.18
|
|
33
|
+
- @zhin.js/tool@1.1.20
|
|
34
|
+
|
|
3
35
|
## 1.1.28
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -165,7 +165,7 @@ IM turn 的场景,应通过 Host 提供的显式账号作用域操作,不能
|
|
|
165
165
|
- **Console 社交/群管 RPC 已接线**:endpoint 把好友/群/群成员列表、请求审批和群管操作归一化为冻结的 `EndpointManagement`。Host 只消费该语义端口。
|
|
166
166
|
- 好友/入群请求与通知:经 the unified `endpoint[endpointSpec].events` ingress 分发到 `handlers`(`notice.receive` / `request.receive`);审批走 `Request.$approve` / `EndpointManagement.approveRequest`。Console `request.list` 优先读 `management.listRequests()`(`getSystemMsg`),不再写入 `unified_inbox_request/notice`。
|
|
167
167
|
- `system.*`(登录扫码等)分发到 `system.receive`。
|
|
168
|
-
-
|
|
168
|
+
- **登录辅助**:每次登录是一条登录流程([ADR 0058](https://zhin.js.org/adr/0058-login-assist-v2)),`system.login.qrcode|slider|device|auth` 变成流程里的挑战,Console、终端和 QQ 命令都能作答,见下文「登录」。
|
|
169
169
|
|
|
170
170
|
## Agent 会话与消息操作
|
|
171
171
|
|
|
@@ -179,9 +179,9 @@ ICQQ Agent 可按需加载 `icqq-conversation` 查询当前群/频道和 Bot
|
|
|
179
179
|
|
|
180
180
|
- 查看运行中的连接、点「测试连接」做连通性诊断;
|
|
181
181
|
- 在账号库保存、修改、启动、停止、替换或删除该 Adapter 的账号;新增仅保存,不创建 SDK Client,需手动启动;
|
|
182
|
-
-
|
|
182
|
+
- 处理登录(扫码、滑块、设备锁、短信、身份验证),并「重新登录」或「刷新二维码」;
|
|
183
183
|
|
|
184
|
-
ICQQ
|
|
184
|
+
ICQQ 首次登录的二维码、滑块、设备锁会出现在「登录待办」里,不需要再回到终端确认。
|
|
185
185
|
|
|
186
186
|
读取需要 `icqq:read`(`read` 级别,内置角色 `viewer` 及以上都有);增删改账号等写操作需要 `icqq:write`(`operate` 级别,`operator` 及以上都有,绑定到本适配器的角色也算),首次迁移还需管理员身份;全局所有者不受限,见[插件权限的级别](https://zhin.js.org/console/#插件权限的级别)。页面里的「添加 endpoint」等价于原来的 `icqq endpoint add`。
|
|
187
187
|
|
|
@@ -189,6 +189,54 @@ ICQQ 首次登录的二维码、滑块、设备确认会出现在「登录待办
|
|
|
189
189
|
|
|
190
190
|
启停意图在进程重启后恢复。停止或删除会取消并关闭目标账号的在途操作,其他账号继续运行;删除保留历史。已启动账号的连接配置更新先关闭旧 incarnation,再仅重连该账号;平台身份变化按替换处理,分配新内部身份、保留旧历史,新账号不继承旧 Session、个人设置或启动意图。提交结果未知时先刷新核实,不自动重复 CAS 或平台操作。
|
|
191
191
|
|
|
192
|
+
## 登录
|
|
193
|
+
|
|
194
|
+
适配器声明 `login: { mode: 'interactive', challenges: ['qrcode', 'slider', 'device', 'auth', 'sms'], relogin: true, refreshQr: true }`。每次登录尝试是一条登录流程:连接 → 挑战 → 验证中 → 在线或失败。提交后能看到真实结果(验证码错误、太频繁、已过期等),不再是「提交了就没消息」。
|
|
195
|
+
|
|
196
|
+
| 挑战 | 怎么完成 |
|
|
197
|
+
|------|----------|
|
|
198
|
+
| 扫码登录 | 用手机 QQ 扫码,再在手机上确认。扫码后时间线显示「已扫码,请在手机上确认登录」,确认后自动继续;没有继续时点「我已确认登录」。二维码过期会自动换新的(最多两次),之后点「刷新二维码」。 |
|
|
199
|
+
| 滑块验证 | 在 Console 打开验证码辅助页(默认 `https://zhin.js.org/login-assist/tcaptcha.html`,可用 `console.loginAssist.captchaHelperUrl` 换成自己的页面),完成后自动提交,不需要开发者工具。手动方式:打开滑块链接,按 F12 在「网络」面板找到 `cap_union_new_verify` 的响应,复制 `ticket`,或者直接粘贴整段响应 / 带 ticket 的链接,会自动提取。 |
|
|
200
|
+
| 设备锁 | 用手机 QQ 扫验证链接的二维码(或在手机上打开链接)完成验证,再点「我已在手机完成验证」。SDK 给出密保手机时可以点「改用短信验证码」:发送验证码后输入 6 位数字,「重新发送」要等 60 秒(按账号计,重新登录也不清零)。不点就不会发短信。只能输入文字的旧版 Console、终端里输入 `sms` 同样表示改用短信。 |
|
|
201
|
+
| 身份验证 | 按步骤:① 复制设备 JSON;② 打开验证链接,进入发送验证码的页面;③ 在该页开发者工具 Console 运行辅助脚本(也可拖到书签栏),按提示粘贴 JSON;④ 完成后点「已完成,继续登录」。不要把设备 JSON 粘贴到其他网站或发给他人。 |
|
|
202
|
+
|
|
203
|
+
细节:
|
|
204
|
+
|
|
205
|
+
- **滑块提交格式**:NT 登录(SDK 的 `sig.nt_captcha` 指向这个滑块)提交 `ticket,randstr`,缺 randstr 时会提示并留在滑块;旧版 wtlogin 只提交 `ticket`。
|
|
206
|
+
- **验证链接**带会话标识(sid / sig),密保手机号也算敏感信息:
|
|
207
|
+
- Zhin 自己的日志和 `system.receive` 副事件只有挑战类型和链接的主机名(`{ kind, host }`),不含链接、手机号、设备信息和二维码图片;`login.error` 只带错误码和清洗后的说明。
|
|
208
|
+
- icqq SDK 自己的日志(log4js,`mark` 级别会打印滑块、设备锁、身份验证链接和手机号)经适配器包装:链接只保留到路径(查询串变成 `?…`),手机号变成 `***`。
|
|
209
|
+
- **未覆盖**:SDK 绕过日志直接写到控制台的两处——终端里画出的登录二维码,以及旧版登录 237 分支的 `console.log(proto.toJSON())`(可能包含验证页信息)。这两处不改 SDK 无法拦截;如果控制台输出会被收集,请注意。
|
|
210
|
+
- `endpoint.login.flow` 事件和历史里不含验证链接(`linkOmitted`),Console 用 `login.flow.get` 读取当前链接。
|
|
211
|
+
- **设备 JSON** 只存在于当前挑战上:挑战结束即清空;不写 Zhin 的日志,不进事件和 `system.receive`。辅助脚本是固定文本,只在 `accounts.qq.com/login/attack` 上运行,只设置 `window.__INITIAL_STATE__.deviceInfo`,从不自动提交,也不含任何设备数据。
|
|
212
|
+
- **错误码**:`system.login.error` 按一张表映射到 `code_invalid`(验证码 / ticket 错误,留在原挑战重试)、`rate_limited`、`challenge_expired`、`risk_blocked`(冻结、设备被拉黑等)、`credential_invalid`(密码错误)、`network`、`unknown`。平台原文只作为清洗后的说明显示(单行,不含本机路径)。
|
|
213
|
+
- **自动重新登录**:有人正在答挑战时不会自动重新登录。验证失败、挑战过期等需要人处理的情况,流程停在「失败 / 已过期」,等你点「重新登录」或发 `icqq login retry`。只有不需要验证的失败(被踢下线、网络或服务器错误、没有出现任何挑战的普通登录失败)才按退避自动重试(5 秒起,最多 8 次),每次重试都记在时间线上;8 次都失败后流程标记为失败(`next: relogin`),并发送不再自动重试的掉线通知。
|
|
214
|
+
- **NT 滑块答错**:NT 验证码提交一次就作废,适配器会重新登录拿一个新滑块(失败则流程失败,等你重新登录);旧版滑块留在原挑战重试。
|
|
215
|
+
- **token 不兼容**(错误码 123456):重新登录读到的还是同一个 token 文件,所以提示删除数据目录里的 `<QQ号>_*_token` 后再登录,而不是建议重新登录。
|
|
216
|
+
- Console「刷新二维码」在扫码挑战上直接换一张新二维码;其他挑战不能单独刷新,会改为重新登录。
|
|
217
|
+
|
|
218
|
+
## 滑块验证助手页
|
|
219
|
+
|
|
220
|
+
ICQQ 登录要求滑块验证时,Console「登录待办」可以用弹窗打开验证助手页 `https://zhin.js.org/login-assist/tcaptcha.html`:你在弹窗里完成滑块,结果自动回填到待办,不必再打开浏览器开发者工具、从 `cap_union_new_verify` 响应里抄 ticket。
|
|
221
|
+
|
|
222
|
+
- **它做什么**:在本页加载腾讯验证码脚本(`https://ssl.captcha.qq.com/template/TCapIframeApi.js` 加滑块链接的查询参数,和腾讯自己的验证页发出的请求一致),由你本人完成验证;通过后用 `postMessage` 把 `{ type: 'zhin.login-assist.captcha', nonce, ticket, randstr }` 交回打开它的 Console 窗口(目标源固定为 Console 的源,从不用 `*`),约 1.5 秒后自动关闭。
|
|
223
|
+
- **它不做什么**:不替你过验证码;不保存、不上报任何数据(无统计、无存储,除腾讯验证码外不发其他请求)。参数只放在 URL 的 `#` 片段里,不会进入任何服务器日志,并在加载腾讯脚本前从地址栏移除。
|
|
224
|
+
- **为什么是独立的域名**:Console 把登录凭据存在自己源的 localStorage,第三方脚本不能在 Console 的源里运行;而腾讯脚本必须在页面自己的源里执行(放进沙箱 iframe 会一直停在「安全检测中」)。所以助手页放在 `zhin.js.org`,不放在 Console 里。
|
|
225
|
+
- **在别的设备上打开**(例如把链接发到手机):没有可回传的 Console 窗口,页面会显示 `ticket,randstr` 文本和「复制」按钮,把它粘贴到 Console 登录待办的滑块输入框提交即可。
|
|
226
|
+
- **验证没通过或被关闭**:页面显示「验证未完成」,点「重试」重新加载验证码。
|
|
227
|
+
|
|
228
|
+
链接格式:`#` 后是 base64url 编码的 JSON `{ q, n, o }`。`q` 是滑块链接的查询串(开头的 `?` 可有可无,参数名在白名单内,值必须已 URL 编码,总长不超过 4096);`n` 是 Console 生成的一次性标识(1–128 个 `A-Z a-z 0-9 . _ ~ -` 字符);`o` 是 Console 的源(如 `https://console.example.com`,只允许 http/https,不能带路径)。不合格的链接会被拒绝,并且不会加载腾讯脚本。
|
|
229
|
+
|
|
230
|
+
**自建**:不想依赖 `zhin.js.org` 时,把 [`docs/public/login-assist/tcaptcha.html`](https://github.com/zhinjs/zhin/blob/main/docs/public/login-assist/tcaptcha.html) 复制到你自己的 https 域名(单文件,不依赖其他资源;该域名没有 `/logo.svg` 时 logo 自动隐藏),再把 `console.loginAssist.captchaHelperUrl` 设为它的地址:
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
console:
|
|
234
|
+
loginAssist:
|
|
235
|
+
captchaHelperUrl: https://captcha.example.com/tcaptcha.html
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
不要把它放在 Console 的同一个源下。页面自带严格的 CSP(内联脚本按 sha256 放行);如果改动了内联脚本,要同步更新 CSP 里的哈希(`pnpm vitest run tests/docs/login-assist-captcha-page.test.ts` 会给出新值)。托管方不能改写 HTML(压缩、注入脚本会让哈希失效),并且应当用响应头禁止嵌入:参考仓库里 `docs/public/_headers` 的 `/login-assist/*` 规则(`Cache-Control: no-transform`、`Content-Security-Policy: frame-ancestors 'none'`、`Referrer-Policy: no-referrer`)。
|
|
239
|
+
|
|
192
240
|
## 掉线通知与重新登录
|
|
193
241
|
|
|
194
242
|
配置 `notify` 后,ICQQ 被踢下线、登录失败或启动失败时,会通过另一个适配器实例给 master 发通知。例如用 QQ 官方机器人实例通知:
|
|
@@ -206,15 +254,17 @@ icqq:
|
|
|
206
254
|
id: "${QQ_MASTER_OPENID}" # master 在 QQ 官方机器人里的 openid
|
|
207
255
|
```
|
|
208
256
|
|
|
209
|
-
`autoReconnect: true`
|
|
257
|
+
`autoReconnect: true` 时,被踢下线会自动重试登录;出现二维码、滑块、设备锁或身份验证时,通知会提示下一步。收到通知后可以直接在 QQ 里完成:
|
|
210
258
|
|
|
211
259
|
| 命令 | 作用 |
|
|
212
260
|
|------|------|
|
|
213
261
|
| `icqq login qr` | 把 ICQQ 登录二维码发到当前会话(QQ 支持图片时) |
|
|
214
|
-
| `icqq login confirm` |
|
|
215
|
-
| `icqq login code <内容>` | 提交滑块 ticket
|
|
216
|
-
| `icqq login status` |
|
|
217
|
-
| `icqq login retry` |
|
|
262
|
+
| `icqq login confirm` | 扫码、设备锁或身份验证完成后继续登录 |
|
|
263
|
+
| `icqq login code [QQ号] <内容>` | 提交滑块 ticket(可粘贴整段响应)或 6 位短信验证码;设备锁回复 `sms` 改用短信,短信挑战回复 `resend` 重新发送 |
|
|
264
|
+
| `icqq login status` | 查看在线状态、当前挑战的步骤和链接、上次失败原因 |
|
|
265
|
+
| `icqq login retry` | 重新登录(新的登录流程) |
|
|
266
|
+
|
|
267
|
+
**设备锁**:`icqq login status` 会给出验证链接。用手机 QQ 完成验证后回复 `icqq login confirm`;无法用链接验证时回复 `icqq login code sms`,收到短信后回复 `icqq login code <验证码>`。不回复 `sms` 不会发短信。身份验证需要复制设备信息,请在 Console 完成。等待验证期间不会自动重新登录(否则链接会失效);5 分钟没有处理,挑战过期,等你重新登录。
|
|
218
268
|
|
|
219
269
|
命令只接受 master(顶层或 `endpoints[i].master`)或 `notify.id` 对应账号;两者都没配置时请用 Console 的 ICQQ 页面。没有配置 `notify` 时,掉线只写日志并在 Console 显示登录待办。
|
|
220
270
|
|
|
@@ -222,7 +272,7 @@ icqq:
|
|
|
222
272
|
|
|
223
273
|
| 现象 | 排查 |
|
|
224
274
|
| --- | --- |
|
|
225
|
-
| 一直停在登录中 | 打开 Console
|
|
275
|
+
| 一直停在登录中 | 打开 Console 登录待办,完成二维码、滑块、设备锁或身份验证;流程停在「失败 / 已过期」时点「重新登录」 |
|
|
226
276
|
| 签名失败 | 检查 `signApiAddr`;本地模式确认 `@icqqjs/qqsign` 已安装且版本匹配 |
|
|
227
277
|
| 重启后重复登录 | 持久化设备与会话数据目录,避免每次生成新设备 |
|
|
228
278
|
| 请求或通知未显示 | 在账号页「聊天」页签检查好友申请与邀请、通知;ICQQ 优先读取平台请求列表 |
|
package/adapters/icqq/index.js
CHANGED
|
@@ -19,6 +19,9 @@ const ICQQ_REACTIONS = [
|
|
|
19
19
|
const ICQQ_MUTE_SECONDS = { min: 60, max: 2592000 };
|
|
20
20
|
export default defineAdapter({
|
|
21
21
|
capabilities: ['inbound', 'outbound'],
|
|
22
|
+
// ADR 0058: a person answers QR / slider / device-lock / SMS / identity challenges; the Endpoint
|
|
23
|
+
// implements `login.relogin` and `login.refreshChallenge` (a new QR code in place).
|
|
24
|
+
login: { mode: 'interactive', challenges: ['qrcode', 'slider', 'device', 'auth', 'sms'], relogin: true, refreshQr: true },
|
|
22
25
|
profile: {
|
|
23
26
|
default: {
|
|
24
27
|
messaging: {
|
package/adapters/icqq/index.ts
CHANGED
|
@@ -24,6 +24,9 @@ const ICQQ_MUTE_SECONDS = { min: 60, max: 2592000 } as const;
|
|
|
24
24
|
|
|
25
25
|
export default defineAdapter<IcqqEndpointConfig>({
|
|
26
26
|
capabilities: ['inbound', 'outbound'],
|
|
27
|
+
// ADR 0058: a person answers QR / slider / device-lock / SMS / identity challenges; the Endpoint
|
|
28
|
+
// implements `login.relogin` and `login.refreshChallenge` (a new QR code in place).
|
|
29
|
+
login: { mode: 'interactive', challenges: ['qrcode', 'slider', 'device', 'auth', 'sms'], relogin: true, refreshQr: true },
|
|
27
30
|
profile: {
|
|
28
31
|
default: {
|
|
29
32
|
messaging: {
|
|
@@ -1,28 +1,53 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { defineCommand } from 'zhin.js/command';
|
|
3
|
-
import { icqqLoginControls, icqqLoginForbidden, isIcqqLoginOperator, pendingTaskLabel,
|
|
3
|
+
import { activeIcqqChallenge, challengeCommandHint, icqqLoginControls, icqqLoginForbidden, isIcqqLoginOperator, pendingTaskLabel, pickIcqqCodeTarget, submitIcqqAnswer, } from '../definition.js';
|
|
4
4
|
export default defineCommand({
|
|
5
|
-
description: '提交 ICQQ
|
|
6
|
-
descriptionI18n: { en: 'Submit an ICQQ slider
|
|
5
|
+
description: '提交 ICQQ 滑块 ticket、短信验证码,或选择设备锁的验证方式',
|
|
6
|
+
descriptionI18n: { en: 'Submit an ICQQ slider ticket or SMS code, or choose how to pass the device lock' },
|
|
7
7
|
async execute({ args, config, input, project, owner }) {
|
|
8
8
|
if (!isIcqqLoginOperator(config, input))
|
|
9
9
|
return icqqLoginForbidden();
|
|
10
10
|
const controls = icqqLoginControls({ project, owner });
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
11
|
+
const target = pickIcqqCodeTarget(controls, args);
|
|
12
|
+
if (!target.control)
|
|
13
|
+
return target.text;
|
|
14
|
+
const { value } = target;
|
|
15
|
+
const control = target.control;
|
|
17
16
|
if (!value) {
|
|
18
17
|
return '请把验证内容放在命令后面,例如 `icqq login code 123456` 或 `icqq login code <ticket>`。';
|
|
19
18
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const active = activeIcqqChallenge(control);
|
|
20
|
+
if (!active || active.challenge.kind === 'qrcode' || active.challenge.kind === 'auth') {
|
|
21
|
+
return active
|
|
22
|
+
? challengeCommandHint(active.challenge.kind, control.endpointId)
|
|
23
|
+
: '当前没有滑块/短信/设备锁验证;发送 `icqq login status` 查看登录状态。';
|
|
24
|
+
}
|
|
25
|
+
const { kind } = active.challenge;
|
|
26
|
+
const word = value.toLowerCase();
|
|
27
|
+
let answer = value;
|
|
28
|
+
if (kind === 'device') {
|
|
29
|
+
if (word === 'sms')
|
|
30
|
+
answer = { action: 'sms', values: {} };
|
|
31
|
+
else if (word === 'ok' || word === 'continue')
|
|
32
|
+
answer = { action: 'continue', values: {} };
|
|
33
|
+
else
|
|
34
|
+
return challengeCommandHint('device', control.endpointId);
|
|
35
|
+
if (!active.challenge.actions.some((action) => action.id === answer.action)) {
|
|
36
|
+
return '这个设备锁不支持短信验证码,请用手机完成验证后回复 `icqq login confirm`。';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (kind === 'sms' && word === 'resend') {
|
|
40
|
+
answer = { action: 'resend', values: {} };
|
|
41
|
+
}
|
|
42
|
+
const result = submitIcqqAnswer(control, active.taskId, answer);
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
return result.error ? `提交未通过:${result.error}` : '登录验证已失效,请发送 `icqq login status` 查看最新状态。';
|
|
45
|
+
if (kind === 'device') {
|
|
46
|
+
return word === 'sms' ? `已请求向 ICQQ ${control.endpointId} 的密保手机发送短信验证码,收到后回复 \`icqq login code <验证码>\`。`
|
|
47
|
+
: `已确认 ICQQ ${control.endpointId} 的设备锁验证,正在登录。`;
|
|
48
|
+
}
|
|
49
|
+
if (kind === 'sms' && word === 'resend')
|
|
50
|
+
return `已请求重新发送 ICQQ ${control.endpointId} 的短信验证码。`;
|
|
51
|
+
return `已提交 ICQQ ${control.endpointId} 的${pendingTaskLabel(kind)}内容,正在验证。`;
|
|
27
52
|
},
|
|
28
53
|
});
|
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
import { defineCommand } from 'zhin.js/command';
|
|
2
2
|
import {
|
|
3
|
+
activeIcqqChallenge,
|
|
4
|
+
challengeCommandHint,
|
|
3
5
|
icqqLoginControls,
|
|
4
6
|
icqqLoginForbidden,
|
|
5
7
|
isIcqqLoginOperator,
|
|
6
8
|
pendingTaskLabel,
|
|
7
|
-
|
|
9
|
+
pickIcqqCodeTarget,
|
|
10
|
+
submitIcqqAnswer,
|
|
8
11
|
} from '../definition.js';
|
|
9
12
|
|
|
10
13
|
export default defineCommand({
|
|
11
|
-
description: '提交 ICQQ
|
|
12
|
-
descriptionI18n: { en: 'Submit an ICQQ slider
|
|
14
|
+
description: '提交 ICQQ 滑块 ticket、短信验证码,或选择设备锁的验证方式',
|
|
15
|
+
descriptionI18n: { en: 'Submit an ICQQ slider ticket or SMS code, or choose how to pass the device lock' },
|
|
13
16
|
async execute({ args, config, input, project, owner }) {
|
|
14
17
|
if (!isIcqqLoginOperator(config, input)) return icqqLoginForbidden();
|
|
15
18
|
const controls = icqqLoginControls({ project, owner });
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const value =
|
|
19
|
-
const
|
|
20
|
-
if (!picked.control) return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
19
|
+
const target = pickIcqqCodeTarget(controls, args);
|
|
20
|
+
if (!target.control) return target.text;
|
|
21
|
+
const { value } = target;
|
|
22
|
+
const control = target.control;
|
|
21
23
|
if (!value) {
|
|
22
24
|
return '请把验证内容放在命令后面,例如 `icqq login code 123456` 或 `icqq login code <ticket>`。';
|
|
23
25
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const active = activeIcqqChallenge(control);
|
|
27
|
+
if (!active || active.challenge.kind === 'qrcode' || active.challenge.kind === 'auth') {
|
|
28
|
+
return active
|
|
29
|
+
? challengeCommandHint(active.challenge.kind, control.endpointId)
|
|
30
|
+
: '当前没有滑块/短信/设备锁验证;发送 `icqq login status` 查看登录状态。';
|
|
31
|
+
}
|
|
32
|
+
const { kind } = active.challenge;
|
|
33
|
+
const word = value.toLowerCase();
|
|
34
|
+
let answer: string | Record<string, unknown> = value;
|
|
35
|
+
if (kind === 'device') {
|
|
36
|
+
if (word === 'sms') answer = { action: 'sms', values: {} };
|
|
37
|
+
else if (word === 'ok' || word === 'continue') answer = { action: 'continue', values: {} };
|
|
38
|
+
else return challengeCommandHint('device', control.endpointId);
|
|
39
|
+
if (!active.challenge.actions.some((action) => action.id === (answer as { action: string }).action)) {
|
|
40
|
+
return '这个设备锁不支持短信验证码,请用手机完成验证后回复 `icqq login confirm`。';
|
|
41
|
+
}
|
|
42
|
+
} else if (kind === 'sms' && word === 'resend') {
|
|
43
|
+
answer = { action: 'resend', values: {} };
|
|
44
|
+
}
|
|
45
|
+
const result = submitIcqqAnswer(control, active.taskId, answer);
|
|
46
|
+
if (!result.ok) return result.error ? `提交未通过:${result.error}` : '登录验证已失效,请发送 `icqq login status` 查看最新状态。';
|
|
47
|
+
if (kind === 'device') {
|
|
48
|
+
return word === 'sms' ? `已请求向 ICQQ ${control.endpointId} 的密保手机发送短信验证码,收到后回复 \`icqq login code <验证码>\`。`
|
|
49
|
+
: `已确认 ICQQ ${control.endpointId} 的设备锁验证,正在登录。`;
|
|
50
|
+
}
|
|
51
|
+
if (kind === 'sms' && word === 'resend') return `已请求重新发送 ICQQ ${control.endpointId} 的短信验证码。`;
|
|
52
|
+
return `已提交 ICQQ ${control.endpointId} 的${pendingTaskLabel(kind)}内容,正在验证。`;
|
|
29
53
|
},
|
|
30
54
|
});
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { defineCommand } from 'zhin.js/command';
|
|
3
|
-
import { icqqLoginControls, icqqLoginForbidden, isIcqqLoginOperator, pickIcqqLoginControl, } from '../definition.js';
|
|
3
|
+
import { activeIcqqChallenge, challengeCommandHint, icqqLoginControls, icqqLoginForbidden, isIcqqLoginOperator, pendingTaskLabel, pickIcqqLoginControl, submitIcqqAnswer, } from '../definition.js';
|
|
4
4
|
export default defineCommand({
|
|
5
|
-
description: '确认已完成 ICQQ
|
|
6
|
-
descriptionI18n: { en: 'Confirm the ICQQ QR
|
|
5
|
+
description: '确认已完成 ICQQ 扫码、设备锁或身份验证',
|
|
6
|
+
descriptionI18n: { en: 'Confirm the ICQQ QR scan, device lock or identity verification is done' },
|
|
7
7
|
async execute({ args, config, input, project, owner }) {
|
|
8
8
|
if (!isIcqqLoginOperator(config, input))
|
|
9
9
|
return icqqLoginForbidden();
|
|
10
10
|
const picked = pickIcqqLoginControl(icqqLoginControls({ project, owner }), args[0]);
|
|
11
11
|
if (!picked.control)
|
|
12
12
|
return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const control = picked.control;
|
|
14
|
+
const active = activeIcqqChallenge(control);
|
|
15
|
+
if (!active)
|
|
16
|
+
return '当前没有等待确认的登录验证;发送 `icqq login qr` 获取二维码。';
|
|
17
|
+
const { kind } = active.challenge;
|
|
18
|
+
if (kind !== 'qrcode' && kind !== 'device' && kind !== 'auth')
|
|
19
|
+
return challengeCommandHint(kind, control.endpointId);
|
|
20
|
+
// A QR challenge takes the legacy `ok`; device lock / identity verification answer `continue`.
|
|
21
|
+
const result = submitIcqqAnswer(control, active.taskId, kind === 'qrcode' ? 'ok' : { action: 'continue', values: {} });
|
|
22
|
+
if (!result.ok)
|
|
23
|
+
return result.error ? `提交未通过:${result.error}` : '登录验证已失效,请重新发送 `icqq login qr`。';
|
|
24
|
+
return `已提交 ICQQ ${control.endpointId} 的${pendingTaskLabel(kind)}确认,正在登录。`;
|
|
19
25
|
},
|
|
20
26
|
});
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { defineCommand } from 'zhin.js/command';
|
|
2
2
|
import {
|
|
3
|
+
activeIcqqChallenge,
|
|
4
|
+
challengeCommandHint,
|
|
3
5
|
icqqLoginControls,
|
|
4
6
|
icqqLoginForbidden,
|
|
5
7
|
isIcqqLoginOperator,
|
|
8
|
+
pendingTaskLabel,
|
|
6
9
|
pickIcqqLoginControl,
|
|
10
|
+
submitIcqqAnswer,
|
|
7
11
|
} from '../definition.js';
|
|
8
12
|
|
|
9
13
|
export default defineCommand({
|
|
10
|
-
description: '确认已完成 ICQQ
|
|
11
|
-
descriptionI18n: { en: 'Confirm the ICQQ QR
|
|
14
|
+
description: '确认已完成 ICQQ 扫码、设备锁或身份验证',
|
|
15
|
+
descriptionI18n: { en: 'Confirm the ICQQ QR scan, device lock or identity verification is done' },
|
|
12
16
|
async execute({ args, config, input, project, owner }) {
|
|
13
17
|
if (!isIcqqLoginOperator(config, input)) return icqqLoginForbidden();
|
|
14
18
|
const picked = pickIcqqLoginControl(icqqLoginControls({ project, owner }), args[0]);
|
|
15
19
|
if (!picked.control) return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (!
|
|
19
|
-
|
|
20
|
+
const control = picked.control;
|
|
21
|
+
const active = activeIcqqChallenge(control);
|
|
22
|
+
if (!active) return '当前没有等待确认的登录验证;发送 `icqq login qr` 获取二维码。';
|
|
23
|
+
const { kind } = active.challenge;
|
|
24
|
+
if (kind !== 'qrcode' && kind !== 'device' && kind !== 'auth') return challengeCommandHint(kind, control.endpointId);
|
|
25
|
+
// A QR challenge takes the legacy `ok`; device lock / identity verification answer `continue`.
|
|
26
|
+
const result = submitIcqqAnswer(control, active.taskId, kind === 'qrcode' ? 'ok' : { action: 'continue', values: {} });
|
|
27
|
+
if (!result.ok) return result.error ? `提交未通过:${result.error}` : '登录验证已失效,请重新发送 `icqq login qr`。';
|
|
28
|
+
return `已提交 ICQQ ${control.endpointId} 的${pendingTaskLabel(kind)}确认,正在登录。`;
|
|
20
29
|
},
|
|
21
30
|
});
|
|
@@ -7,8 +7,36 @@
|
|
|
7
7
|
* owner can fetch the QR image and confirm/send verification codes without the
|
|
8
8
|
* Console or terminal.
|
|
9
9
|
*/
|
|
10
|
+
import { LoginSubmissionError } from '@zhin.js/core';
|
|
10
11
|
import { icqqLoginControls } from "../../../lib/icqq-login-control.js";
|
|
11
12
|
export { icqqLoginControls };
|
|
13
|
+
/** The challenge this account is waiting on (ADR 0058 login flow), with its task id. */
|
|
14
|
+
export function activeIcqqChallenge(control) {
|
|
15
|
+
const flow = control.flow();
|
|
16
|
+
const taskId = flow?.activeTaskId;
|
|
17
|
+
const challenge = taskId ? flow?.steps.find((step) => step.taskId === taskId)?.challenge : undefined;
|
|
18
|
+
return taskId && challenge ? { taskId, challenge } : undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Links of a challenge (link blocks first, then `media.link`), deduplicated. */
|
|
21
|
+
export function challengeLinks(challenge) {
|
|
22
|
+
const links = (challenge.blocks ?? [])
|
|
23
|
+
.filter((block) => block.kind === 'link')
|
|
24
|
+
.map((block) => ({ label: block.label, url: block.url }));
|
|
25
|
+
if (challenge.media?.link && !links.some((link) => link.url === challenge.media.link.url))
|
|
26
|
+
links.push(challenge.media.link);
|
|
27
|
+
return links;
|
|
28
|
+
}
|
|
29
|
+
/** Submit through the control; an answer that does not fit the challenge returns its reason. */
|
|
30
|
+
export function submitIcqqAnswer(control, taskId, value) {
|
|
31
|
+
try {
|
|
32
|
+
return control.submit(taskId, value) ? { ok: true } : { ok: false };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof LoginSubmissionError)
|
|
36
|
+
return { ok: false, error: error.message };
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
12
40
|
/**
|
|
13
41
|
* Operator = a **global** chat-role `owner` (ADR 0056 §2: the command reaches an ICQQ account from
|
|
14
42
|
* any adapter, so a conversation / account owner elsewhere does not qualify), or the configured
|
|
@@ -53,6 +81,21 @@ export function pickIcqqLoginControl(controls, requested) {
|
|
|
53
81
|
}
|
|
54
82
|
return { text: `有多个 ICQQ endpoint,请指定 QQ 号:${[...controls.keys()].join('、')}` };
|
|
55
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* `icqq login code [uin] <内容>`: the UIN is required with several endpoints, optional with one.
|
|
86
|
+
* The reply never echoes the arguments (the first one may be the code itself).
|
|
87
|
+
*/
|
|
88
|
+
export function pickIcqqCodeTarget(controls, args) {
|
|
89
|
+
const words = args.map((arg) => String(arg));
|
|
90
|
+
const named = words.length > 1 && controls.has(words[0].trim());
|
|
91
|
+
if (controls.size === 0)
|
|
92
|
+
return { text: 'ICQQ 适配器还没有启动' };
|
|
93
|
+
if (controls.size > 1 && !named) {
|
|
94
|
+
return { text: `有多个 ICQQ 账号,请用 \`icqq login code <QQ号> <内容>\`。可选 QQ 号:${[...controls.keys()].join('、')}` };
|
|
95
|
+
}
|
|
96
|
+
const control = named ? controls.get(words[0].trim()) : [...controls.values()][0];
|
|
97
|
+
return { control, value: (named ? words.slice(1) : words).join(' ').trim() };
|
|
98
|
+
}
|
|
56
99
|
export function icqqLoginForbidden() {
|
|
57
100
|
return '仅 owner(聊天角色)或 notify 目标账号可以操作 ICQQ 登录';
|
|
58
101
|
}
|
|
@@ -60,9 +103,20 @@ export function pendingTaskLabel(type) {
|
|
|
60
103
|
switch (type) {
|
|
61
104
|
case 'qrcode': return '二维码';
|
|
62
105
|
case 'slider': return '滑块验证';
|
|
63
|
-
case 'device': return '
|
|
64
|
-
case 'sms': return '
|
|
65
|
-
case 'auth': return '
|
|
106
|
+
case 'device': return '设备锁验证';
|
|
107
|
+
case 'sms': return '短信验证码';
|
|
108
|
+
case 'auth': return '身份验证';
|
|
66
109
|
default: return '登录验证';
|
|
67
110
|
}
|
|
68
111
|
}
|
|
112
|
+
/** Chat command that answers each challenge kind. */
|
|
113
|
+
export function challengeCommandHint(kind, endpointId) {
|
|
114
|
+
switch (kind) {
|
|
115
|
+
case 'qrcode': return `二维码:\`icqq login qr ${endpointId}\`;扫码并在手机上确认后会自动继续,没有继续时回复 \`icqq login confirm ${endpointId}\`。`;
|
|
116
|
+
case 'slider': return `完成滑块后回复 \`icqq login code ${endpointId} <ticket>\`(也可以粘贴整段响应 JSON);Console 的验证码辅助页可以一键完成。`;
|
|
117
|
+
case 'device': return `在手机完成验证后回复 \`icqq login confirm ${endpointId}\`;改用短信验证码回复 \`icqq login code ${endpointId} sms\`。`;
|
|
118
|
+
case 'sms': return `回复 \`icqq login code ${endpointId} <6 位验证码>\`;重新发送回复 \`icqq login code ${endpointId} resend\`。`;
|
|
119
|
+
case 'auth': return `身份验证需要复制设备信息到验证页,请在 Console 的 ICQQ 页面按步骤完成;完成后回复 \`icqq login confirm ${endpointId}\`。`;
|
|
120
|
+
default: return `回复 \`icqq login code ${endpointId} <内容>\`。`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -6,10 +6,39 @@
|
|
|
6
6
|
* owner can fetch the QR image and confirm/send verification codes without the
|
|
7
7
|
* Console or terminal.
|
|
8
8
|
*/
|
|
9
|
+
import { LoginSubmissionError, type LoginChallenge, type LoginChallengeBlock } from '@zhin.js/core';
|
|
9
10
|
import { icqqLoginControls, type IcqqLoginControl } from '../../../src/icqq-login-control.js';
|
|
10
11
|
|
|
11
12
|
export { icqqLoginControls };
|
|
12
13
|
|
|
14
|
+
/** The challenge this account is waiting on (ADR 0058 login flow), with its task id. */
|
|
15
|
+
export function activeIcqqChallenge(control: IcqqLoginControl): { taskId: string; challenge: LoginChallenge } | undefined {
|
|
16
|
+
const flow = control.flow();
|
|
17
|
+
const taskId = flow?.activeTaskId;
|
|
18
|
+
const challenge = taskId ? flow?.steps.find((step) => step.taskId === taskId)?.challenge : undefined;
|
|
19
|
+
return taskId && challenge ? { taskId, challenge } : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Links of a challenge (link blocks first, then `media.link`), deduplicated. */
|
|
23
|
+
export function challengeLinks(challenge: LoginChallenge): { label: string; url: string }[] {
|
|
24
|
+
const links = (challenge.blocks ?? [])
|
|
25
|
+
.filter((block): block is Extract<LoginChallengeBlock, { kind: 'link' }> => block.kind === 'link')
|
|
26
|
+
.map((block) => ({ label: block.label, url: block.url }));
|
|
27
|
+
if (challenge.media?.link && !links.some((link) => link.url === challenge.media!.link!.url)) links.push(challenge.media.link);
|
|
28
|
+
return links;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Submit through the control; an answer that does not fit the challenge returns its reason. */
|
|
32
|
+
export function submitIcqqAnswer(control: IcqqLoginControl, taskId: string, value: string | Record<string, unknown>):
|
|
33
|
+
{ ok: true } | { ok: false; error?: string } {
|
|
34
|
+
try {
|
|
35
|
+
return control.submit(taskId, value) ? { ok: true } : { ok: false };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof LoginSubmissionError) return { ok: false, error: error.message };
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
/**
|
|
14
43
|
* Operator = a **global** chat-role `owner` (ADR 0056 §2: the command reaches an ICQQ account from
|
|
15
44
|
* any adapter, so a conversation / account owner elsewhere does not qualify), or the configured
|
|
@@ -56,6 +85,24 @@ export function pickIcqqLoginControl(
|
|
|
56
85
|
return { text: `有多个 ICQQ endpoint,请指定 QQ 号:${[...controls.keys()].join('、')}` };
|
|
57
86
|
}
|
|
58
87
|
|
|
88
|
+
/**
|
|
89
|
+
* `icqq login code [uin] <内容>`: the UIN is required with several endpoints, optional with one.
|
|
90
|
+
* The reply never echoes the arguments (the first one may be the code itself).
|
|
91
|
+
*/
|
|
92
|
+
export function pickIcqqCodeTarget(
|
|
93
|
+
controls: ReadonlyMap<string, IcqqLoginControl>,
|
|
94
|
+
args: readonly unknown[],
|
|
95
|
+
): { control: IcqqLoginControl; value: string; text?: undefined } | { control?: undefined; text: string } {
|
|
96
|
+
const words = args.map((arg) => String(arg));
|
|
97
|
+
const named = words.length > 1 && controls.has(words[0]!.trim());
|
|
98
|
+
if (controls.size === 0) return { text: 'ICQQ 适配器还没有启动' };
|
|
99
|
+
if (controls.size > 1 && !named) {
|
|
100
|
+
return { text: `有多个 ICQQ 账号,请用 \`icqq login code <QQ号> <内容>\`。可选 QQ 号:${[...controls.keys()].join('、')}` };
|
|
101
|
+
}
|
|
102
|
+
const control = named ? controls.get(words[0]!.trim())! : [...controls.values()][0]!;
|
|
103
|
+
return { control, value: (named ? words.slice(1) : words).join(' ').trim() };
|
|
104
|
+
}
|
|
105
|
+
|
|
59
106
|
export function icqqLoginForbidden(): string {
|
|
60
107
|
return '仅 owner(聊天角色)或 notify 目标账号可以操作 ICQQ 登录';
|
|
61
108
|
}
|
|
@@ -64,9 +111,21 @@ export function pendingTaskLabel(type: string): string {
|
|
|
64
111
|
switch (type) {
|
|
65
112
|
case 'qrcode': return '二维码';
|
|
66
113
|
case 'slider': return '滑块验证';
|
|
67
|
-
case 'device': return '
|
|
68
|
-
case 'sms': return '
|
|
69
|
-
case 'auth': return '
|
|
114
|
+
case 'device': return '设备锁验证';
|
|
115
|
+
case 'sms': return '短信验证码';
|
|
116
|
+
case 'auth': return '身份验证';
|
|
70
117
|
default: return '登录验证';
|
|
71
118
|
}
|
|
72
119
|
}
|
|
120
|
+
|
|
121
|
+
/** Chat command that answers each challenge kind. */
|
|
122
|
+
export function challengeCommandHint(kind: string, endpointId: string): string {
|
|
123
|
+
switch (kind) {
|
|
124
|
+
case 'qrcode': return `二维码:\`icqq login qr ${endpointId}\`;扫码并在手机上确认后会自动继续,没有继续时回复 \`icqq login confirm ${endpointId}\`。`;
|
|
125
|
+
case 'slider': return `完成滑块后回复 \`icqq login code ${endpointId} <ticket>\`(也可以粘贴整段响应 JSON);Console 的验证码辅助页可以一键完成。`;
|
|
126
|
+
case 'device': return `在手机完成验证后回复 \`icqq login confirm ${endpointId}\`;改用短信验证码回复 \`icqq login code ${endpointId} sms\`。`;
|
|
127
|
+
case 'sms': return `回复 \`icqq login code ${endpointId} <6 位验证码>\`;重新发送回复 \`icqq login code ${endpointId} resend\`。`;
|
|
128
|
+
case 'auth': return `身份验证需要复制设备信息到验证页,请在 Console 的 ICQQ 页面按步骤完成;完成后回复 \`icqq login confirm ${endpointId}\`。`;
|
|
129
|
+
default: return `回复 \`icqq login code ${endpointId} <内容>\`。`;
|
|
130
|
+
}
|
|
131
|
+
}
|