@zhin.js/adapter-icqq 1.1.17 → 1.1.18
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 +46 -0
- package/README.md +44 -3
- package/adapters/icqq/index.js +10 -2
- package/adapters/icqq/index.ts +10 -2
- package/commands/icqq/login/code/index.js +28 -0
- package/commands/icqq/login/code/index.ts +30 -0
- package/commands/icqq/login/confirm/index.js +20 -0
- package/commands/icqq/login/confirm/index.ts +21 -0
- package/commands/icqq/login/definition.js +65 -0
- package/commands/icqq/login/definition.ts +72 -0
- package/commands/icqq/login/qr/index.js +53 -0
- package/commands/icqq/login/qr/index.ts +55 -0
- package/commands/icqq/login/retry/index.js +21 -0
- package/commands/icqq/login/retry/index.ts +23 -0
- package/commands/icqq/login/status/index.js +31 -0
- package/commands/icqq/login/status/index.ts +34 -0
- package/lib/console-spec.d.ts +17 -0
- package/lib/console-spec.js +11 -0
- package/lib/console.d.ts +8 -0
- package/lib/console.js +16 -0
- package/lib/endpoint.d.ts +9 -2
- package/lib/endpoint.js +194 -37
- package/lib/icqq-login-control.d.ts +18 -0
- package/lib/icqq-login-control.js +8 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +10 -0
- package/lib/protocol.js +24 -9
- package/package.json +17 -9
- package/pages/index/index.js +15 -0
- package/pages/index/index.ts +16 -0
- package/plugin.js +6 -1
- package/schema.json +174 -0
- package/src/console-spec.ts +17 -0
- package/src/console.ts +21 -0
- package/src/endpoint.ts +193 -42
- package/src/icqq-login-control.ts +30 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +32 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @zhin.js/adapter-icqq
|
|
2
2
|
|
|
3
|
+
## 1.1.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6345a38: feat(adapters): add per-adapter Console pages
|
|
8
|
+
|
|
9
|
+
Every platform adapter now ships a `pages/index` Console page backed by a shared
|
|
10
|
+
`createAdapterConsolePage` component and `createAdapterConsoleRpc` server helper:
|
|
11
|
+
|
|
12
|
+
- See running endpoints, test connectivity, and manage
|
|
13
|
+
`plugins.<adapter>.endpoints` (add/remove writes `.env` references) from the
|
|
14
|
+
Console instead of chat commands.
|
|
15
|
+
- Surface LoginAssist pending tasks (QR / slider / SMS / device) so ICQQ login no
|
|
16
|
+
longer requires the terminal.
|
|
17
|
+
- QQ moves its QR binding flow (`qq endpoint add`) into Console actions with
|
|
18
|
+
public/private selection.
|
|
19
|
+
- Pages are permission-gated by `<adapter>:read`; writes need `<adapter>:write`.
|
|
20
|
+
|
|
21
|
+
- ff3efe7: feat(adapters): notify masters when ICQQ or QQ goes offline and support ICQQ re-login
|
|
22
|
+
|
|
23
|
+
- Add an explicit `notify` target to ICQQ and QQ endpoint config: when the
|
|
24
|
+
transport goes offline or login fails, send a short notice through another
|
|
25
|
+
adapter instance's Endpoint via `OutboundHost`.
|
|
26
|
+
- ICQQ automatically retries login after `system.offline.kickoff` when
|
|
27
|
+
`autoReconnect` is enabled, and exposes pending QR/slider/device challenges.
|
|
28
|
+
- Add `icqq login status|qr|confirm|code|retry` recovery commands. They can be
|
|
29
|
+
invoked from the QQ conversation that received the notice; `qr` returns the
|
|
30
|
+
login QR code as an image when the notifying platform supports it.
|
|
31
|
+
- `notify.id` also counts as the master identity for the related endpoint
|
|
32
|
+
management commands, so recovery can happen from the notified conversation.
|
|
33
|
+
- Document the new config and commands in the ICQQ/QQ READMEs and generated
|
|
34
|
+
configuration reference.
|
|
35
|
+
|
|
36
|
+
- c58c589: Group deferred tools by their canonical plugin or MCP server instead of installation paths. Omit already represented delivered text and ledger facts only from the current model projection, preserving full history and restoring facts when their covering reply leaves the active context. Allow tools to declare a synchronous, read-only availability predicate before turn disclosure, with execution-time checks remaining authoritative.
|
|
37
|
+
|
|
38
|
+
Preserve every confirmed platform message identity across Agent replies and ICQQ split sends. Give each bubble its own stable single-message alias while retaining the reply's ordered association; preserve confirmed prefixes on partial delivery without retrying or inventing unknown message IDs.
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [6345a38]
|
|
41
|
+
- Updated dependencies [ff3efe7]
|
|
42
|
+
- Updated dependencies [c58c589]
|
|
43
|
+
- @zhin.js/adapter@1.1.25
|
|
44
|
+
- @zhin.js/client@1.1.21
|
|
45
|
+
- @zhin.js/core@1.1.53
|
|
46
|
+
- @zhin.js/im-contract@1.1.9
|
|
47
|
+
- @zhin.js/tool@1.1.12
|
|
48
|
+
|
|
3
49
|
## 1.1.17
|
|
4
50
|
|
|
5
51
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -173,6 +173,47 @@ ICQQ Agent 可按需加载 `icqq-conversation` 查询当前群/频道和 Bot
|
|
|
173
173
|
|
|
174
174
|
`icqq-group-admin` 新增撤回他人消息与退群:前者保留群管发起权限、实时平台角色检查,无需逐次审批;退群仅主人可发起、每次审批,Bot 为群主时拒绝执行。引用/转发走统一发送链路,不支持跨会话转发。当前 guild-channel 接入仅提供身份查询,不能使用这些群/私聊操作。详见 [权限与能力说明](./PERMITS.md)。
|
|
175
175
|
|
|
176
|
+
## Console 页面
|
|
177
|
+
|
|
178
|
+
启动后打开 Console 的「ICQQ」页(地址 `/icqq`),可以:
|
|
179
|
+
|
|
180
|
+
- 查看运行中的连接、点「测试连接」做连通性诊断;
|
|
181
|
+
- 增删 `plugins.icqq.endpoints`(凭据写入 `.env`,配置里只保留 `${ENV}` 引用);
|
|
182
|
+
- 处理登录待办(扫码 / 滑块 / 设备确认等,来自 LoginAssist);
|
|
183
|
+
|
|
184
|
+
ICQQ 首次登录的二维码、滑块、设备确认会出现在「登录待办」里,不需要再回到终端确认。
|
|
185
|
+
|
|
186
|
+
读取需要 `icqq:read`,写操作需要 `icqq:write`;管理员 full token 不受限。页面里的「添加 endpoint」等价于原来的 `icqq endpoint add`。
|
|
187
|
+
|
|
188
|
+
## 掉线通知与重新登录
|
|
189
|
+
|
|
190
|
+
配置 `notify` 后,ICQQ 被踢下线、登录失败或启动失败时,会通过另一个适配器实例给 master 发通知。例如用 QQ 官方机器人实例通知:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
qq:
|
|
194
|
+
# ... 已配置好的 QQ 官方机器人
|
|
195
|
+
icqq:
|
|
196
|
+
master: 123456
|
|
197
|
+
autoReconnect: true # 默认 true:被踢下线后自动重新登录
|
|
198
|
+
notify:
|
|
199
|
+
adapter: qq
|
|
200
|
+
endpoint: bot-1
|
|
201
|
+
kind: private
|
|
202
|
+
id: "${QQ_MASTER_OPENID}" # master 在 QQ 官方机器人里的 openid
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`autoReconnect: true` 时,被踢下线会自动重试登录;二维码/滑块/设备确认就绪后,通知会提示下一步。收到通知后可以直接在 QQ 里完成:
|
|
206
|
+
|
|
207
|
+
| 命令 | 作用 |
|
|
208
|
+
|------|------|
|
|
209
|
+
| `icqq login qr` | 把 ICQQ 登录二维码发到当前会话(QQ 支持图片时) |
|
|
210
|
+
| `icqq login confirm` | 扫码后确认继续登录 |
|
|
211
|
+
| `icqq login code <内容>` | 提交滑块 ticket、短信验证码或设备确认内容 |
|
|
212
|
+
| `icqq login status` | 查看在线状态与登录待办 |
|
|
213
|
+
| `icqq login retry` | 强制重新触发登录 |
|
|
214
|
+
|
|
215
|
+
命令只接受 master(顶层或 `endpoints[i].master`)或 `notify.id` 对应账号;两者都没配置时请用 Console 的 ICQQ 页面。没有配置 `notify` 时,掉线只写日志并在 Console 显示登录待办。
|
|
216
|
+
|
|
176
217
|
## 故障排查
|
|
177
218
|
|
|
178
219
|
| 现象 | 排查 |
|
|
@@ -190,14 +231,14 @@ MIT
|
|
|
190
231
|
|
|
191
232
|
发送成功回执只接受平台真实消息 ID。网络超时、断线、缺少消息 ID 的响应会报告 `deliveryUnknown`,框架不会自动重发;手动重发可能重复发送。
|
|
192
233
|
|
|
193
|
-
ICQQ 原生分享和文件 API 可能已经成功,但只返回空值或文件 ID;这类结果不能当作可引用、可撤回的消息 ID
|
|
234
|
+
ICQQ 原生分享和文件 API 可能已经成功,但只返回空值或文件 ID;这类结果不能当作可引用、可撤回的消息 ID。适配器不会根据时间和卡片类型从历史猜测本次分享的 ID;同一时刻的其他卡片也可能匹配。没有直接消息 ID 的部分保留为未知,平台操作仍只执行一次。文件撤回应使用平台专用文件 API。
|
|
194
235
|
|
|
195
236
|
### 独立元素按顺序拆分发送
|
|
196
237
|
|
|
197
238
|
ICQQ 要求分享、文件、语音、视频、JSON 卡片和合并转发单独成条。一条消息里同时有文字和这些元素时(例如 Agent 回复“找到了:”加一张音乐分享卡片),适配器按原顺序拆成多条依次发送,引用只附在第一条普通消息上。
|
|
198
239
|
|
|
199
|
-
-
|
|
200
|
-
- 第一条就失败时原样报告失败;后面的部分失败时报告 `partial_delivery
|
|
240
|
+
- 回执按发送顺序保留每部分的真实 ID 和原始段位置;没有 ID 的卡片或文件保留未知槽位,不把完整回复正文复制给每个 ID。所有部分都没有 ID(如单独一张分享)时,仍报告投递结果未知。
|
|
241
|
+
- 第一条就失败时原样报告失败;后面的部分失败时报告 `partial_delivery`(结果未知),保留已完成部分的回执。前面的调用可能已被平台接收,重发可能重复。
|
|
201
242
|
|
|
202
243
|
## 语言
|
|
203
244
|
|
package/adapters/icqq/index.js
CHANGED
|
@@ -3,10 +3,12 @@ import { projectLocaleToken } from 'zhin.js/i18n';
|
|
|
3
3
|
/**
|
|
4
4
|
* Convention entry: discover `adapters/icqq/index.ts` → defineAdapter.
|
|
5
5
|
*/
|
|
6
|
-
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
+
import { createAdapterOutageNotifier, defineAdapter } from 'zhin.js/adapter';
|
|
7
7
|
import { loginAssistToken } from '@zhin.js/core/runtime';
|
|
8
|
+
import { outboundHostToken } from 'zhin.js';
|
|
8
9
|
import { IcqqEndpoint } from "../../lib/endpoint.js";
|
|
9
10
|
import { icqqRuntimeStateToken } from "../../lib/icqq-runtime-state.js";
|
|
11
|
+
import { icqqLoginControlToken } from "../../lib/icqq-login-control.js";
|
|
10
12
|
import { resolveIcqqConfig, } from "../../lib/protocol.js";
|
|
11
13
|
export { IcqqEndpoint } from "../../lib/endpoint.js";
|
|
12
14
|
export default defineAdapter({
|
|
@@ -44,11 +46,17 @@ export default defineAdapter({
|
|
|
44
46
|
id: config.id,
|
|
45
47
|
mode: 'direct',
|
|
46
48
|
});
|
|
47
|
-
|
|
49
|
+
const outbound = context.has?.(outboundHostToken) ? context.use(outboundHostToken) : undefined;
|
|
50
|
+
const endpoint = new IcqqEndpoint({
|
|
48
51
|
id: context.id,
|
|
49
52
|
loginAssist: context.use(loginAssistToken),
|
|
50
53
|
locale: context.use(projectLocaleToken),
|
|
51
54
|
config,
|
|
55
|
+
notifier: createAdapterOutageNotifier(outbound, config.notify),
|
|
52
56
|
});
|
|
57
|
+
if (context.has?.(icqqLoginControlToken)) {
|
|
58
|
+
context.use(icqqLoginControlToken).set(config.id, endpoint.loginControl);
|
|
59
|
+
}
|
|
60
|
+
return endpoint;
|
|
53
61
|
},
|
|
54
62
|
});
|
package/adapters/icqq/index.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { projectLocaleToken } from 'zhin.js/i18n';
|
|
|
2
2
|
/**
|
|
3
3
|
* Convention entry: discover `adapters/icqq/index.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
|
-
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
+
import { createAdapterOutageNotifier, defineAdapter } from 'zhin.js/adapter';
|
|
6
6
|
import { loginAssistToken } from '@zhin.js/core/runtime';
|
|
7
|
+
import { outboundHostToken } from 'zhin.js';
|
|
7
8
|
import { IcqqEndpoint } from '../../src/endpoint.js';
|
|
8
9
|
import { icqqRuntimeStateToken } from '../../src/icqq-runtime-state.js';
|
|
10
|
+
import { icqqLoginControlToken } from '../../src/icqq-login-control.js';
|
|
9
11
|
import {
|
|
10
12
|
resolveIcqqConfig,
|
|
11
13
|
type IcqqEndpointConfig,
|
|
@@ -55,11 +57,17 @@ export default defineAdapter<IcqqEndpointConfig>({
|
|
|
55
57
|
id: config.id,
|
|
56
58
|
mode: 'direct',
|
|
57
59
|
});
|
|
58
|
-
|
|
60
|
+
const outbound = context.has?.(outboundHostToken) ? context.use(outboundHostToken) : undefined;
|
|
61
|
+
const endpoint = new IcqqEndpoint({
|
|
59
62
|
id: context.id,
|
|
60
63
|
loginAssist: context.use(loginAssistToken),
|
|
61
64
|
locale: context.use(projectLocaleToken),
|
|
62
65
|
config,
|
|
66
|
+
notifier: createAdapterOutageNotifier(outbound, config.notify),
|
|
63
67
|
});
|
|
68
|
+
if (context.has?.(icqqLoginControlToken)) {
|
|
69
|
+
context.use(icqqLoginControlToken).set(config.id, endpoint.loginControl);
|
|
70
|
+
}
|
|
71
|
+
return endpoint;
|
|
64
72
|
},
|
|
65
73
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { icqqLoginControlToken, icqqLoginForbidden, isIcqqLoginOperator, pendingTaskLabel, pickIcqqLoginControl, } from '../definition.js';
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
description: '提交 ICQQ 滑块/短信/设备验证内容',
|
|
6
|
+
descriptionI18n: { en: 'Submit an ICQQ slider, SMS or device verification value' },
|
|
7
|
+
async execute({ args, config, input, use }) {
|
|
8
|
+
if (!isIcqqLoginOperator(config, input))
|
|
9
|
+
return icqqLoginForbidden();
|
|
10
|
+
const controls = use(icqqLoginControlToken);
|
|
11
|
+
// One endpoint: the whole argument is the code. Multiple: first arg selects the UIN.
|
|
12
|
+
const requested = controls.size > 1 ? args[0] : undefined;
|
|
13
|
+
const value = (controls.size > 1 ? args.slice(1) : args).join(' ').trim();
|
|
14
|
+
const picked = pickIcqqLoginControl(controls, requested);
|
|
15
|
+
if (!picked.control)
|
|
16
|
+
return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
17
|
+
if (!value) {
|
|
18
|
+
return '请把验证内容放在命令后面,例如 `icqq login code 123456` 或 `icqq login code <ticket>`。';
|
|
19
|
+
}
|
|
20
|
+
const task = picked.control.pending()
|
|
21
|
+
.find((item) => item.type === 'slider' || item.type === 'device' || item.type === 'sms' || item.type === 'other');
|
|
22
|
+
if (!task)
|
|
23
|
+
return '当前没有滑块/短信/设备验证待办;发送 `icqq login qr` 查看二维码。';
|
|
24
|
+
if (!picked.control.submit(task.id, value))
|
|
25
|
+
return '登录待办已失效,请重新触发 `icqq login qr`。';
|
|
26
|
+
return `已提交 ICQQ ${task.endpointKey} 的${pendingTaskLabel(task.type)}内容,正在验证。`;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineCommand } from 'zhin.js/command';
|
|
2
|
+
import {
|
|
3
|
+
icqqLoginControlToken,
|
|
4
|
+
icqqLoginForbidden,
|
|
5
|
+
isIcqqLoginOperator,
|
|
6
|
+
pendingTaskLabel,
|
|
7
|
+
pickIcqqLoginControl,
|
|
8
|
+
} from '../definition.js';
|
|
9
|
+
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
description: '提交 ICQQ 滑块/短信/设备验证内容',
|
|
12
|
+
descriptionI18n: { en: 'Submit an ICQQ slider, SMS or device verification value' },
|
|
13
|
+
async execute({ args, config, input, use }) {
|
|
14
|
+
if (!isIcqqLoginOperator(config, input)) return icqqLoginForbidden();
|
|
15
|
+
const controls = use(icqqLoginControlToken);
|
|
16
|
+
// One endpoint: the whole argument is the code. Multiple: first arg selects the UIN.
|
|
17
|
+
const requested = controls.size > 1 ? args[0] : undefined;
|
|
18
|
+
const value = (controls.size > 1 ? args.slice(1) : args).join(' ').trim();
|
|
19
|
+
const picked = pickIcqqLoginControl(controls, requested);
|
|
20
|
+
if (!picked.control) return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
21
|
+
if (!value) {
|
|
22
|
+
return '请把验证内容放在命令后面,例如 `icqq login code 123456` 或 `icqq login code <ticket>`。';
|
|
23
|
+
}
|
|
24
|
+
const task = picked.control.pending()
|
|
25
|
+
.find((item) => item.type === 'slider' || item.type === 'device' || item.type === 'sms' || item.type === 'other');
|
|
26
|
+
if (!task) return '当前没有滑块/短信/设备验证待办;发送 `icqq login qr` 查看二维码。';
|
|
27
|
+
if (!picked.control.submit(task.id, value)) return '登录待办已失效,请重新触发 `icqq login qr`。';
|
|
28
|
+
return `已提交 ICQQ ${task.endpointKey} 的${pendingTaskLabel(task.type)}内容,正在验证。`;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { icqqLoginControlToken, icqqLoginForbidden, isIcqqLoginOperator, pickIcqqLoginControl, } from '../definition.js';
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
description: '确认已完成 ICQQ 扫码登录',
|
|
6
|
+
descriptionI18n: { en: 'Confirm the ICQQ QR login was scanned' },
|
|
7
|
+
async execute({ args, config, input, use }) {
|
|
8
|
+
if (!isIcqqLoginOperator(config, input))
|
|
9
|
+
return icqqLoginForbidden();
|
|
10
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
11
|
+
if (!picked.control)
|
|
12
|
+
return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
13
|
+
const task = picked.control.pending().find((item) => item.type === 'qrcode' || item.type === 'auth');
|
|
14
|
+
if (!task)
|
|
15
|
+
return '当前没有等待确认的扫码登录;发送 `icqq login qr` 获取二维码。';
|
|
16
|
+
if (!picked.control.submit(task.id, 'ok'))
|
|
17
|
+
return '登录待办已失效,请重新发送 `icqq login qr`。';
|
|
18
|
+
return `已提交 ICQQ ${task.endpointKey} 的扫码确认,正在登录。`;
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineCommand } from 'zhin.js/command';
|
|
2
|
+
import {
|
|
3
|
+
icqqLoginControlToken,
|
|
4
|
+
icqqLoginForbidden,
|
|
5
|
+
isIcqqLoginOperator,
|
|
6
|
+
pickIcqqLoginControl,
|
|
7
|
+
} from '../definition.js';
|
|
8
|
+
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: '确认已完成 ICQQ 扫码登录',
|
|
11
|
+
descriptionI18n: { en: 'Confirm the ICQQ QR login was scanned' },
|
|
12
|
+
async execute({ args, config, input, use }) {
|
|
13
|
+
if (!isIcqqLoginOperator(config, input)) return icqqLoginForbidden();
|
|
14
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
15
|
+
if (!picked.control) return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
16
|
+
const task = picked.control.pending().find((item) => item.type === 'qrcode' || item.type === 'auth');
|
|
17
|
+
if (!task) return '当前没有等待确认的扫码登录;发送 `icqq login qr` 获取二维码。';
|
|
18
|
+
if (!picked.control.submit(task.id, 'ok')) return '登录待办已失效,请重新发送 `icqq login qr`。';
|
|
19
|
+
return `已提交 ICQQ ${task.endpointKey} 的扫码确认,正在登录。`;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* `icqq login` recovery commands.
|
|
4
|
+
*
|
|
5
|
+
* They are registered by the ICQQ plugin but can be invoked from any connected
|
|
6
|
+
* adapter (typically the QQ instance that delivered the outage notice), so the
|
|
7
|
+
* master can fetch the QR image and confirm/send verification codes without the
|
|
8
|
+
* Console or terminal.
|
|
9
|
+
*/
|
|
10
|
+
import { icqqLoginControlToken } from "../../../lib/icqq-login-control.js";
|
|
11
|
+
export { icqqLoginControlToken };
|
|
12
|
+
/** Master = framework master(s), or the configured outage-notify conversation id(s). */
|
|
13
|
+
export function isIcqqLoginOperator(config, input) {
|
|
14
|
+
const cfg = (config ?? {});
|
|
15
|
+
const masters = new Set();
|
|
16
|
+
const collect = (value) => {
|
|
17
|
+
if (value === undefined || value === null)
|
|
18
|
+
return;
|
|
19
|
+
const text = String(value).trim();
|
|
20
|
+
if (text)
|
|
21
|
+
masters.add(text);
|
|
22
|
+
};
|
|
23
|
+
collect(cfg.master);
|
|
24
|
+
collect(cfg.notify?.id);
|
|
25
|
+
if (Array.isArray(cfg.endpoints)) {
|
|
26
|
+
for (const endpoint of cfg.endpoints) {
|
|
27
|
+
collect(endpoint.master);
|
|
28
|
+
collect(endpoint.notify?.id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Login control is account-sensitive: without an explicit master/notify target,
|
|
32
|
+
// refuse chat recovery and leave it to the authenticated Console page.
|
|
33
|
+
if (masters.size === 0)
|
|
34
|
+
return false;
|
|
35
|
+
const senderRaw = input?.sender;
|
|
36
|
+
const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? String(senderRaw.id).trim() : '';
|
|
37
|
+
return senderId !== '' && masters.has(senderId);
|
|
38
|
+
}
|
|
39
|
+
export function pickIcqqLoginControl(controls, requested) {
|
|
40
|
+
const wanted = requested?.trim();
|
|
41
|
+
if (wanted) {
|
|
42
|
+
const control = controls.get(wanted);
|
|
43
|
+
return control ? { control } : { text: `找不到 ICQQ endpoint「${wanted}」` };
|
|
44
|
+
}
|
|
45
|
+
if (controls.size === 0)
|
|
46
|
+
return { text: 'ICQQ 适配器还没有启动' };
|
|
47
|
+
if (controls.size === 1) {
|
|
48
|
+
const control = [...controls.values()][0];
|
|
49
|
+
return control ? { control } : { text: 'ICQQ 适配器还没有启动' };
|
|
50
|
+
}
|
|
51
|
+
return { text: `有多个 ICQQ endpoint,请指定 QQ 号:${[...controls.keys()].join('、')}` };
|
|
52
|
+
}
|
|
53
|
+
export function icqqLoginForbidden() {
|
|
54
|
+
return '仅 master 或 notify 目标账号可以操作 ICQQ 登录';
|
|
55
|
+
}
|
|
56
|
+
export function pendingTaskLabel(type) {
|
|
57
|
+
switch (type) {
|
|
58
|
+
case 'qrcode': return '二维码';
|
|
59
|
+
case 'slider': return '滑块验证';
|
|
60
|
+
case 'device': return '设备确认';
|
|
61
|
+
case 'sms': return '短信验证';
|
|
62
|
+
case 'auth': return '登录确认';
|
|
63
|
+
default: return '登录验证';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `icqq login` recovery commands.
|
|
3
|
+
*
|
|
4
|
+
* They are registered by the ICQQ plugin but can be invoked from any connected
|
|
5
|
+
* adapter (typically the QQ instance that delivered the outage notice), so the
|
|
6
|
+
* master can fetch the QR image and confirm/send verification codes without the
|
|
7
|
+
* Console or terminal.
|
|
8
|
+
*/
|
|
9
|
+
import { icqqLoginControlToken, type IcqqLoginControl } from '../../../src/icqq-login-control.js';
|
|
10
|
+
|
|
11
|
+
export { icqqLoginControlToken };
|
|
12
|
+
|
|
13
|
+
/** Master = framework master(s), or the configured outage-notify conversation id(s). */
|
|
14
|
+
export function isIcqqLoginOperator(config: unknown, input: unknown): boolean {
|
|
15
|
+
const cfg = (config ?? {}) as {
|
|
16
|
+
master?: unknown;
|
|
17
|
+
notify?: { id?: unknown };
|
|
18
|
+
endpoints?: readonly { master?: unknown; notify?: { id?: unknown } }[];
|
|
19
|
+
};
|
|
20
|
+
const masters = new Set<string>();
|
|
21
|
+
const collect = (value: unknown) => {
|
|
22
|
+
if (value === undefined || value === null) return;
|
|
23
|
+
const text = String(value).trim();
|
|
24
|
+
if (text) masters.add(text);
|
|
25
|
+
};
|
|
26
|
+
collect(cfg.master);
|
|
27
|
+
collect(cfg.notify?.id);
|
|
28
|
+
if (Array.isArray(cfg.endpoints)) {
|
|
29
|
+
for (const endpoint of cfg.endpoints) {
|
|
30
|
+
collect(endpoint.master);
|
|
31
|
+
collect(endpoint.notify?.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Login control is account-sensitive: without an explicit master/notify target,
|
|
35
|
+
// refuse chat recovery and leave it to the authenticated Console page.
|
|
36
|
+
if (masters.size === 0) return false;
|
|
37
|
+
const senderRaw = (input as { sender?: { id?: unknown } } | null | undefined)?.sender;
|
|
38
|
+
const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? String(senderRaw.id).trim() : '';
|
|
39
|
+
return senderId !== '' && masters.has(senderId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function pickIcqqLoginControl(
|
|
43
|
+
controls: ReadonlyMap<string, IcqqLoginControl>,
|
|
44
|
+
requested?: string,
|
|
45
|
+
): { control?: IcqqLoginControl; text?: string } {
|
|
46
|
+
const wanted = requested?.trim();
|
|
47
|
+
if (wanted) {
|
|
48
|
+
const control = controls.get(wanted);
|
|
49
|
+
return control ? { control } : { text: `找不到 ICQQ endpoint「${wanted}」` };
|
|
50
|
+
}
|
|
51
|
+
if (controls.size === 0) return { text: 'ICQQ 适配器还没有启动' };
|
|
52
|
+
if (controls.size === 1) {
|
|
53
|
+
const control = [...controls.values()][0];
|
|
54
|
+
return control ? { control } : { text: 'ICQQ 适配器还没有启动' };
|
|
55
|
+
}
|
|
56
|
+
return { text: `有多个 ICQQ endpoint,请指定 QQ 号:${[...controls.keys()].join('、')}` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function icqqLoginForbidden(): string {
|
|
60
|
+
return '仅 master 或 notify 目标账号可以操作 ICQQ 登录';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function pendingTaskLabel(type: string): string {
|
|
64
|
+
switch (type) {
|
|
65
|
+
case 'qrcode': return '二维码';
|
|
66
|
+
case 'slider': return '滑块验证';
|
|
67
|
+
case 'device': return '设备确认';
|
|
68
|
+
case 'sms': return '短信验证';
|
|
69
|
+
case 'auth': return '登录确认';
|
|
70
|
+
default: return '登录验证';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { icqqLoginControlToken, icqqLoginForbidden, isIcqqLoginOperator, pickIcqqLoginControl, } from '../definition.js';
|
|
4
|
+
const QR_IMAGE = /^data:image\/[a-z0-9.+-]+;base64,(.+)$/iu;
|
|
5
|
+
export default defineCommand({
|
|
6
|
+
description: '获取 ICQQ 登录二维码(发给当前会话)',
|
|
7
|
+
descriptionI18n: { en: 'Fetch the ICQQ login QR code into this conversation' },
|
|
8
|
+
async execute({ args, config, input, use }) {
|
|
9
|
+
if (!isIcqqLoginOperator(config, input))
|
|
10
|
+
return icqqLoginForbidden();
|
|
11
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
12
|
+
if (!picked.control)
|
|
13
|
+
return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
14
|
+
const control = picked.control;
|
|
15
|
+
const findQr = () => control.pending().find((task) => task.type === 'qrcode' || task.type === 'auth');
|
|
16
|
+
let task = findQr();
|
|
17
|
+
if (!task) {
|
|
18
|
+
const other = control.pending().find((item) => item.type !== 'qrcode' && item.type !== 'auth');
|
|
19
|
+
if (other) {
|
|
20
|
+
const url = typeof other.payload.url === 'string' ? other.payload.url : '';
|
|
21
|
+
return `ICQQ ${other.endpointKey} 当前是${other.type === 'slider' ? '滑块验证' : '登录验证'},请打开 ${url || '验证页面'},`
|
|
22
|
+
+ '完成后回复 `icqq login code <内容>`。';
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await control.relogin();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// The SDK may still deliver a QR challenge asynchronously.
|
|
29
|
+
}
|
|
30
|
+
task = findQr();
|
|
31
|
+
if (!task)
|
|
32
|
+
return '已触发重新登录,请 5 秒后再发送 `icqq login qr` 获取二维码。';
|
|
33
|
+
}
|
|
34
|
+
const raw = typeof task.payload.image === 'string' ? task.payload.image : '';
|
|
35
|
+
const base64 = QR_IMAGE.exec(raw)?.[1];
|
|
36
|
+
if (!base64) {
|
|
37
|
+
return `ICQQ ${task.endpointKey} 当前是 ${task.type === 'qrcode' ? '二维码' : '登录确认'}待办,但没有可发送的图片;请打开 Console 的 ICQQ 页面完成。`;
|
|
38
|
+
}
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
data: {
|
|
43
|
+
text: `ICQQ ${task.endpointKey} 登录二维码(约 3 分钟有效)。\n`
|
|
44
|
+
+ '用手机 QQ 扫码,完成后回复 `icqq login confirm`。',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'image',
|
|
49
|
+
data: { media: { kind: 'base64', value: base64, mime_type: 'image/png' } },
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineCommand } from 'zhin.js/command';
|
|
2
|
+
import {
|
|
3
|
+
icqqLoginControlToken,
|
|
4
|
+
icqqLoginForbidden,
|
|
5
|
+
isIcqqLoginOperator,
|
|
6
|
+
pickIcqqLoginControl,
|
|
7
|
+
} from '../definition.js';
|
|
8
|
+
|
|
9
|
+
const QR_IMAGE = /^data:image\/[a-z0-9.+-]+;base64,(.+)$/iu;
|
|
10
|
+
|
|
11
|
+
export default defineCommand({
|
|
12
|
+
description: '获取 ICQQ 登录二维码(发给当前会话)',
|
|
13
|
+
descriptionI18n: { en: 'Fetch the ICQQ login QR code into this conversation' },
|
|
14
|
+
async execute({ args, config, input, use }) {
|
|
15
|
+
if (!isIcqqLoginOperator(config, input)) return icqqLoginForbidden();
|
|
16
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
17
|
+
if (!picked.control) return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
18
|
+
const control = picked.control;
|
|
19
|
+
const findQr = () => control.pending().find((task) => task.type === 'qrcode' || task.type === 'auth');
|
|
20
|
+
let task = findQr();
|
|
21
|
+
if (!task) {
|
|
22
|
+
const other = control.pending().find((item) => item.type !== 'qrcode' && item.type !== 'auth');
|
|
23
|
+
if (other) {
|
|
24
|
+
const url = typeof other.payload.url === 'string' ? other.payload.url : '';
|
|
25
|
+
return `ICQQ ${other.endpointKey} 当前是${other.type === 'slider' ? '滑块验证' : '登录验证'},请打开 ${url || '验证页面'},`
|
|
26
|
+
+ '完成后回复 `icqq login code <内容>`。';
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
await control.relogin();
|
|
30
|
+
} catch {
|
|
31
|
+
// The SDK may still deliver a QR challenge asynchronously.
|
|
32
|
+
}
|
|
33
|
+
task = findQr();
|
|
34
|
+
if (!task) return '已触发重新登录,请 5 秒后再发送 `icqq login qr` 获取二维码。';
|
|
35
|
+
}
|
|
36
|
+
const raw = typeof task.payload.image === 'string' ? task.payload.image : '';
|
|
37
|
+
const base64 = QR_IMAGE.exec(raw)?.[1];
|
|
38
|
+
if (!base64) {
|
|
39
|
+
return `ICQQ ${task.endpointKey} 当前是 ${task.type === 'qrcode' ? '二维码' : '登录确认'}待办,但没有可发送的图片;请打开 Console 的 ICQQ 页面完成。`;
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
data: {
|
|
45
|
+
text: `ICQQ ${task.endpointKey} 登录二维码(约 3 分钟有效)。\n`
|
|
46
|
+
+ '用手机 QQ 扫码,完成后回复 `icqq login confirm`。',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'image',
|
|
51
|
+
data: { media: { kind: 'base64', value: base64, mime_type: 'image/png' } },
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { icqqLoginControlToken, icqqLoginForbidden, isIcqqLoginOperator, pickIcqqLoginControl, } from '../definition.js';
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
description: '重新触发 ICQQ 登录',
|
|
6
|
+
descriptionI18n: { en: 'Trigger an ICQQ login attempt again' },
|
|
7
|
+
async execute({ args, config, input, use }) {
|
|
8
|
+
if (!isIcqqLoginOperator(config, input))
|
|
9
|
+
return icqqLoginForbidden();
|
|
10
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
11
|
+
if (!picked.control)
|
|
12
|
+
return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
13
|
+
try {
|
|
14
|
+
await picked.control.relogin();
|
|
15
|
+
return `已触发 ICQQ ${picked.control.endpointId} 重新登录;需要扫码时发送 \`icqq login qr\`。`;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return `ICQQ ${picked.control.endpointId} 重新登录失败,请稍后重试或查看日志。`;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineCommand } from 'zhin.js/command';
|
|
2
|
+
import {
|
|
3
|
+
icqqLoginControlToken,
|
|
4
|
+
icqqLoginForbidden,
|
|
5
|
+
isIcqqLoginOperator,
|
|
6
|
+
pickIcqqLoginControl,
|
|
7
|
+
} from '../definition.js';
|
|
8
|
+
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: '重新触发 ICQQ 登录',
|
|
11
|
+
descriptionI18n: { en: 'Trigger an ICQQ login attempt again' },
|
|
12
|
+
async execute({ args, config, input, use }) {
|
|
13
|
+
if (!isIcqqLoginOperator(config, input)) return icqqLoginForbidden();
|
|
14
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
15
|
+
if (!picked.control) return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
16
|
+
try {
|
|
17
|
+
await picked.control.relogin();
|
|
18
|
+
return `已触发 ICQQ ${picked.control.endpointId} 重新登录;需要扫码时发送 \`icqq login qr\`。`;
|
|
19
|
+
} catch {
|
|
20
|
+
return `ICQQ ${picked.control.endpointId} 重新登录失败,请稍后重试或查看日志。`;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { icqqLoginControlToken, icqqLoginForbidden, isIcqqLoginOperator, pendingTaskLabel, pickIcqqLoginControl, } from '../definition.js';
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
description: '查看 ICQQ 掉线状态与登录待办',
|
|
6
|
+
descriptionI18n: { en: 'Show ICQQ offline status and pending login tasks' },
|
|
7
|
+
async execute({ args, config, input, use }) {
|
|
8
|
+
if (!isIcqqLoginOperator(config, input))
|
|
9
|
+
return icqqLoginForbidden();
|
|
10
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
11
|
+
if (!picked.control)
|
|
12
|
+
return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
13
|
+
const state = picked.control.state();
|
|
14
|
+
const lines = [
|
|
15
|
+
`ICQQ ${state.endpointId}: ${state.online ? '在线' : '离线'}(${state.transportState})`,
|
|
16
|
+
];
|
|
17
|
+
if (state.pending.length === 0) {
|
|
18
|
+
lines.push('没有待处理的登录验证。');
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
lines.push('待处理:');
|
|
22
|
+
for (const task of state.pending) {
|
|
23
|
+
const url = typeof task.payload.url === 'string' ? task.payload.url : undefined;
|
|
24
|
+
lines.push(`- ${pendingTaskLabel(task.type)}(${task.id},endpoint ${task.endpointKey})`
|
|
25
|
+
+ (url ? `:${url}` : ''));
|
|
26
|
+
}
|
|
27
|
+
lines.push('二维码:`icqq login qr`;完成扫码后:`icqq login confirm`;滑块/短信:`icqq login code <内容>`。');
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineCommand } from 'zhin.js/command';
|
|
2
|
+
import {
|
|
3
|
+
icqqLoginControlToken,
|
|
4
|
+
icqqLoginForbidden,
|
|
5
|
+
isIcqqLoginOperator,
|
|
6
|
+
pendingTaskLabel,
|
|
7
|
+
pickIcqqLoginControl,
|
|
8
|
+
} from '../definition.js';
|
|
9
|
+
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
description: '查看 ICQQ 掉线状态与登录待办',
|
|
12
|
+
descriptionI18n: { en: 'Show ICQQ offline status and pending login tasks' },
|
|
13
|
+
async execute({ args, config, input, use }) {
|
|
14
|
+
if (!isIcqqLoginOperator(config, input)) return icqqLoginForbidden();
|
|
15
|
+
const picked = pickIcqqLoginControl(use(icqqLoginControlToken), args[0]);
|
|
16
|
+
if (!picked.control) return picked.text ?? 'ICQQ 适配器还没有启动';
|
|
17
|
+
const state = picked.control.state();
|
|
18
|
+
const lines = [
|
|
19
|
+
`ICQQ ${state.endpointId}: ${state.online ? '在线' : '离线'}(${state.transportState})`,
|
|
20
|
+
];
|
|
21
|
+
if (state.pending.length === 0) {
|
|
22
|
+
lines.push('没有待处理的登录验证。');
|
|
23
|
+
} else {
|
|
24
|
+
lines.push('待处理:');
|
|
25
|
+
for (const task of state.pending) {
|
|
26
|
+
const url = typeof task.payload.url === 'string' ? task.payload.url : undefined;
|
|
27
|
+
lines.push(`- ${pendingTaskLabel(task.type)}(${task.id},endpoint ${task.endpointKey})`
|
|
28
|
+
+ (url ? `:${url}` : ''));
|
|
29
|
+
}
|
|
30
|
+
lines.push('二维码:`icqq login qr`;完成扫码后:`icqq login confirm`;滑块/短信:`icqq login code <内容>`。');
|
|
31
|
+
}
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
},
|
|
34
|
+
});
|