@zhin.js/adapter-qq 7.0.13 → 7.0.14
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 +40 -0
- package/README.md +7 -2
- package/adapters/qq.js +6 -1
- package/adapters/qq.ts +12 -1
- package/lib/endpoint.d.ts +3 -1
- package/lib/endpoint.js +7 -0
- package/lib/outbound.js +2 -2
- package/lib/side-event-dispatch.d.ts +13 -0
- package/lib/side-event-dispatch.js +93 -0
- package/lib/webhook.js +3 -0
- package/lib/ws.d.ts +5 -0
- package/lib/ws.js +3 -0
- package/package.json +15 -15
- package/src/endpoint.ts +28 -1
- package/src/outbound.ts +3 -2
- package/src/side-event-dispatch.ts +120 -0
- package/src/webhook.ts +4 -0
- package/src/ws.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @zhin.js/adapter-qq
|
|
2
2
|
|
|
3
|
+
## 7.0.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 974772e: Replace the user-facing `Prompt` vocabulary with the `UserInteraction` authoring surface for input, confirmation, and selection. Commands and handlers now expose `interaction`; IM Runtime exposes `createInteraction`; schema-driven endpoint collection is named `SchemaInteraction`. The old prompt-named interaction types and properties are removed rather than aliased. User interactions render through one canonical Markdown and keyboard/list presentation module shared by commands and Agent `ask_user` turns.
|
|
8
|
+
|
|
9
|
+
Extract the transport-neutral interaction contract into `@zhin.js/interaction`. A discriminated `ask()` API supports text, number, confirmation, single-select, multi-select, and typed lists with structured `title`, `description`, and `tip` content. Typed `sequence()` interactions return one result object keyed by step id, render progress, and retry invalid replies without leaking invalid values to callers.
|
|
10
|
+
|
|
11
|
+
Preserve AI Markdown and card command actions through outbound publishing. QQ delivers Markdown with native command buttons; KOOK, Discord, Telegram, DingTalk, and Lark/Feishu now declare and encode their native Markdown dialects while retaining each adapter's interaction policy. Correct QQ callback button action encoding and button style mapping.
|
|
12
|
+
|
|
13
|
+
- 5969c5b: Add SideEventGateway so adapters forward notice/request/system into HandlerIndex. HandlerContext now exposes only generation-safe capabilities and prompt ports; live Endpoint escape hatches are removed.
|
|
14
|
+
- Updated dependencies [5969c5b]
|
|
15
|
+
- Updated dependencies [d336a3f]
|
|
16
|
+
- Updated dependencies [0c82a7e]
|
|
17
|
+
- Updated dependencies [b9217e4]
|
|
18
|
+
- Updated dependencies [5969c5b]
|
|
19
|
+
- Updated dependencies [5969c5b]
|
|
20
|
+
- Updated dependencies [974772e]
|
|
21
|
+
- Updated dependencies [5969c5b]
|
|
22
|
+
- Updated dependencies [5969c5b]
|
|
23
|
+
- Updated dependencies [2f786bd]
|
|
24
|
+
- Updated dependencies [63d89f9]
|
|
25
|
+
- Updated dependencies [71c7cdd]
|
|
26
|
+
- Updated dependencies [3cca0ea]
|
|
27
|
+
- Updated dependencies [1312ca0]
|
|
28
|
+
- Updated dependencies [985fa22]
|
|
29
|
+
- Updated dependencies [04b861d]
|
|
30
|
+
- Updated dependencies [a23d544]
|
|
31
|
+
- Updated dependencies [8cddabf]
|
|
32
|
+
- Updated dependencies [dbe5081]
|
|
33
|
+
- @zhin.js/im-contract@1.0.4
|
|
34
|
+
- @zhin.js/core@1.5.12
|
|
35
|
+
- @zhin.js/adapter@1.1.11
|
|
36
|
+
- @zhin.js/agent@1.1.14
|
|
37
|
+
- @zhin.js/host-http@1.0.11
|
|
38
|
+
- @zhin.js/command@1.0.15
|
|
39
|
+
- zhin.js@6.0.12
|
|
40
|
+
- @zhin.js/middleware@1.0.12
|
|
41
|
+
- @zhin.js/permission@1.0.3
|
|
42
|
+
|
|
3
43
|
## 7.0.13
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -135,9 +135,14 @@ master 可执行;未配置则放行(首个扫码绑定者会写入该 endpoi
|
|
|
135
135
|
|
|
136
136
|
platform permit checker 由 `plugin.ts` 的 generation 生命周期注册;`@zhin.js/tool` descriptor 保留 `platforms` / `scopes` / `permissions`,CapabilityIngress 与 ToolSystem 统一经 Core `canAccessTool()` 执行门禁。
|
|
137
137
|
|
|
138
|
-
##
|
|
138
|
+
## Markdown 与交互输出
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
QQ Endpoint 声明原生 `markdown` / `keyboard` 能力。普通 AI Markdown 会保留为 QQ Markdown
|
|
141
|
+
消息;命令或 Agent 通过 `UserInteraction.ask()` 发起 `confirm` / `select` 时,会生成
|
|
142
|
+
Markdown + 指令按钮组。按钮值与手动输入共用同一解析入口,不维护 QQ 专用交互状态。
|
|
143
|
+
|
|
144
|
+
不支持原生按钮的 Adapter 会在 Core 中把同一 keyboard 语义降级为编号列表,并保存数字到
|
|
145
|
+
payload 的回跳映射。图片、音频、视频和文件仍沿 canonical `MediaRef` 出站链投递。
|
|
141
146
|
|
|
142
147
|
## 许可证
|
|
143
148
|
|
package/adapters/qq.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Convention entry: discover `adapters/qq.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
6
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
7
7
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
8
8
|
import { QqHttpEndpoint, QqWebsocketEndpoint } from "../lib/endpoint.js";
|
|
9
9
|
import { resolveQqConfig, } from "../lib/protocol.js";
|
|
@@ -14,12 +14,15 @@ export default defineAdapter({
|
|
|
14
14
|
// 媒体 url 直发,base64/path 由 SDK formatMediaData 物化走 /files 上传;
|
|
15
15
|
// markdown/keyboard 原生按钮承载交互段。
|
|
16
16
|
segments: {
|
|
17
|
+
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'markdown', 'keyboard'],
|
|
18
|
+
markdown: 'native',
|
|
17
19
|
outboundMedia: ['url', 'upload'],
|
|
18
20
|
interactive: 'native',
|
|
19
21
|
},
|
|
20
22
|
create(context) {
|
|
21
23
|
const config = resolveQqConfig(context.config);
|
|
22
24
|
const gateway = context.use(messageGatewayToken);
|
|
25
|
+
const sideEvents = context.use(sideEventGatewayToken);
|
|
23
26
|
// 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
|
|
24
27
|
const state = context.use(qqRuntimeStateToken);
|
|
25
28
|
state.endpoints.set(config.id, {
|
|
@@ -32,11 +35,13 @@ export default defineAdapter({
|
|
|
32
35
|
? new QqWebsocketEndpoint({
|
|
33
36
|
id: context.id,
|
|
34
37
|
gateway,
|
|
38
|
+
sideEvents,
|
|
35
39
|
config,
|
|
36
40
|
})
|
|
37
41
|
: new QqHttpEndpoint({
|
|
38
42
|
id: context.id,
|
|
39
43
|
gateway,
|
|
44
|
+
sideEvents,
|
|
40
45
|
http: context.use(httpHostToken),
|
|
41
46
|
config,
|
|
42
47
|
});
|
package/adapters/qq.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Convention entry: discover `adapters/qq.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
5
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
6
6
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
7
|
import { QqHttpEndpoint, QqWebsocketEndpoint } from '../src/endpoint.js';
|
|
8
8
|
import {
|
|
@@ -24,17 +24,26 @@ export type {
|
|
|
24
24
|
QqHttpEndpointOptions,
|
|
25
25
|
} from '../src/endpoint.js';
|
|
26
26
|
|
|
27
|
+
declare module '@zhin.js/core' {
|
|
28
|
+
interface AdapterEndpoints {
|
|
29
|
+
qq: import('../src/endpoint.js').QqWebsocketEndpoint;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
export default defineAdapter<QqAdapterConfig>({
|
|
28
34
|
capabilities: ['inbound', 'outbound'],
|
|
29
35
|
// 媒体 url 直发,base64/path 由 SDK formatMediaData 物化走 /files 上传;
|
|
30
36
|
// markdown/keyboard 原生按钮承载交互段。
|
|
31
37
|
segments: {
|
|
38
|
+
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'markdown', 'keyboard'],
|
|
39
|
+
markdown: 'native',
|
|
32
40
|
outboundMedia: ['url', 'upload'],
|
|
33
41
|
interactive: 'native',
|
|
34
42
|
},
|
|
35
43
|
create(context) {
|
|
36
44
|
const config = resolveQqConfig(context.config);
|
|
37
45
|
const gateway = context.use(messageGatewayToken);
|
|
46
|
+
const sideEvents = context.use(sideEventGatewayToken);
|
|
38
47
|
// 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
|
|
39
48
|
const state = context.use(qqRuntimeStateToken);
|
|
40
49
|
state.endpoints.set(config.id, {
|
|
@@ -47,11 +56,13 @@ export default defineAdapter<QqAdapterConfig>({
|
|
|
47
56
|
? new QqWebsocketEndpoint({
|
|
48
57
|
id: context.id,
|
|
49
58
|
gateway,
|
|
59
|
+
sideEvents,
|
|
50
60
|
config,
|
|
51
61
|
})
|
|
52
62
|
: new QqHttpEndpoint({
|
|
53
63
|
id: context.id,
|
|
54
64
|
gateway,
|
|
65
|
+
sideEvents,
|
|
55
66
|
http: context.use(httpHostToken),
|
|
56
67
|
config,
|
|
57
68
|
});
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
3
3
|
*/
|
|
4
4
|
import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
|
|
5
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
5
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
6
6
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
7
7
|
import type { CapabilityId } from 'zhin.js';
|
|
8
8
|
import { type QqInboundMessage, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig } from './protocol.js';
|
|
@@ -13,6 +13,7 @@ export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
|
13
13
|
export interface QqEndpointOptions {
|
|
14
14
|
readonly id: CapabilityId;
|
|
15
15
|
readonly gateway: MessageGateway;
|
|
16
|
+
readonly sideEvents?: SideEventGateway;
|
|
16
17
|
readonly config: ResolvedQqWebsocketConfig;
|
|
17
18
|
readonly createBot?: CreateQqBot;
|
|
18
19
|
}
|
|
@@ -42,6 +43,7 @@ export declare class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
42
43
|
export interface QqHttpEndpointOptions {
|
|
43
44
|
readonly id: CapabilityId;
|
|
44
45
|
readonly gateway: MessageGateway;
|
|
46
|
+
readonly sideEvents?: SideEventGateway;
|
|
45
47
|
readonly http: HttpHost;
|
|
46
48
|
readonly config: ResolvedQqHttpConfig;
|
|
47
49
|
readonly createBot?: CreateQqHttpBot;
|
package/lib/endpoint.js
CHANGED
|
@@ -4,6 +4,7 @@ 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
|
+
import { bindQqBotSideEvents, receiveQqSideEvent, } from './side-event-dispatch.js';
|
|
7
8
|
export class QqWebsocketEndpoint {
|
|
8
9
|
#logger;
|
|
9
10
|
#options;
|
|
@@ -180,6 +181,9 @@ export class QqWebsocketEndpoint {
|
|
|
180
181
|
if (msg)
|
|
181
182
|
this.admit(msg);
|
|
182
183
|
});
|
|
184
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
185
|
+
receiveQqSideEvent(this.#options.sideEvents, this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
186
|
+
});
|
|
183
187
|
}
|
|
184
188
|
#requireBot() {
|
|
185
189
|
if (!this.#bot)
|
|
@@ -372,6 +376,9 @@ export class QqHttpEndpoint {
|
|
|
372
376
|
if (msg)
|
|
373
377
|
this.admit(msg);
|
|
374
378
|
});
|
|
379
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
380
|
+
receiveQqSideEvent(this.#options.sideEvents, this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
381
|
+
});
|
|
375
382
|
}
|
|
376
383
|
#requireBot() {
|
|
377
384
|
if (!this.#bot)
|
package/lib/outbound.js
CHANGED
|
@@ -82,7 +82,7 @@ function normalizeMarkdownSegment(seg) {
|
|
|
82
82
|
function coreButtonToQq(btn) {
|
|
83
83
|
const isCommand = btn.mode === 'command';
|
|
84
84
|
const action = {
|
|
85
|
-
type: isCommand ? 2 :
|
|
85
|
+
type: isCommand ? 2 : 0,
|
|
86
86
|
permission: { type: 2 },
|
|
87
87
|
data: btn.payload ?? '',
|
|
88
88
|
click_limit: btn.disabled ? 0 : 10,
|
|
@@ -99,7 +99,7 @@ function coreButtonToQq(btn) {
|
|
|
99
99
|
render_data: {
|
|
100
100
|
label: btn.label ?? '',
|
|
101
101
|
visited_label: btn.label ?? '',
|
|
102
|
-
style: 0,
|
|
102
|
+
style: btn.style === 'primary' ? 1 : btn.style === 'danger' ? 2 : 0,
|
|
103
103
|
},
|
|
104
104
|
action,
|
|
105
105
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SideEventGateway } from '@zhin.js/core/runtime';
|
|
2
|
+
import { type getAdapterLogger } from '@zhin.js/logger';
|
|
3
|
+
export interface QqSideEventCaller {
|
|
4
|
+
approveGroupJoinRequest?(groupId: string, userId: string, options: {
|
|
5
|
+
op: 'approve' | 'reject';
|
|
6
|
+
join_request_id: string;
|
|
7
|
+
reason?: string;
|
|
8
|
+
}): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
export declare function bindQqBotSideEvents(bot: {
|
|
11
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
12
|
+
}, dispatch: (eventName: string, raw: unknown) => void): void;
|
|
13
|
+
export declare function receiveQqSideEvent(sideEvents: SideEventGateway | undefined, endpointKey: string, caller: QqSideEventCaller, eventName: string, raw: unknown, logger: ReturnType<typeof getAdapterLogger>): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { receiveOneBotLikeSideEvent } from '@zhin.js/core';
|
|
2
|
+
import { formatCompact } from '@zhin.js/logger';
|
|
3
|
+
const QQ_SIDE_EVENT_NAMES = [
|
|
4
|
+
'notice.group.join_request',
|
|
5
|
+
'notice.group.member',
|
|
6
|
+
'notice.group.member.increase',
|
|
7
|
+
'notice.group.member.decrease',
|
|
8
|
+
'notice.group.increase',
|
|
9
|
+
'notice.group.decrease',
|
|
10
|
+
'notice.guild.member',
|
|
11
|
+
'notice.guild.member.increase',
|
|
12
|
+
'notice.guild.member.decrease',
|
|
13
|
+
];
|
|
14
|
+
export function bindQqBotSideEvents(bot, dispatch) {
|
|
15
|
+
for (const name of QQ_SIDE_EVENT_NAMES) {
|
|
16
|
+
bot.on(name, (raw) => dispatch(name, raw));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function toOneBotLikeRaw(eventName, raw) {
|
|
20
|
+
if (!raw || typeof raw !== 'object')
|
|
21
|
+
return null;
|
|
22
|
+
const record = raw;
|
|
23
|
+
const asId = (value) => {
|
|
24
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
25
|
+
return value;
|
|
26
|
+
return undefined;
|
|
27
|
+
};
|
|
28
|
+
const asTime = (value) => {
|
|
29
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
30
|
+
return Number.isFinite(n) && n > 0 ? n : Date.now();
|
|
31
|
+
};
|
|
32
|
+
if (eventName === 'notice.group.join_request') {
|
|
33
|
+
return {
|
|
34
|
+
...record,
|
|
35
|
+
post_type: 'request',
|
|
36
|
+
request_type: 'group',
|
|
37
|
+
flag: record.join_request_id != null ? String(record.join_request_id) : undefined,
|
|
38
|
+
user_id: asId(record.user_id),
|
|
39
|
+
group_id: asId(record.group_id),
|
|
40
|
+
comment: record.verify_info != null ? String(record.verify_info) : undefined,
|
|
41
|
+
time: asTime(record.timestamp),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (eventName.startsWith('notice.')) {
|
|
45
|
+
return {
|
|
46
|
+
...record,
|
|
47
|
+
post_type: eventName,
|
|
48
|
+
notice_type: eventName.replace(/^notice\./, ''),
|
|
49
|
+
time: asTime(record.timestamp),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function receiveQqSideEvent(sideEvents, endpointKey, caller, eventName, raw, logger) {
|
|
55
|
+
if (!sideEvents)
|
|
56
|
+
return;
|
|
57
|
+
const mapped = toOneBotLikeRaw(eventName, raw);
|
|
58
|
+
if (!mapped)
|
|
59
|
+
return;
|
|
60
|
+
const isJoinRequest = eventName === 'notice.group.join_request';
|
|
61
|
+
const joinRaw = isJoinRequest && raw && typeof raw === 'object'
|
|
62
|
+
? raw
|
|
63
|
+
: undefined;
|
|
64
|
+
void receiveOneBotLikeSideEvent(sideEvents, {
|
|
65
|
+
adapter: 'qq',
|
|
66
|
+
endpointKey,
|
|
67
|
+
platform: 'qq',
|
|
68
|
+
raw: mapped,
|
|
69
|
+
...(isJoinRequest && caller.approveGroupJoinRequest && joinRaw ? {
|
|
70
|
+
approve: async (flag, remark) => {
|
|
71
|
+
await caller.approveGroupJoinRequest(String(joinRaw.group_id ?? ''), String(joinRaw.user_id ?? ''), {
|
|
72
|
+
op: 'approve',
|
|
73
|
+
join_request_id: String(joinRaw.join_request_id ?? flag),
|
|
74
|
+
...(remark ? { reason: remark } : {}),
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
reject: async (flag, reason) => {
|
|
78
|
+
await caller.approveGroupJoinRequest(String(joinRaw.group_id ?? ''), String(joinRaw.user_id ?? ''), {
|
|
79
|
+
op: 'reject',
|
|
80
|
+
join_request_id: String(joinRaw.join_request_id ?? flag),
|
|
81
|
+
...(reason ? { reason } : {}),
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
} : {}),
|
|
85
|
+
}).catch((err) => {
|
|
86
|
+
logger.warn(formatCompact({
|
|
87
|
+
op: 'qq_side_event_failed',
|
|
88
|
+
endpoint: endpointKey,
|
|
89
|
+
event: eventName,
|
|
90
|
+
error: err instanceof Error ? err.message : String(err),
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
}
|
package/lib/webhook.js
CHANGED
|
@@ -94,6 +94,9 @@ export function defaultCreateHttpBot(config) {
|
|
|
94
94
|
await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
|
|
95
95
|
return true;
|
|
96
96
|
},
|
|
97
|
+
approveGroupJoinRequest: (groupId, userId, options) => bot
|
|
98
|
+
.approveGroupJoinRequest?.(groupId, userId, options)
|
|
99
|
+
?? Promise.reject(new Error('approveGroupJoinRequest is not available')),
|
|
97
100
|
middleware: (ctx, next) => bot.middleware(ctx, next),
|
|
98
101
|
};
|
|
99
102
|
}
|
package/lib/ws.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface QqBotTransport {
|
|
|
22
22
|
createGuildRole(guildId: string, name: string, color?: number): Promise<unknown>;
|
|
23
23
|
addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
|
|
24
24
|
removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
|
|
25
|
+
approveGroupJoinRequest?(groupId: string, userId: string, options: {
|
|
26
|
+
op: 'approve' | 'reject';
|
|
27
|
+
join_request_id: string;
|
|
28
|
+
reason?: string;
|
|
29
|
+
}): Promise<unknown>;
|
|
25
30
|
}
|
|
26
31
|
export type CreateQqBot = (config: ResolvedQqWebsocketConfig) => QqBotTransport;
|
|
27
32
|
interface QqOfficialBotLifecycle {
|
package/lib/ws.js
CHANGED
|
@@ -226,5 +226,8 @@ export function defaultCreateBot(config) {
|
|
|
226
226
|
await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
|
|
227
227
|
return true;
|
|
228
228
|
},
|
|
229
|
+
approveGroupJoinRequest: (groupId, userId, options) => bot
|
|
230
|
+
.approveGroupJoinRequest?.(groupId, userId, options)
|
|
231
|
+
?? Promise.reject(new Error('approveGroupJoinRequest is not available')),
|
|
229
232
|
};
|
|
230
233
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-qq",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.14",
|
|
4
4
|
"description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -35,21 +35,21 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"qq-official-bot": "^1.2.3",
|
|
38
|
-
"@zhin.js/adapter": "1.1.
|
|
39
|
-
"@zhin.js/core": "1.5.
|
|
40
|
-
"@zhin.js/host-http": "1.0.
|
|
41
|
-
"@zhin.js/im-contract": "1.0.
|
|
38
|
+
"@zhin.js/adapter": "1.1.11",
|
|
39
|
+
"@zhin.js/core": "1.5.12",
|
|
40
|
+
"@zhin.js/host-http": "1.0.11",
|
|
41
|
+
"@zhin.js/im-contract": "1.0.4",
|
|
42
42
|
"@zhin.js/logger": "1.0.76",
|
|
43
|
-
"@zhin.js/permission": "1.0.
|
|
43
|
+
"@zhin.js/permission": "1.0.3"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"zod": "^4.0.0",
|
|
47
|
-
"@zhin.js/adapter": "1.1.
|
|
48
|
-
"@zhin.js/agent": "1.1.
|
|
49
|
-
"@zhin.js/command": "1.0.
|
|
50
|
-
"@zhin.js/core": "1.5.
|
|
51
|
-
"@zhin.js/middleware": "1.0.
|
|
52
|
-
"zhin.js": "6.0.
|
|
47
|
+
"@zhin.js/adapter": "1.1.11",
|
|
48
|
+
"@zhin.js/agent": "1.1.14",
|
|
49
|
+
"@zhin.js/command": "1.0.15",
|
|
50
|
+
"@zhin.js/core": "1.5.12",
|
|
51
|
+
"@zhin.js/middleware": "1.0.12",
|
|
52
|
+
"zhin.js": "6.0.12"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@zhin.js/agent": {
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"typescript": "^6.0.3",
|
|
74
74
|
"vitest": "^4.1.10",
|
|
75
75
|
"zod": "^4.4.3",
|
|
76
|
-
"@zhin.js/agent": "1.1.
|
|
77
|
-
"@zhin.js/host-http": "1.0.
|
|
78
|
-
"zhin.js": "6.0.
|
|
76
|
+
"@zhin.js/agent": "1.1.14",
|
|
77
|
+
"@zhin.js/host-http": "1.0.11",
|
|
78
|
+
"zhin.js": "6.0.12"
|
|
79
79
|
},
|
|
80
80
|
"files": [
|
|
81
81
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
EndpointManagement,
|
|
8
8
|
EndpointSendRequest,
|
|
9
9
|
} from 'zhin.js/adapter';
|
|
10
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
10
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
11
11
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
12
12
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
13
13
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -37,6 +37,11 @@ import {
|
|
|
37
37
|
type CreateQqBot,
|
|
38
38
|
type QqBotTransport,
|
|
39
39
|
} from './ws.js';
|
|
40
|
+
import {
|
|
41
|
+
bindQqBotSideEvents,
|
|
42
|
+
receiveQqSideEvent,
|
|
43
|
+
type QqSideEventCaller,
|
|
44
|
+
} from './side-event-dispatch.js';
|
|
40
45
|
|
|
41
46
|
export type { CreateQqBot, QqBotTransport } from './ws.js';
|
|
42
47
|
export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
@@ -44,6 +49,7 @@ export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
|
44
49
|
export interface QqEndpointOptions {
|
|
45
50
|
readonly id: CapabilityId;
|
|
46
51
|
readonly gateway: MessageGateway;
|
|
52
|
+
readonly sideEvents?: SideEventGateway;
|
|
47
53
|
readonly config: ResolvedQqWebsocketConfig;
|
|
48
54
|
readonly createBot?: CreateQqBot;
|
|
49
55
|
}
|
|
@@ -246,6 +252,16 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
246
252
|
const msg = normalizeQqMessage(raw);
|
|
247
253
|
if (msg) this.admit(msg);
|
|
248
254
|
});
|
|
255
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
256
|
+
receiveQqSideEvent(
|
|
257
|
+
this.#options.sideEvents,
|
|
258
|
+
this.#options.config.id,
|
|
259
|
+
bot as QqSideEventCaller,
|
|
260
|
+
eventName,
|
|
261
|
+
raw,
|
|
262
|
+
this.#logger,
|
|
263
|
+
);
|
|
264
|
+
});
|
|
249
265
|
}
|
|
250
266
|
|
|
251
267
|
#requireBot(): QqBotTransport {
|
|
@@ -257,6 +273,7 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
257
273
|
export interface QqHttpEndpointOptions {
|
|
258
274
|
readonly id: CapabilityId;
|
|
259
275
|
readonly gateway: MessageGateway;
|
|
276
|
+
readonly sideEvents?: SideEventGateway;
|
|
260
277
|
readonly http: HttpHost;
|
|
261
278
|
readonly config: ResolvedQqHttpConfig;
|
|
262
279
|
readonly createBot?: CreateQqHttpBot;
|
|
@@ -469,6 +486,16 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
469
486
|
const msg = normalizeQqMessage(raw);
|
|
470
487
|
if (msg) this.admit(msg);
|
|
471
488
|
});
|
|
489
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
490
|
+
receiveQqSideEvent(
|
|
491
|
+
this.#options.sideEvents,
|
|
492
|
+
this.#options.config.id,
|
|
493
|
+
bot as QqSideEventCaller,
|
|
494
|
+
eventName,
|
|
495
|
+
raw,
|
|
496
|
+
this.#logger,
|
|
497
|
+
);
|
|
498
|
+
});
|
|
472
499
|
}
|
|
473
500
|
|
|
474
501
|
#requireBot(): QqBotTransport {
|
package/src/outbound.ts
CHANGED
|
@@ -31,6 +31,7 @@ interface QqOutboundButtonSpec {
|
|
|
31
31
|
readonly label?: string;
|
|
32
32
|
readonly payload?: string;
|
|
33
33
|
readonly disabled?: boolean;
|
|
34
|
+
readonly style?: 'primary' | 'danger' | 'secondary';
|
|
34
35
|
readonly mode?: string;
|
|
35
36
|
readonly command?: { readonly enter?: boolean; readonly reply?: boolean };
|
|
36
37
|
}
|
|
@@ -103,7 +104,7 @@ function normalizeMarkdownSegment(seg: QqWireSegment): QqOutboundElem | null {
|
|
|
103
104
|
function coreButtonToQq(btn: QqOutboundButtonSpec): Record<string, unknown> {
|
|
104
105
|
const isCommand = btn.mode === 'command';
|
|
105
106
|
const action: Record<string, unknown> = {
|
|
106
|
-
type: isCommand ? 2 :
|
|
107
|
+
type: isCommand ? 2 : 0,
|
|
107
108
|
permission: { type: 2 },
|
|
108
109
|
data: btn.payload ?? '',
|
|
109
110
|
click_limit: btn.disabled ? 0 : 10,
|
|
@@ -118,7 +119,7 @@ function coreButtonToQq(btn: QqOutboundButtonSpec): Record<string, unknown> {
|
|
|
118
119
|
render_data: {
|
|
119
120
|
label: btn.label ?? '',
|
|
120
121
|
visited_label: btn.label ?? '',
|
|
121
|
-
style: 0,
|
|
122
|
+
style: btn.style === 'primary' ? 1 : btn.style === 'danger' ? 2 : 0,
|
|
122
123
|
},
|
|
123
124
|
action,
|
|
124
125
|
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { receiveOneBotLikeSideEvent, type OneBotLikeRawEvent } from '@zhin.js/core';
|
|
2
|
+
import type { SideEventGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
|
|
4
|
+
|
|
5
|
+
export interface QqSideEventCaller {
|
|
6
|
+
approveGroupJoinRequest?(
|
|
7
|
+
groupId: string,
|
|
8
|
+
userId: string,
|
|
9
|
+
options: { op: 'approve' | 'reject'; join_request_id: string; reason?: string },
|
|
10
|
+
): Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const QQ_SIDE_EVENT_NAMES = [
|
|
14
|
+
'notice.group.join_request',
|
|
15
|
+
'notice.group.member',
|
|
16
|
+
'notice.group.member.increase',
|
|
17
|
+
'notice.group.member.decrease',
|
|
18
|
+
'notice.group.increase',
|
|
19
|
+
'notice.group.decrease',
|
|
20
|
+
'notice.guild.member',
|
|
21
|
+
'notice.guild.member.increase',
|
|
22
|
+
'notice.guild.member.decrease',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export function bindQqBotSideEvents(
|
|
26
|
+
bot: { on(event: string, listener: (...args: unknown[]) => void): void },
|
|
27
|
+
dispatch: (eventName: string, raw: unknown) => void,
|
|
28
|
+
): void {
|
|
29
|
+
for (const name of QQ_SIDE_EVENT_NAMES) {
|
|
30
|
+
bot.on(name, (raw) => dispatch(name, raw));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toOneBotLikeRaw(eventName: string, raw: unknown): OneBotLikeRawEvent | null {
|
|
35
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
36
|
+
const record = raw as Record<string, unknown>;
|
|
37
|
+
const asId = (value: unknown): string | number | undefined => {
|
|
38
|
+
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
41
|
+
const asTime = (value: unknown): number => {
|
|
42
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
43
|
+
return Number.isFinite(n) && n > 0 ? n : Date.now();
|
|
44
|
+
};
|
|
45
|
+
if (eventName === 'notice.group.join_request') {
|
|
46
|
+
return {
|
|
47
|
+
...record,
|
|
48
|
+
post_type: 'request',
|
|
49
|
+
request_type: 'group',
|
|
50
|
+
flag: record.join_request_id != null ? String(record.join_request_id) : undefined,
|
|
51
|
+
user_id: asId(record.user_id),
|
|
52
|
+
group_id: asId(record.group_id),
|
|
53
|
+
comment: record.verify_info != null ? String(record.verify_info) : undefined,
|
|
54
|
+
time: asTime(record.timestamp),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (eventName.startsWith('notice.')) {
|
|
58
|
+
return {
|
|
59
|
+
...record,
|
|
60
|
+
post_type: eventName,
|
|
61
|
+
notice_type: eventName.replace(/^notice\./, ''),
|
|
62
|
+
time: asTime(record.timestamp),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function receiveQqSideEvent(
|
|
69
|
+
sideEvents: SideEventGateway | undefined,
|
|
70
|
+
endpointKey: string,
|
|
71
|
+
caller: QqSideEventCaller,
|
|
72
|
+
eventName: string,
|
|
73
|
+
raw: unknown,
|
|
74
|
+
logger: ReturnType<typeof getAdapterLogger>,
|
|
75
|
+
): void {
|
|
76
|
+
if (!sideEvents) return;
|
|
77
|
+
const mapped = toOneBotLikeRaw(eventName, raw);
|
|
78
|
+
if (!mapped) return;
|
|
79
|
+
const isJoinRequest = eventName === 'notice.group.join_request';
|
|
80
|
+
const joinRaw = isJoinRequest && raw && typeof raw === 'object'
|
|
81
|
+
? raw as { group_id?: string | number; user_id?: string | number; join_request_id?: string | number }
|
|
82
|
+
: undefined;
|
|
83
|
+
void receiveOneBotLikeSideEvent(sideEvents, {
|
|
84
|
+
adapter: 'qq',
|
|
85
|
+
endpointKey,
|
|
86
|
+
platform: 'qq',
|
|
87
|
+
raw: mapped,
|
|
88
|
+
...(isJoinRequest && caller.approveGroupJoinRequest && joinRaw ? {
|
|
89
|
+
approve: async (flag, remark) => {
|
|
90
|
+
await caller.approveGroupJoinRequest!(
|
|
91
|
+
String(joinRaw.group_id ?? ''),
|
|
92
|
+
String(joinRaw.user_id ?? ''),
|
|
93
|
+
{
|
|
94
|
+
op: 'approve',
|
|
95
|
+
join_request_id: String(joinRaw.join_request_id ?? flag),
|
|
96
|
+
...(remark ? { reason: remark } : {}),
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
reject: async (flag, reason) => {
|
|
101
|
+
await caller.approveGroupJoinRequest!(
|
|
102
|
+
String(joinRaw.group_id ?? ''),
|
|
103
|
+
String(joinRaw.user_id ?? ''),
|
|
104
|
+
{
|
|
105
|
+
op: 'reject',
|
|
106
|
+
join_request_id: String(joinRaw.join_request_id ?? flag),
|
|
107
|
+
...(reason ? { reason } : {}),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
} : {}),
|
|
112
|
+
}).catch((err) => {
|
|
113
|
+
logger.warn(formatCompact({
|
|
114
|
+
op: 'qq_side_event_failed',
|
|
115
|
+
endpoint: endpointKey,
|
|
116
|
+
event: eventName,
|
|
117
|
+
error: err instanceof Error ? err.message : String(err),
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
}
|
package/src/webhook.ts
CHANGED
|
@@ -122,6 +122,10 @@ export function defaultCreateHttpBot(config: ResolvedQqHttpConfig): QqHttpBotTra
|
|
|
122
122
|
await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
|
|
123
123
|
return true;
|
|
124
124
|
},
|
|
125
|
+
approveGroupJoinRequest: (groupId, userId, options) =>
|
|
126
|
+
(bot as { approveGroupJoinRequest?: QqBotTransport['approveGroupJoinRequest'] })
|
|
127
|
+
.approveGroupJoinRequest?.(groupId, userId, options)
|
|
128
|
+
?? Promise.reject(new Error('approveGroupJoinRequest is not available')),
|
|
125
129
|
middleware: (ctx, next) => bot.middleware(ctx as never, next as never),
|
|
126
130
|
};
|
|
127
131
|
}
|
package/src/ws.ts
CHANGED
|
@@ -39,6 +39,11 @@ export interface QqBotTransport {
|
|
|
39
39
|
userId: string,
|
|
40
40
|
roleId: string,
|
|
41
41
|
): Promise<boolean>;
|
|
42
|
+
approveGroupJoinRequest?(
|
|
43
|
+
groupId: string,
|
|
44
|
+
userId: string,
|
|
45
|
+
options: { op: 'approve' | 'reject'; join_request_id: string; reason?: string },
|
|
46
|
+
): Promise<unknown>;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export type CreateQqBot = (config: ResolvedQqWebsocketConfig) => QqBotTransport;
|
|
@@ -288,5 +293,9 @@ export function defaultCreateBot(config: ResolvedQqWebsocketConfig): QqBotTransp
|
|
|
288
293
|
await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
|
|
289
294
|
return true;
|
|
290
295
|
},
|
|
296
|
+
approveGroupJoinRequest: (groupId, userId, options) =>
|
|
297
|
+
(bot as { approveGroupJoinRequest?: QqBotTransport['approveGroupJoinRequest'] })
|
|
298
|
+
.approveGroupJoinRequest?.(groupId, userId, options)
|
|
299
|
+
?? Promise.reject(new Error('approveGroupJoinRequest is not available')),
|
|
291
300
|
};
|
|
292
301
|
}
|