@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 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
- 迁移到 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";
@@ -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 { EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
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 : 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.13",
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.1.10",
39
- "@zhin.js/core": "1.5.11",
40
- "@zhin.js/host-http": "1.0.10",
41
- "@zhin.js/im-contract": "1.0.3",
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.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.13",
49
- "@zhin.js/command": "1.0.14",
50
- "@zhin.js/core": "1.5.11",
51
- "@zhin.js/middleware": "1.0.11",
52
- "zhin.js": "6.0.11"
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.13",
77
- "@zhin.js/host-http": "1.0.10",
78
- "zhin.js": "6.0.11"
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 type {
5
- EndpointChannel,
6
- EndpointInstance,
7
- EndpointManagement,
8
- EndpointSendRequest,
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 : 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
  }