@zhin.js/adapter-qq 7.0.13 → 8.0.0
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 +54 -0
- package/README.md +7 -2
- package/adapters/qq.js +7 -1
- package/adapters/qq.ts +13 -1
- package/lib/endpoint.d.ts +6 -2
- package/lib/endpoint.js +13 -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 +37 -6
- 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,59 @@
|
|
|
1
1
|
# @zhin.js/adapter-qq
|
|
2
2
|
|
|
3
|
+
## 8.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f2c532f: Expose exact per-Endpoint message operations through one validated Adapter capability model, route Core control calls through declared active capabilities, and connect existing recall, edit, reaction, and typing implementations across platform adapters.
|
|
8
|
+
- Updated dependencies [b10d058]
|
|
9
|
+
- Updated dependencies [f2c532f]
|
|
10
|
+
- Updated dependencies [3dbf990]
|
|
11
|
+
- @zhin.js/host-http@1.0.12
|
|
12
|
+
- @zhin.js/adapter@1.2.0
|
|
13
|
+
- @zhin.js/core@1.5.13
|
|
14
|
+
- @zhin.js/agent@1.1.15
|
|
15
|
+
- zhin.js@6.0.13
|
|
16
|
+
|
|
17
|
+
## 7.0.14
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
- 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.
|
|
28
|
+
- Updated dependencies [5969c5b]
|
|
29
|
+
- Updated dependencies [d336a3f]
|
|
30
|
+
- Updated dependencies [0c82a7e]
|
|
31
|
+
- Updated dependencies [b9217e4]
|
|
32
|
+
- Updated dependencies [5969c5b]
|
|
33
|
+
- Updated dependencies [5969c5b]
|
|
34
|
+
- Updated dependencies [974772e]
|
|
35
|
+
- Updated dependencies [5969c5b]
|
|
36
|
+
- Updated dependencies [5969c5b]
|
|
37
|
+
- Updated dependencies [2f786bd]
|
|
38
|
+
- Updated dependencies [63d89f9]
|
|
39
|
+
- Updated dependencies [71c7cdd]
|
|
40
|
+
- Updated dependencies [3cca0ea]
|
|
41
|
+
- Updated dependencies [1312ca0]
|
|
42
|
+
- Updated dependencies [985fa22]
|
|
43
|
+
- Updated dependencies [04b861d]
|
|
44
|
+
- Updated dependencies [a23d544]
|
|
45
|
+
- Updated dependencies [8cddabf]
|
|
46
|
+
- Updated dependencies [dbe5081]
|
|
47
|
+
- @zhin.js/im-contract@1.0.4
|
|
48
|
+
- @zhin.js/core@1.5.12
|
|
49
|
+
- @zhin.js/adapter@1.1.11
|
|
50
|
+
- @zhin.js/agent@1.1.14
|
|
51
|
+
- @zhin.js/host-http@1.0.11
|
|
52
|
+
- @zhin.js/command@1.0.15
|
|
53
|
+
- zhin.js@6.0.12
|
|
54
|
+
- @zhin.js/middleware@1.0.12
|
|
55
|
+
- @zhin.js/permission@1.0.3
|
|
56
|
+
|
|
3
57
|
## 7.0.13
|
|
4
58
|
|
|
5
59
|
### 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";
|
|
@@ -11,15 +11,19 @@ import { qqRuntimeStateToken } from "../lib/qq-runtime-state.js";
|
|
|
11
11
|
export { QqHttpEndpoint, QqWebsocketEndpoint, } from "../lib/endpoint.js";
|
|
12
12
|
export default defineAdapter({
|
|
13
13
|
capabilities: ['inbound', 'outbound'],
|
|
14
|
+
operations: ['recall'],
|
|
14
15
|
// 媒体 url 直发,base64/path 由 SDK formatMediaData 物化走 /files 上传;
|
|
15
16
|
// markdown/keyboard 原生按钮承载交互段。
|
|
16
17
|
segments: {
|
|
18
|
+
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'markdown', 'keyboard'],
|
|
19
|
+
markdown: 'native',
|
|
17
20
|
outboundMedia: ['url', 'upload'],
|
|
18
21
|
interactive: 'native',
|
|
19
22
|
},
|
|
20
23
|
create(context) {
|
|
21
24
|
const config = resolveQqConfig(context.config);
|
|
22
25
|
const gateway = context.use(messageGatewayToken);
|
|
26
|
+
const sideEvents = context.use(sideEventGatewayToken);
|
|
23
27
|
// 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
|
|
24
28
|
const state = context.use(qqRuntimeStateToken);
|
|
25
29
|
state.endpoints.set(config.id, {
|
|
@@ -32,11 +36,13 @@ export default defineAdapter({
|
|
|
32
36
|
? new QqWebsocketEndpoint({
|
|
33
37
|
id: context.id,
|
|
34
38
|
gateway,
|
|
39
|
+
sideEvents,
|
|
35
40
|
config,
|
|
36
41
|
})
|
|
37
42
|
: new QqHttpEndpoint({
|
|
38
43
|
id: context.id,
|
|
39
44
|
gateway,
|
|
45
|
+
sideEvents,
|
|
40
46
|
http: context.use(httpHostToken),
|
|
41
47
|
config,
|
|
42
48
|
});
|
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,27 @@ 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'],
|
|
35
|
+
operations: ['recall'],
|
|
29
36
|
// 媒体 url 直发,base64/path 由 SDK formatMediaData 物化走 /files 上传;
|
|
30
37
|
// markdown/keyboard 原生按钮承载交互段。
|
|
31
38
|
segments: {
|
|
39
|
+
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'markdown', 'keyboard'],
|
|
40
|
+
markdown: 'native',
|
|
32
41
|
outboundMedia: ['url', 'upload'],
|
|
33
42
|
interactive: 'native',
|
|
34
43
|
},
|
|
35
44
|
create(context) {
|
|
36
45
|
const config = resolveQqConfig(context.config);
|
|
37
46
|
const gateway = context.use(messageGatewayToken);
|
|
47
|
+
const sideEvents = context.use(sideEventGatewayToken);
|
|
38
48
|
// 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
|
|
39
49
|
const state = context.use(qqRuntimeStateToken);
|
|
40
50
|
state.endpoints.set(config.id, {
|
|
@@ -47,11 +57,13 @@ export default defineAdapter<QqAdapterConfig>({
|
|
|
47
57
|
? new QqWebsocketEndpoint({
|
|
48
58
|
id: context.id,
|
|
49
59
|
gateway,
|
|
60
|
+
sideEvents,
|
|
50
61
|
config,
|
|
51
62
|
})
|
|
52
63
|
: new QqHttpEndpoint({
|
|
53
64
|
id: context.id,
|
|
54
65
|
gateway,
|
|
66
|
+
sideEvents,
|
|
55
67
|
http: context.use(httpHostToken),
|
|
56
68
|
config,
|
|
57
69
|
});
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
5
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
4
|
+
import { type EndpointControl, type EndpointInstance, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
|
|
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,12 +13,14 @@ 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
|
}
|
|
19
20
|
export declare class QqWebsocketEndpoint implements EndpointInstance {
|
|
20
21
|
#private;
|
|
21
22
|
readonly management: EndpointManagement;
|
|
23
|
+
readonly control: EndpointControl;
|
|
22
24
|
constructor(options: QqEndpointOptions);
|
|
23
25
|
/** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
|
|
24
26
|
get name(): string;
|
|
@@ -42,6 +44,7 @@ export declare class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
42
44
|
export interface QqHttpEndpointOptions {
|
|
43
45
|
readonly id: CapabilityId;
|
|
44
46
|
readonly gateway: MessageGateway;
|
|
47
|
+
readonly sideEvents?: SideEventGateway;
|
|
45
48
|
readonly http: HttpHost;
|
|
46
49
|
readonly config: ResolvedQqHttpConfig;
|
|
47
50
|
readonly createBot?: CreateQqHttpBot;
|
|
@@ -50,6 +53,7 @@ export interface QqHttpEndpointOptions {
|
|
|
50
53
|
export declare class QqHttpEndpoint implements EndpointInstance {
|
|
51
54
|
#private;
|
|
52
55
|
readonly management: EndpointManagement;
|
|
56
|
+
readonly control: EndpointControl;
|
|
53
57
|
constructor(options: QqHttpEndpointOptions);
|
|
54
58
|
/** Live endpoint 名(Console/AdapterIndex 展示用)。 */
|
|
55
59
|
get name(): string;
|
package/lib/endpoint.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
3
|
+
*/
|
|
4
|
+
import { createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
1
5
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
2
6
|
import { formatOutbound } from './outbound.js';
|
|
3
7
|
import { registerQqAgentEndpoint } from './qq-agent-deps.js';
|
|
4
8
|
import { formatInboundContent, parseCompoundMessageId, qqInboundConversation, qqOutboundKind, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
|
|
5
9
|
import { defaultCreateHttpBot, registerQqWebhookRoutes, } from './webhook.js';
|
|
6
10
|
import { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
|
|
11
|
+
import { bindQqBotSideEvents, receiveQqSideEvent, } from './side-event-dispatch.js';
|
|
7
12
|
export class QqWebsocketEndpoint {
|
|
8
13
|
#logger;
|
|
9
14
|
#options;
|
|
@@ -13,6 +18,7 @@ export class QqWebsocketEndpoint {
|
|
|
13
18
|
#started = false;
|
|
14
19
|
#unregisterAgent;
|
|
15
20
|
management = createQqEndpointManagement(this);
|
|
21
|
+
control = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
16
22
|
constructor(options) {
|
|
17
23
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
18
24
|
this.#options = options;
|
|
@@ -180,6 +186,9 @@ export class QqWebsocketEndpoint {
|
|
|
180
186
|
if (msg)
|
|
181
187
|
this.admit(msg);
|
|
182
188
|
});
|
|
189
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
190
|
+
receiveQqSideEvent(this.#options.sideEvents, this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
191
|
+
});
|
|
183
192
|
}
|
|
184
193
|
#requireBot() {
|
|
185
194
|
if (!this.#bot)
|
|
@@ -198,6 +207,7 @@ export class QqHttpEndpoint {
|
|
|
198
207
|
#started = false;
|
|
199
208
|
#unregisterAgent;
|
|
200
209
|
management = createQqEndpointManagement(this);
|
|
210
|
+
control = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
201
211
|
constructor(options) {
|
|
202
212
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
203
213
|
this.#options = options;
|
|
@@ -372,6 +382,9 @@ export class QqHttpEndpoint {
|
|
|
372
382
|
if (msg)
|
|
373
383
|
this.admit(msg);
|
|
374
384
|
});
|
|
385
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
386
|
+
receiveQqSideEvent(this.#options.sideEvents, this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
387
|
+
});
|
|
375
388
|
}
|
|
376
389
|
#requireBot() {
|
|
377
390
|
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": "
|
|
3
|
+
"version": "8.0.0",
|
|
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.
|
|
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.2.0",
|
|
39
|
+
"@zhin.js/core": "1.5.13",
|
|
40
|
+
"@zhin.js/host-http": "1.0.12",
|
|
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.
|
|
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.2.0",
|
|
48
|
+
"@zhin.js/agent": "1.1.15",
|
|
49
|
+
"@zhin.js/command": "1.0.15",
|
|
50
|
+
"@zhin.js/core": "1.5.13",
|
|
51
|
+
"@zhin.js/middleware": "1.0.12",
|
|
52
|
+
"zhin.js": "6.0.13"
|
|
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.15",
|
|
77
|
+
"@zhin.js/host-http": "1.0.12",
|
|
78
|
+
"zhin.js": "6.0.13"
|
|
79
79
|
},
|
|
80
80
|
"files": [
|
|
81
81
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
createRecallEndpointControl,
|
|
6
|
+
type EndpointControl,
|
|
7
|
+
type EndpointChannel,
|
|
8
|
+
type EndpointInstance,
|
|
9
|
+
type EndpointManagement,
|
|
10
|
+
type EndpointSendRequest,
|
|
9
11
|
} from 'zhin.js/adapter';
|
|
10
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
12
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
11
13
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
12
14
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
13
15
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -37,6 +39,11 @@ import {
|
|
|
37
39
|
type CreateQqBot,
|
|
38
40
|
type QqBotTransport,
|
|
39
41
|
} from './ws.js';
|
|
42
|
+
import {
|
|
43
|
+
bindQqBotSideEvents,
|
|
44
|
+
receiveQqSideEvent,
|
|
45
|
+
type QqSideEventCaller,
|
|
46
|
+
} from './side-event-dispatch.js';
|
|
40
47
|
|
|
41
48
|
export type { CreateQqBot, QqBotTransport } from './ws.js';
|
|
42
49
|
export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
@@ -44,6 +51,7 @@ export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
|
44
51
|
export interface QqEndpointOptions {
|
|
45
52
|
readonly id: CapabilityId;
|
|
46
53
|
readonly gateway: MessageGateway;
|
|
54
|
+
readonly sideEvents?: SideEventGateway;
|
|
47
55
|
readonly config: ResolvedQqWebsocketConfig;
|
|
48
56
|
readonly createBot?: CreateQqBot;
|
|
49
57
|
}
|
|
@@ -58,6 +66,7 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
58
66
|
#started = false;
|
|
59
67
|
#unregisterAgent?: () => void;
|
|
60
68
|
readonly management: EndpointManagement = createQqEndpointManagement(this);
|
|
69
|
+
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
61
70
|
|
|
62
71
|
constructor(options: QqEndpointOptions) {
|
|
63
72
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
@@ -246,6 +255,16 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
246
255
|
const msg = normalizeQqMessage(raw);
|
|
247
256
|
if (msg) this.admit(msg);
|
|
248
257
|
});
|
|
258
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
259
|
+
receiveQqSideEvent(
|
|
260
|
+
this.#options.sideEvents,
|
|
261
|
+
this.#options.config.id,
|
|
262
|
+
bot as QqSideEventCaller,
|
|
263
|
+
eventName,
|
|
264
|
+
raw,
|
|
265
|
+
this.#logger,
|
|
266
|
+
);
|
|
267
|
+
});
|
|
249
268
|
}
|
|
250
269
|
|
|
251
270
|
#requireBot(): QqBotTransport {
|
|
@@ -257,6 +276,7 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
257
276
|
export interface QqHttpEndpointOptions {
|
|
258
277
|
readonly id: CapabilityId;
|
|
259
278
|
readonly gateway: MessageGateway;
|
|
279
|
+
readonly sideEvents?: SideEventGateway;
|
|
260
280
|
readonly http: HttpHost;
|
|
261
281
|
readonly config: ResolvedQqHttpConfig;
|
|
262
282
|
readonly createBot?: CreateQqHttpBot;
|
|
@@ -274,6 +294,7 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
274
294
|
#started = false;
|
|
275
295
|
#unregisterAgent?: () => void;
|
|
276
296
|
readonly management: EndpointManagement = createQqEndpointManagement(this);
|
|
297
|
+
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
277
298
|
|
|
278
299
|
constructor(options: QqHttpEndpointOptions) {
|
|
279
300
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
@@ -469,6 +490,16 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
469
490
|
const msg = normalizeQqMessage(raw);
|
|
470
491
|
if (msg) this.admit(msg);
|
|
471
492
|
});
|
|
493
|
+
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
494
|
+
receiveQqSideEvent(
|
|
495
|
+
this.#options.sideEvents,
|
|
496
|
+
this.#options.config.id,
|
|
497
|
+
bot as QqSideEventCaller,
|
|
498
|
+
eventName,
|
|
499
|
+
raw,
|
|
500
|
+
this.#logger,
|
|
501
|
+
);
|
|
502
|
+
});
|
|
472
503
|
}
|
|
473
504
|
|
|
474
505
|
#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
|
}
|