@zhin.js/adapter-qq 7.0.12 → 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 CHANGED
@@ -1,5 +1,53 @@
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
+
43
+ ## 7.0.13
44
+
45
+ ### Patch Changes
46
+
47
+ - @zhin.js/core@1.5.11
48
+ - @zhin.js/agent@1.1.13
49
+ - zhin.js@6.0.11
50
+
3
51
  ## 7.0.12
4
52
 
5
53
  ### 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
- 迁移到 Plugin Runtime 后,出站统一经 `messageGatewayToken` 渲染为文本后发送(`sendPrivateMessage` / `sendGroupMessage` / `sendGuildMessage`)。旧 Adapter 的富媒体出站能力(图片 / 语音 / 视频、keyboard 按钮、markdown 模板等)暂未迁移,当前出站等价于纯文本。如需富媒体,可通过 endpoint 的 QQ API 封装或直接调用 QQ HTTP API 作为逃生舱。
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 : 1,
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.12",
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.10",
39
- "@zhin.js/core": "1.5.10",
40
- "@zhin.js/host-http": "1.0.10",
41
- "@zhin.js/im-contract": "1.0.3",
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.2"
43
+ "@zhin.js/permission": "1.0.3"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "zod": "^4.0.0",
47
- "@zhin.js/adapter": "1.1.10",
48
- "@zhin.js/agent": "1.1.12",
49
- "@zhin.js/command": "1.0.14",
50
- "@zhin.js/core": "1.5.10",
51
- "@zhin.js/middleware": "1.0.11",
52
- "zhin.js": "6.0.10"
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.12",
77
- "@zhin.js/host-http": "1.0.10",
78
- "zhin.js": "6.0.10"
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 : 1,
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
  }