@zhin.js/adapter-qq 7.0.3 → 7.0.5
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 +80 -0
- package/README.md +36 -9
- package/adapters/qq.js +9 -7
- package/adapters/qq.ts +9 -7
- package/commands/endpoint/cancel.js +1 -1
- package/commands/endpoint/cancel.ts +1 -1
- package/lib/endpoint.js +55 -45
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/platform-permit.d.ts +2 -3
- package/lib/platform-permit.js +3 -11
- package/lib/protocol.d.ts +9 -5
- package/lib/protocol.js +11 -7
- package/lib/qq-agent-deps.d.ts +2 -2
- package/lib/qq-agent-deps.js +7 -7
- package/lib/qq-bind-persist.d.ts +2 -2
- package/lib/qq-bind-persist.js +5 -5
- package/lib/qq-bot-kind-prompt.d.ts +9 -0
- package/lib/qq-bot-kind-prompt.js +37 -0
- package/lib/qq-endpoint-commands.d.ts +13 -6
- package/lib/qq-endpoint-commands.js +89 -31
- package/lib/qq-endpoint-config.d.ts +4 -4
- package/lib/qq-endpoint-config.js +4 -4
- package/lib/qq-intents.d.ts +27 -0
- package/lib/qq-intents.js +50 -0
- package/lib/qq-runtime-state.d.ts +17 -4
- package/lib/qq-runtime-state.js +3 -2
- package/lib/webhook.js +2 -0
- package/lib/ws.js +2 -2
- package/middlewares/bot-kind-reply.js +46 -0
- package/middlewares/bot-kind-reply.ts +60 -0
- package/package.json +23 -16
- package/plugin.js +6 -4
- package/schema.json +66 -9
- package/src/endpoint.ts +69 -46
- package/src/index.ts +10 -1
- package/src/platform-permit.ts +4 -9
- package/src/protocol.ts +19 -11
- package/src/qq-agent-deps.ts +8 -8
- package/src/qq-bind-persist.ts +5 -5
- package/src/qq-bot-kind-prompt.ts +47 -0
- package/src/qq-endpoint-commands.ts +125 -34
- package/src/qq-endpoint-config.ts +5 -5
- package/src/qq-intents.ts +76 -0
- package/src/qq-runtime-state.ts +21 -6
- package/src/webhook.ts +2 -0
- package/src/ws.ts +2 -2
- /package/commands/endpoint/add/{[[name]].js → [[id]].js} +0 -0
- /package/commands/endpoint/add/{[[name]].ts → [[id]].ts} +0 -0
- /package/commands/endpoint/remove/{[name].js → [id].js} +0 -0
- /package/commands/endpoint/remove/{[name].ts → [id].ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# @zhin.js/adapter-qq
|
|
2
2
|
|
|
3
|
+
## 7.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 51015d6: refactor(adapters): migrate platform-permit to PermissionSubject
|
|
8
|
+
|
|
9
|
+
All adapter platform-permit checkers now accept PermissionSubject
|
|
10
|
+
(duck-typed from Message/CommandSession) instead of Message directly.
|
|
11
|
+
registerPlatformPermitChecker is replaced by host.registerPlatform in
|
|
12
|
+
the adapter plugin setup. activity-feedback service updated for new
|
|
13
|
+
PermissionHost integration.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [6f9c366]
|
|
16
|
+
- Updated dependencies [373a56b]
|
|
17
|
+
- Updated dependencies [0de46a8]
|
|
18
|
+
- Updated dependencies [c106ecc]
|
|
19
|
+
- Updated dependencies [ca92e03]
|
|
20
|
+
- Updated dependencies [b0f37ae]
|
|
21
|
+
- Updated dependencies [ba08a2f]
|
|
22
|
+
- Updated dependencies [b08f7fe]
|
|
23
|
+
- Updated dependencies [daffd4c]
|
|
24
|
+
- Updated dependencies [36c7400]
|
|
25
|
+
- Updated dependencies [a9fa72e]
|
|
26
|
+
- Updated dependencies [c8de3ef]
|
|
27
|
+
- Updated dependencies [60f0fc8]
|
|
28
|
+
- Updated dependencies [574c990]
|
|
29
|
+
- Updated dependencies [d096f16]
|
|
30
|
+
- Updated dependencies [3f29623]
|
|
31
|
+
- Updated dependencies [2916852]
|
|
32
|
+
- Updated dependencies [d047869]
|
|
33
|
+
- Updated dependencies [162fa34]
|
|
34
|
+
- Updated dependencies [e62561e]
|
|
35
|
+
- Updated dependencies [3eeeb46]
|
|
36
|
+
- Updated dependencies [85d0f82]
|
|
37
|
+
- Updated dependencies [61bfc1c]
|
|
38
|
+
- Updated dependencies [04bad47]
|
|
39
|
+
- Updated dependencies [e40b048]
|
|
40
|
+
- Updated dependencies [5a5b1bb]
|
|
41
|
+
- Updated dependencies [f1708c3]
|
|
42
|
+
- Updated dependencies [d254a81]
|
|
43
|
+
- Updated dependencies [7123c47]
|
|
44
|
+
- Updated dependencies [05befc1]
|
|
45
|
+
- Updated dependencies [0663b6a]
|
|
46
|
+
- Updated dependencies [f28f9b3]
|
|
47
|
+
- Updated dependencies [9f340f7]
|
|
48
|
+
- Updated dependencies [e53444f]
|
|
49
|
+
- Updated dependencies [5eedd26]
|
|
50
|
+
- Updated dependencies [92b0dd7]
|
|
51
|
+
- Updated dependencies [f919b6f]
|
|
52
|
+
- Updated dependencies [098e411]
|
|
53
|
+
- Updated dependencies [e1b7c01]
|
|
54
|
+
- Updated dependencies [9b94f87]
|
|
55
|
+
- Updated dependencies [a7df753]
|
|
56
|
+
- @zhin.js/agent@1.1.5
|
|
57
|
+
- @zhin.js/host-http@1.0.8
|
|
58
|
+
- @zhin.js/permission@1.0.1
|
|
59
|
+
- @zhin.js/im-contract@1.0.3
|
|
60
|
+
- @zhin.js/adapter@1.1.7
|
|
61
|
+
- @zhin.js/plugin-runtime@1.1.5
|
|
62
|
+
- @zhin.js/command@1.0.9
|
|
63
|
+
- @zhin.js/core@1.5.4
|
|
64
|
+
- zhin.js@6.0.4
|
|
65
|
+
- @zhin.js/middleware@1.0.8
|
|
66
|
+
|
|
67
|
+
## 7.0.4
|
|
68
|
+
|
|
69
|
+
### Patch Changes
|
|
70
|
+
|
|
71
|
+
- f8c7a54: fix: im
|
|
72
|
+
- Updated dependencies [f8c7a54]
|
|
73
|
+
- @zhin.js/logger@1.0.76
|
|
74
|
+
- @zhin.js/host-http@1.0.7
|
|
75
|
+
- @zhin.js/adapter@1.1.6
|
|
76
|
+
- @zhin.js/agent@1.1.4
|
|
77
|
+
- @zhin.js/command@1.0.8
|
|
78
|
+
- @zhin.js/core@1.5.3
|
|
79
|
+
- @zhin.js/im-contract@1.0.2
|
|
80
|
+
- @zhin.js/plugin-runtime@1.1.4
|
|
81
|
+
- zhin.js@6.0.3
|
|
82
|
+
|
|
3
83
|
## 7.0.3
|
|
4
84
|
|
|
5
85
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Zhin.js QQ 官方机器人适配器(Plugin Runtime),默认通过 **WebSock
|
|
|
6
6
|
|
|
7
7
|
- WebSocket Gateway 入站(默认;无需公网 HTTPS / host)
|
|
8
8
|
- 解析私聊 / 群 / 频道消息
|
|
9
|
-
- 出站 `send({
|
|
9
|
+
- 出站 `send({ conversation, payload })` → QQ API(`conversation.kind`/`id`/`parent` 结构化寻址)
|
|
10
10
|
- 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
|
|
11
11
|
- Webhook / middleware 模式已实现(经 `httpHostToken` 注册 POST 路由)
|
|
12
12
|
- AI `@` 触发标注:群消息(GROUP_AT_MESSAGE_CREATE 仅 @ 时下发)与频道 `mentions[].bot` 会在入站 metadata 标 `mentioned: true`(新 Plugin Runtime 纯文本 content 经 metadata 传递 @)
|
|
@@ -25,8 +25,8 @@ pnpm add @zhin.js/adapter-qq
|
|
|
25
25
|
- 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
|
|
26
26
|
- **无需** `@zhin.js/host-http` / `@zhin.js/host-router`(WebSocket 路径)
|
|
27
27
|
|
|
28
|
-
入站:`gateway.receive({
|
|
29
|
-
出站:`send({
|
|
28
|
+
入站:`gateway.receive({ conversation, message, content, sender, metadata })`(ConversationRef 结构化寻址)
|
|
29
|
+
出站:`send({ conversation, payload })` → `sendPrivateMessage` / `sendGroupMessage` / `sendGuildMessage`
|
|
30
30
|
|
|
31
31
|
## 前置条件
|
|
32
32
|
|
|
@@ -49,22 +49,49 @@ plugins:
|
|
|
49
49
|
- name: my-qq-bot
|
|
50
50
|
appid: ${QQ_APPID}
|
|
51
51
|
secret: ${QQ_SECRET}
|
|
52
|
+
# botKind / intents 可由向导写入;`qq.endpoint add` 扫码后会追问公域/私域再写入
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+
### botKind 与 intents
|
|
56
|
+
|
|
57
|
+
WebSocket Identify 的 `intents` **必须与开放平台已开通的权限一致**,否则会断连。
|
|
58
|
+
群聊无公/私域之分,两类都订阅 `GROUP_AND_C2C_EVENT`;差异只在频道消息 intent:
|
|
59
|
+
|
|
60
|
+
| `botKind` | 频道消息 intent | 共用 intents |
|
|
61
|
+
|-----------|-----------------|--------------|
|
|
62
|
+
| `public`(默认,公域) | `PUBLIC_GUILD_MESSAGES`(仅 @) | `GROUP_AND_C2C_EVENT`, `GUILDS`, `GUILD_MEMBERS`, `DIRECT_MESSAGE` |
|
|
63
|
+
| `private`(私域) | `GUILD_MESSAGES`(频道全量) | 同上 |
|
|
64
|
+
|
|
65
|
+
公域机器人订阅 `GUILD_MESSAGES` 会 Identify 失败断连。显式配置 `intents` 时优先于 `botKind`。
|
|
66
|
+
`create-zhin` / `zhin setup` 会询问公/私域并写入;`qq.endpoint add` 扫码成功后在同一会话追问 `public`/`private`(或 `公域`/`私域`),确认后一次性写 `.env` 与 yaml。
|
|
67
|
+
|
|
54
68
|
多账号:一个插件实例挂多个 endpoint(`endpoints` 数组逐项覆盖顶层字段,`name` 必填):
|
|
55
69
|
|
|
56
70
|
```yaml
|
|
57
71
|
plugins:
|
|
58
72
|
qq:
|
|
59
73
|
mode: websocket
|
|
60
|
-
intents: [GUILDS, GROUP_AND_C2C_EVENT]
|
|
61
74
|
endpoints:
|
|
62
75
|
- name: main-bot
|
|
63
76
|
appid: ${QQ_APPID}
|
|
64
77
|
secret: ${QQ_SECRET}
|
|
65
|
-
|
|
78
|
+
botKind: public
|
|
79
|
+
intents:
|
|
80
|
+
- GROUP_AND_C2C_EVENT
|
|
81
|
+
- GUILDS
|
|
82
|
+
- GUILD_MEMBERS
|
|
83
|
+
- DIRECT_MESSAGE
|
|
84
|
+
- PUBLIC_GUILD_MESSAGES
|
|
85
|
+
- name: private-bot
|
|
66
86
|
appid: ${QQ_APPID_2}
|
|
67
87
|
secret: ${QQ_SECRET_2}
|
|
88
|
+
botKind: private
|
|
89
|
+
intents:
|
|
90
|
+
- GROUP_AND_C2C_EVENT
|
|
91
|
+
- GUILDS
|
|
92
|
+
- GUILD_MEMBERS
|
|
93
|
+
- DIRECT_MESSAGE
|
|
94
|
+
- GUILD_MESSAGES
|
|
68
95
|
```
|
|
69
96
|
|
|
70
97
|
根插件 `zhin.plugins`(或项目图)需引用 `@zhin.js/adapter-qq`(`instanceKey: qq`)。
|
|
@@ -75,13 +102,13 @@ plugins:
|
|
|
75
102
|
|
|
76
103
|
| 命令 | 说明 |
|
|
77
104
|
|------|------|
|
|
78
|
-
| `qq.endpoint add [name]` | 手机 QQ
|
|
79
|
-
| `qq.endpoint cancel` |
|
|
105
|
+
| `qq.endpoint add [name]` | 手机 QQ 扫码绑定 → **追问公域/私域** → 一次性写入 `.env` + `plugins.qq.endpoints`(重启生效) |
|
|
106
|
+
| `qq.endpoint cancel` | 取消进行中的扫码绑定或待确认的公域/私域选择 |
|
|
80
107
|
| `qq.endpoint list` | 列出运行中与配置中的 endpoints |
|
|
81
108
|
| `qq.endpoint remove <name>` | 从配置移除 endpoint(`.env` 键保留,可手动清理) |
|
|
82
109
|
|
|
83
|
-
add/cancel/remove 受 `master` 限制:实例配置声明了 `master`(顶层或 `endpoints[i]`)时仅
|
|
84
|
-
master
|
|
110
|
+
add/cancel/remove 受 `master` 限制:实例配置声明了 `master`(顶层或 `endpoints[i].master`)时仅
|
|
111
|
+
master 可执行;未配置则放行(首个扫码绑定者会写入该 endpoint 的 `master`)。二维码当前以链接文本下发
|
|
85
112
|
(出站富媒体待迁移),用手机 QQ 打开链接即可扫码。
|
|
86
113
|
|
|
87
114
|
## 环境变量
|
package/adapters/qq.js
CHANGED
|
@@ -22,26 +22,28 @@ export default defineAdapter({
|
|
|
22
22
|
const gateway = context.use(messageGatewayToken);
|
|
23
23
|
// 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
|
|
24
24
|
const state = context.use(qqRuntimeStateToken);
|
|
25
|
-
state.endpoints.set(config.
|
|
26
|
-
|
|
25
|
+
state.endpoints.set(config.id, {
|
|
26
|
+
id: config.id,
|
|
27
27
|
mode: config.mode,
|
|
28
28
|
});
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// 正向判 websocket:TS 对 `mode === 'webhook' || mode === 'middleware'` 的
|
|
30
|
+
// 否定分支不做联合消减(多字面量判别式),反向写会让 config 窄化失败。
|
|
31
|
+
const endpoint = config.mode === 'websocket'
|
|
32
|
+
? new QqWebsocketEndpoint({
|
|
31
33
|
id: context.id,
|
|
32
34
|
gateway,
|
|
33
|
-
http: context.use(httpHostToken),
|
|
34
35
|
config,
|
|
35
36
|
})
|
|
36
|
-
: new
|
|
37
|
+
: new QqHttpEndpoint({
|
|
37
38
|
id: context.id,
|
|
38
39
|
gateway,
|
|
40
|
+
http: context.use(httpHostToken),
|
|
39
41
|
config,
|
|
40
42
|
});
|
|
41
43
|
// 运行状态表只增不减:stop 时同步摘除,避免 stop 后 qq.endpoint list 仍显示运行中。
|
|
42
44
|
const stopEndpoint = endpoint.stop.bind(endpoint);
|
|
43
45
|
endpoint.stop = async () => {
|
|
44
|
-
state.endpoints.delete(config.
|
|
46
|
+
state.endpoints.delete(config.id);
|
|
45
47
|
await stopEndpoint();
|
|
46
48
|
};
|
|
47
49
|
return endpoint;
|
package/adapters/qq.ts
CHANGED
|
@@ -37,26 +37,28 @@ export default defineAdapter<QqAdapterConfig>({
|
|
|
37
37
|
const gateway = context.use(messageGatewayToken);
|
|
38
38
|
// 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
|
|
39
39
|
const state = context.use(qqRuntimeStateToken);
|
|
40
|
-
state.endpoints.set(config.
|
|
41
|
-
|
|
40
|
+
state.endpoints.set(config.id, {
|
|
41
|
+
id: config.id,
|
|
42
42
|
mode: config.mode,
|
|
43
43
|
});
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// 正向判 websocket:TS 对 `mode === 'webhook' || mode === 'middleware'` 的
|
|
45
|
+
// 否定分支不做联合消减(多字面量判别式),反向写会让 config 窄化失败。
|
|
46
|
+
const endpoint = config.mode === 'websocket'
|
|
47
|
+
? new QqWebsocketEndpoint({
|
|
46
48
|
id: context.id,
|
|
47
49
|
gateway,
|
|
48
|
-
http: context.use(httpHostToken),
|
|
49
50
|
config,
|
|
50
51
|
})
|
|
51
|
-
: new
|
|
52
|
+
: new QqHttpEndpoint({
|
|
52
53
|
id: context.id,
|
|
53
54
|
gateway,
|
|
55
|
+
http: context.use(httpHostToken),
|
|
54
56
|
config,
|
|
55
57
|
});
|
|
56
58
|
// 运行状态表只增不减:stop 时同步摘除,避免 stop 后 qq.endpoint list 仍显示运行中。
|
|
57
59
|
const stopEndpoint = endpoint.stop.bind(endpoint);
|
|
58
60
|
endpoint.stop = async () => {
|
|
59
|
-
state.endpoints.delete(config.
|
|
61
|
+
state.endpoints.delete(config.id);
|
|
60
62
|
await stopEndpoint();
|
|
61
63
|
};
|
|
62
64
|
return endpoint;
|
|
@@ -3,7 +3,7 @@ import { defineCommand } from '@zhin.js/command';
|
|
|
3
3
|
import { isQqEndpointOperator, QQ_ENDPOINT_FORBIDDEN, runQqEndpointCancel, } from "../../lib/qq-endpoint-commands.js";
|
|
4
4
|
import { qqRuntimeStateToken } from "../../lib/qq-runtime-state.js";
|
|
5
5
|
export default defineCommand({
|
|
6
|
-
description: '取消进行中的 QQ
|
|
6
|
+
description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
|
|
7
7
|
execute({ config, input, use }) {
|
|
8
8
|
if (!isQqEndpointOperator(config, input))
|
|
9
9
|
return QQ_ENDPOINT_FORBIDDEN;
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { qqRuntimeStateToken } from '../../src/qq-runtime-state.js';
|
|
8
8
|
|
|
9
9
|
export default defineCommand({
|
|
10
|
-
description: '取消进行中的 QQ
|
|
10
|
+
description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
|
|
11
11
|
execute({ config, input, use }) {
|
|
12
12
|
if (!isQqEndpointOperator(config, input)) return QQ_ENDPOINT_FORBIDDEN;
|
|
13
13
|
return runQqEndpointCancel(use(qqRuntimeStateToken));
|
package/lib/endpoint.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { formatCompact,
|
|
1
|
+
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
2
2
|
import { formatOutbound } from './outbound.js';
|
|
3
3
|
import { registerQqAgentEndpoint } from './qq-agent-deps.js';
|
|
4
4
|
import { formatInboundContent, parseCompoundMessageId, qqInboundConversation, qqOutboundKind, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
|
|
5
5
|
import { defaultCreateHttpBot, registerQqWebhookRoutes, } from './webhook.js';
|
|
6
6
|
import { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
|
|
7
|
-
const logger = getLogger('qq');
|
|
8
7
|
export class QqWebsocketEndpoint {
|
|
8
|
+
#logger;
|
|
9
9
|
#options;
|
|
10
10
|
#createBot;
|
|
11
11
|
#bot = null;
|
|
@@ -14,32 +14,28 @@ export class QqWebsocketEndpoint {
|
|
|
14
14
|
#unregisterAgent;
|
|
15
15
|
management = createQqEndpointManagement(this);
|
|
16
16
|
constructor(options) {
|
|
17
|
+
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
17
18
|
this.#options = options;
|
|
18
19
|
this.#createBot = options.createBot ?? defaultCreateBot;
|
|
19
20
|
}
|
|
20
21
|
/** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
|
|
21
22
|
get name() {
|
|
22
|
-
return this.#options.config.
|
|
23
|
+
return this.#options.config.id;
|
|
23
24
|
}
|
|
24
25
|
async start() {
|
|
25
26
|
if (this.#started)
|
|
26
27
|
return;
|
|
27
28
|
this.#started = true;
|
|
28
29
|
try {
|
|
29
|
-
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.
|
|
30
|
+
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.id, this);
|
|
30
31
|
this.#bot = this.#createBot(this.#options.config);
|
|
31
32
|
this.#bindBot(this.#bot);
|
|
32
33
|
await this.#bot.start();
|
|
33
|
-
logger.info(
|
|
34
|
-
op: 'connect',
|
|
35
|
-
endpoint: this.#options.config.name,
|
|
36
|
-
mode: 'websocket',
|
|
37
|
-
appid: this.#options.config.appid,
|
|
38
|
-
}));
|
|
34
|
+
this.#logger.info(`connected (websocket) | appid: ${this.#options.config.appid}`);
|
|
39
35
|
}
|
|
40
36
|
catch (error) {
|
|
41
37
|
await this.stop();
|
|
42
|
-
logger.error('Failed to connect QQ websocket:', error);
|
|
38
|
+
this.#logger.error('Failed to connect QQ websocket:', error);
|
|
43
39
|
const raw = error instanceof Error ? error.message : String(error);
|
|
44
40
|
throw new Error(`QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`, { cause: error });
|
|
45
41
|
}
|
|
@@ -65,7 +61,7 @@ export class QqWebsocketEndpoint {
|
|
|
65
61
|
this.#bot = null;
|
|
66
62
|
}
|
|
67
63
|
this.#started = false;
|
|
68
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
64
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
69
65
|
}
|
|
70
66
|
async send({ conversation, payload }) {
|
|
71
67
|
const body = formatOutbound(payload);
|
|
@@ -89,12 +85,9 @@ export class QqWebsocketEndpoint {
|
|
|
89
85
|
break;
|
|
90
86
|
}
|
|
91
87
|
const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
|
|
92
|
-
logger.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
target: `${kind}:${conversation.id}`,
|
|
96
|
-
messageId,
|
|
97
|
-
}));
|
|
88
|
+
this.#logger.info(`send ${kind}:${conversation.id}`
|
|
89
|
+
+ ` | id: ${messageId}`
|
|
90
|
+
+ ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`);
|
|
98
91
|
return messageId;
|
|
99
92
|
}
|
|
100
93
|
async recallMessage(messageId) {
|
|
@@ -114,9 +107,9 @@ export class QqWebsocketEndpoint {
|
|
|
114
107
|
await bot.recallDirectMessage?.(channelId, qqMsgId);
|
|
115
108
|
break;
|
|
116
109
|
}
|
|
117
|
-
logger.debug(formatCompact({
|
|
110
|
+
this.#logger.debug(formatCompact({
|
|
118
111
|
op: 'qq_recall',
|
|
119
|
-
endpoint: this.#options.config.
|
|
112
|
+
endpoint: this.#options.config.id,
|
|
120
113
|
kind,
|
|
121
114
|
messageId,
|
|
122
115
|
}));
|
|
@@ -126,23 +119,31 @@ export class QqWebsocketEndpoint {
|
|
|
126
119
|
if (!this.#open)
|
|
127
120
|
return;
|
|
128
121
|
const conversation = qqInboundConversation(String(this.#options.id), msg);
|
|
122
|
+
const content = formatInboundContent(msg);
|
|
123
|
+
this.#logger.info(`recv ${conversation.kind}:${conversation.id}`
|
|
124
|
+
+ (msg.authorId ? ` from ${msg.authorId}` : '')
|
|
125
|
+
+ (msg.mentioned ? ' (mentioned)' : '')
|
|
126
|
+
+ ` | ${truncatePreview(content, 80)}`);
|
|
129
127
|
void this.#options.gateway.receive({
|
|
130
128
|
conversation,
|
|
131
129
|
message: { conversation, id: msg.id },
|
|
132
|
-
content
|
|
130
|
+
content,
|
|
133
131
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
134
|
-
sender:
|
|
132
|
+
sender: {
|
|
133
|
+
id: msg.authorId,
|
|
134
|
+
name: senderDisplayName(msg) || undefined,
|
|
135
|
+
...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
|
|
136
|
+
},
|
|
137
|
+
endpointId: this.#options.config.id,
|
|
138
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
135
139
|
metadata: Object.freeze({
|
|
136
|
-
endpoint: this.#options.config.name,
|
|
137
140
|
channelKind: msg.channelKind,
|
|
138
141
|
userId: msg.authorId,
|
|
139
142
|
guildId: msg.guildId,
|
|
140
143
|
roles: msg.authorRoles,
|
|
141
|
-
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
142
|
-
...(msg.mentioned ? { mentioned: true } : {}),
|
|
143
144
|
}),
|
|
144
145
|
}).catch((err) => {
|
|
145
|
-
logger.warn(formatCompact({
|
|
146
|
+
this.#logger.warn(formatCompact({
|
|
146
147
|
op: 'qq_gateway_receive_failed',
|
|
147
148
|
target: `${conversation.kind}:${conversation.id}`,
|
|
148
149
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -188,6 +189,7 @@ export class QqWebsocketEndpoint {
|
|
|
188
189
|
}
|
|
189
190
|
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
190
191
|
export class QqHttpEndpoint {
|
|
192
|
+
#logger;
|
|
191
193
|
#options;
|
|
192
194
|
#createBot;
|
|
193
195
|
#bot = null;
|
|
@@ -197,33 +199,29 @@ export class QqHttpEndpoint {
|
|
|
197
199
|
#unregisterAgent;
|
|
198
200
|
management = createQqEndpointManagement(this);
|
|
199
201
|
constructor(options) {
|
|
202
|
+
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
200
203
|
this.#options = options;
|
|
201
204
|
this.#createBot = options.createBot ?? defaultCreateHttpBot;
|
|
202
205
|
}
|
|
203
206
|
/** Live endpoint 名(Console/AdapterIndex 展示用)。 */
|
|
204
207
|
get name() {
|
|
205
|
-
return this.#options.config.
|
|
208
|
+
return this.#options.config.id;
|
|
206
209
|
}
|
|
207
210
|
async start() {
|
|
208
211
|
if (this.#started)
|
|
209
212
|
return;
|
|
210
213
|
this.#started = true;
|
|
211
214
|
try {
|
|
212
|
-
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.
|
|
215
|
+
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.id, this);
|
|
213
216
|
this.#setupRoutes();
|
|
214
217
|
this.#bot = this.#createBot(this.#options.config);
|
|
215
218
|
this.#bindBot(this.#bot);
|
|
216
219
|
await this.#bot.start();
|
|
217
|
-
logger.info(
|
|
218
|
-
op: 'connect',
|
|
219
|
-
endpoint: this.#options.config.name,
|
|
220
|
-
mode: this.#options.config.mode,
|
|
221
|
-
path: this.#options.config.webhookPath,
|
|
222
|
-
}));
|
|
220
|
+
this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
|
|
223
221
|
}
|
|
224
222
|
catch (error) {
|
|
225
223
|
await this.stop();
|
|
226
|
-
logger.error('Failed to connect QQ HTTP receiver:', error);
|
|
224
|
+
this.#logger.error('Failed to connect QQ HTTP receiver:', error);
|
|
227
225
|
throw error;
|
|
228
226
|
}
|
|
229
227
|
}
|
|
@@ -250,7 +248,7 @@ export class QqHttpEndpoint {
|
|
|
250
248
|
this.#bot = null;
|
|
251
249
|
}
|
|
252
250
|
this.#started = false;
|
|
253
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
251
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
254
252
|
}
|
|
255
253
|
async send({ conversation, payload }) {
|
|
256
254
|
const body = formatOutbound(payload);
|
|
@@ -273,7 +271,11 @@ export class QqHttpEndpoint {
|
|
|
273
271
|
result = await bot.sendDirectMessage(conversation.id, body);
|
|
274
272
|
break;
|
|
275
273
|
}
|
|
276
|
-
|
|
274
|
+
const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
|
|
275
|
+
this.#logger.info(`send ${kind}:${conversation.id}`
|
|
276
|
+
+ ` | id: ${messageId}`
|
|
277
|
+
+ ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`);
|
|
278
|
+
return messageId;
|
|
277
279
|
}
|
|
278
280
|
async recallMessage(messageId) {
|
|
279
281
|
const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
|
|
@@ -292,9 +294,9 @@ export class QqHttpEndpoint {
|
|
|
292
294
|
await bot.recallDirectMessage?.(channelId, qqMsgId);
|
|
293
295
|
break;
|
|
294
296
|
}
|
|
295
|
-
logger.debug(formatCompact({
|
|
297
|
+
this.#logger.debug(formatCompact({
|
|
296
298
|
op: 'qq_recall',
|
|
297
|
-
endpoint: this.#options.config.
|
|
299
|
+
endpoint: this.#options.config.id,
|
|
298
300
|
kind,
|
|
299
301
|
messageId,
|
|
300
302
|
}));
|
|
@@ -303,23 +305,31 @@ export class QqHttpEndpoint {
|
|
|
303
305
|
if (!this.#open)
|
|
304
306
|
return;
|
|
305
307
|
const conversation = qqInboundConversation(String(this.#options.id), msg);
|
|
308
|
+
const content = formatInboundContent(msg);
|
|
309
|
+
this.#logger.info(`recv ${conversation.kind}:${conversation.id}`
|
|
310
|
+
+ (msg.authorId ? ` from ${msg.authorId}` : '')
|
|
311
|
+
+ (msg.mentioned ? ' (mentioned)' : '')
|
|
312
|
+
+ ` | ${truncatePreview(content, 80)}`);
|
|
306
313
|
void this.#options.gateway.receive({
|
|
307
314
|
conversation,
|
|
308
315
|
message: { conversation, id: msg.id },
|
|
309
|
-
content
|
|
316
|
+
content,
|
|
310
317
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
311
|
-
sender:
|
|
318
|
+
sender: {
|
|
319
|
+
id: msg.authorId,
|
|
320
|
+
name: senderDisplayName(msg) || undefined,
|
|
321
|
+
...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
|
|
322
|
+
},
|
|
323
|
+
endpointId: this.#options.config.id,
|
|
324
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
312
325
|
metadata: Object.freeze({
|
|
313
|
-
endpoint: this.#options.config.name,
|
|
314
326
|
channelKind: msg.channelKind,
|
|
315
327
|
userId: msg.authorId,
|
|
316
328
|
guildId: msg.guildId,
|
|
317
329
|
roles: msg.authorRoles,
|
|
318
|
-
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
319
|
-
...(msg.mentioned ? { mentioned: true } : {}),
|
|
320
330
|
}),
|
|
321
331
|
}).catch((err) => {
|
|
322
|
-
logger.warn(formatCompact({
|
|
332
|
+
this.#logger.warn(formatCompact({
|
|
323
333
|
op: 'qq_gateway_receive_failed',
|
|
324
334
|
target: `${conversation.kind}:${conversation.id}`,
|
|
325
335
|
error: err instanceof Error ? err.message : String(err),
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { formatInboundContent, formatOutboundText, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, type QqAdapterConfig, type QqChannelKind, type QqInboundMessage, type QqWireSegment, type ResolvedQqConfig, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig, } from './protocol.js';
|
|
2
|
+
export { DEFAULT_QQ_BOT_KIND, QQ_INTENTS_BY_KIND, defaultQqEndpointIntentFields, parseQqBotKind, resolveQqIntents, type QqBotKind, type QqIntent, } from './qq-intents.js';
|
|
2
3
|
export { formatOutbound, resolveMediaFile, type QqOutboundElem, type QqOutboundMessage, } from './outbound.js';
|
|
3
4
|
export { QqHttpEndpoint, QqWebsocketEndpoint, type CreateQqBot, type CreateQqHttpBot, type QqBotTransport, type QqEndpointOptions, type QqHttpBotTransport, type QqHttpEndpointOptions, } from './endpoint.js';
|
|
4
5
|
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, type QqWebhookHandler, } from './webhook.js';
|
|
5
6
|
export { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
|
|
6
7
|
export { getQqAgentDeps, registerQqAgentEndpoint, setQqAgentDeps, type QqAgentDeps, type QqAgentEndpoint, } from './qq-agent-deps.js';
|
|
7
|
-
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver,
|
|
8
|
+
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, } from './platform-permit.js';
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { formatInboundContent, formatOutboundText, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, } from './protocol.js';
|
|
2
|
+
export { DEFAULT_QQ_BOT_KIND, QQ_INTENTS_BY_KIND, defaultQqEndpointIntentFields, parseQqBotKind, resolveQqIntents, } from './qq-intents.js';
|
|
2
3
|
export { formatOutbound, resolveMediaFile, } from './outbound.js';
|
|
3
4
|
export { QqHttpEndpoint, QqWebsocketEndpoint, } from './endpoint.js';
|
|
4
5
|
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, } from './webhook.js';
|
|
5
6
|
export { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
|
|
6
7
|
export { getQqAgentDeps, registerQqAgentEndpoint, setQqAgentDeps, } from './qq-agent-deps.js';
|
|
7
|
-
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver,
|
|
8
|
+
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, } from './platform-permit.js';
|
package/lib/platform-permit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import type { PermissionSubject } from '@zhin.js/permission';
|
|
5
5
|
export declare function platformPermit(perm: string): string;
|
|
6
6
|
export declare function qqGuildPermitResolver(logicalPerm: string): string;
|
|
7
7
|
export declare function normalizeQqGuildSenderForPermit(input: {
|
|
@@ -12,5 +12,4 @@ export declare function normalizeQqGuildSenderForPermit(input: {
|
|
|
12
12
|
role?: string;
|
|
13
13
|
permissions?: string[];
|
|
14
14
|
};
|
|
15
|
-
export declare function checkQqPlatformPermit(perm: string,
|
|
16
|
-
export declare function registerQqPlatformPermitChecker(): () => void;
|
|
15
|
+
export declare function checkQqPlatformPermit(perm: string, subject: PermissionSubject): boolean;
|
package/lib/platform-permit.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
|
|
3
|
-
*/
|
|
4
|
-
import { registerPlatformPermitChecker } from '@zhin.js/core';
|
|
5
1
|
const ADAPTER = 'qq';
|
|
6
2
|
export function platformPermit(perm) {
|
|
7
3
|
return `platform(${ADAPTER},${perm})`;
|
|
@@ -26,10 +22,9 @@ export function normalizeQqGuildSenderForPermit(input) {
|
|
|
26
22
|
}
|
|
27
23
|
return { role: 'member', permissions };
|
|
28
24
|
}
|
|
29
|
-
export function checkQqPlatformPermit(perm,
|
|
30
|
-
const
|
|
31
|
-
const permissions = sender
|
|
32
|
-
const role = sender.role;
|
|
25
|
+
export function checkQqPlatformPermit(perm, subject) {
|
|
26
|
+
const role = subject.sender?.role?.[0];
|
|
27
|
+
const permissions = subject.sender?.permissions ?? [];
|
|
33
28
|
const has = (t) => permissions.includes(t);
|
|
34
29
|
switch (perm) {
|
|
35
30
|
case 'guild_owner':
|
|
@@ -44,6 +39,3 @@ export function checkQqPlatformPermit(perm, message) {
|
|
|
44
39
|
return false;
|
|
45
40
|
}
|
|
46
41
|
}
|
|
47
|
-
export function registerQqPlatformPermitChecker() {
|
|
48
|
-
return registerPlatformPermitChecker(ADAPTER, checkQqPlatformPermit);
|
|
49
|
-
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
6
6
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
7
7
|
export interface QqAdapterConfig {
|
|
8
|
-
readonly
|
|
8
|
+
readonly id?: string;
|
|
9
9
|
readonly appid?: string;
|
|
10
10
|
readonly secret?: string;
|
|
11
11
|
/** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
|
|
12
12
|
readonly mode?: 'websocket' | 'webhook' | 'middleware';
|
|
13
13
|
readonly sandbox?: boolean;
|
|
14
|
+
/** Explicit intents; when omitted, derived from `botKind` (default public). */
|
|
14
15
|
readonly intents?: readonly string[];
|
|
16
|
+
/** public | private — drives default intents when `intents` omitted. */
|
|
17
|
+
readonly botKind?: string;
|
|
15
18
|
readonly accessTokenUrl?: string;
|
|
16
19
|
readonly gatewayUrl?: string;
|
|
17
20
|
readonly webhookPath?: string;
|
|
@@ -20,12 +23,13 @@ export interface QqAdapterConfig {
|
|
|
20
23
|
/** Transitional: legacy root `endpoints[]` with `context: qq`. */
|
|
21
24
|
readonly endpoints?: ReadonlyArray<{
|
|
22
25
|
readonly context?: string;
|
|
23
|
-
readonly
|
|
26
|
+
readonly id?: string;
|
|
24
27
|
readonly appid?: string;
|
|
25
28
|
readonly secret?: string;
|
|
26
29
|
readonly mode?: 'websocket' | 'webhook' | 'middleware';
|
|
27
30
|
readonly sandbox?: boolean;
|
|
28
31
|
readonly intents?: readonly string[];
|
|
32
|
+
readonly botKind?: string;
|
|
29
33
|
readonly accessTokenUrl?: string;
|
|
30
34
|
readonly gatewayUrl?: string;
|
|
31
35
|
readonly webhookPath?: string;
|
|
@@ -36,7 +40,7 @@ export interface QqAdapterConfig {
|
|
|
36
40
|
export interface ResolvedQqWebsocketConfig {
|
|
37
41
|
readonly context: 'qq';
|
|
38
42
|
readonly mode: 'websocket';
|
|
39
|
-
readonly
|
|
43
|
+
readonly id: string;
|
|
40
44
|
readonly appid: string;
|
|
41
45
|
readonly secret: string;
|
|
42
46
|
readonly sandbox: boolean;
|
|
@@ -47,7 +51,7 @@ export interface ResolvedQqWebsocketConfig {
|
|
|
47
51
|
export interface ResolvedQqHttpConfig {
|
|
48
52
|
readonly context: 'qq';
|
|
49
53
|
readonly mode: 'webhook' | 'middleware';
|
|
50
|
-
readonly
|
|
54
|
+
readonly id: string;
|
|
51
55
|
readonly appid: string;
|
|
52
56
|
readonly secret: string;
|
|
53
57
|
readonly webhookPath: string;
|
|
@@ -85,7 +89,7 @@ export declare function resolveQqConfig(config?: QqAdapterConfig): ResolvedQqCon
|
|
|
85
89
|
* 入站归一化 → ConversationRef:`direct`(频道私信)映射为 guild 容器内的
|
|
86
90
|
* private 会话;频道消息的所属 guild 进 `parent`。
|
|
87
91
|
*/
|
|
88
|
-
export declare function qqInboundConversation(
|
|
92
|
+
export declare function qqInboundConversation(endpointKey: string, msg: QqInboundMessage): ConversationRef;
|
|
89
93
|
/** 出站:guild 容器内的 private 会话即 QQ `direct`(频道私信)。 */
|
|
90
94
|
export declare function qqOutboundKind(conversation: ConversationRef): QqChannelKind;
|
|
91
95
|
export declare function senderDisplayName(msg: QqInboundMessage): string;
|