@zhin.js/adapter-qq 5.0.2 → 5.0.3

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.
Files changed (116) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +51 -417
  3. package/adapters/qq.ts +45 -0
  4. package/agent/tools/add_role.ts +2 -2
  5. package/agent/tools/channel_info.ts +2 -2
  6. package/agent/tools/create_role.ts +2 -2
  7. package/agent/tools/list_channels.ts +2 -2
  8. package/agent/tools/list_guilds.ts +2 -2
  9. package/agent/tools/list_roles.ts +2 -2
  10. package/agent/tools/member_detail.ts +2 -2
  11. package/agent/tools/remove_role.ts +2 -2
  12. package/lib/endpoint.d.ts +69 -0
  13. package/lib/endpoint.js +315 -0
  14. package/lib/index.d.ts +6 -0
  15. package/lib/index.js +6 -0
  16. package/lib/platform-permit.d.ts +16 -0
  17. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  18. package/lib/protocol.d.ts +96 -0
  19. package/lib/protocol.js +129 -0
  20. package/lib/qq-agent-deps.d.ts +21 -0
  21. package/lib/qq-agent-deps.js +30 -0
  22. package/lib/webhook.d.ts +18 -0
  23. package/lib/webhook.js +100 -0
  24. package/lib/ws.d.ts +24 -0
  25. package/lib/ws.js +118 -0
  26. package/package.json +44 -40
  27. package/plugin.ts +13 -0
  28. package/schema.json +51 -0
  29. package/src/endpoint.ts +314 -600
  30. package/src/index.ts +56 -186
  31. package/src/platform-permit.ts +1 -1
  32. package/src/protocol.ts +229 -0
  33. package/src/qq-agent-deps.ts +49 -12
  34. package/src/webhook.ts +127 -0
  35. package/src/ws.ts +172 -0
  36. package/client/Dashboard.tsx +0 -230
  37. package/client/index.tsx +0 -11
  38. package/client/tsconfig.json +0 -7
  39. package/client/utils/api.ts +0 -30
  40. package/dist/index.js +0 -30
  41. package/lib/agent/tools/add_role.js +0 -23
  42. package/lib/agent/tools/add_role.js.map +0 -1
  43. package/lib/agent/tools/channel_info.js +0 -19
  44. package/lib/agent/tools/channel_info.js.map +0 -1
  45. package/lib/agent/tools/create_role.js +0 -22
  46. package/lib/agent/tools/create_role.js.map +0 -1
  47. package/lib/agent/tools/list_channels.js +0 -20
  48. package/lib/agent/tools/list_channels.js.map +0 -1
  49. package/lib/agent/tools/list_guilds.js +0 -17
  50. package/lib/agent/tools/list_guilds.js.map +0 -1
  51. package/lib/agent/tools/list_roles.js +0 -20
  52. package/lib/agent/tools/list_roles.js.map +0 -1
  53. package/lib/agent/tools/member_detail.js +0 -20
  54. package/lib/agent/tools/member_detail.js.map +0 -1
  55. package/lib/agent/tools/remove_role.js +0 -23
  56. package/lib/agent/tools/remove_role.js.map +0 -1
  57. package/lib/package.json +0 -105
  58. package/lib/src/adapter.js +0 -81
  59. package/lib/src/adapter.js.map +0 -1
  60. package/lib/src/endpoint.js +0 -600
  61. package/lib/src/endpoint.js.map +0 -1
  62. package/lib/src/gateway-config.js +0 -32
  63. package/lib/src/gateway-config.js.map +0 -1
  64. package/lib/src/group-at-normalize.js +0 -68
  65. package/lib/src/group-at-normalize.js.map +0 -1
  66. package/lib/src/inbound-action.js +0 -57
  67. package/lib/src/inbound-action.js.map +0 -1
  68. package/lib/src/inbound-normalize.js +0 -47
  69. package/lib/src/inbound-normalize.js.map +0 -1
  70. package/lib/src/index.js +0 -194
  71. package/lib/src/index.js.map +0 -1
  72. package/lib/src/mention-enrich.js +0 -52
  73. package/lib/src/mention-enrich.js.map +0 -1
  74. package/lib/src/outbound-keyboard.js +0 -100
  75. package/lib/src/outbound-keyboard.js.map +0 -1
  76. package/lib/src/outbound-markdown.js +0 -189
  77. package/lib/src/outbound-markdown.js.map +0 -1
  78. package/lib/src/outbound-media.js +0 -51
  79. package/lib/src/outbound-media.js.map +0 -1
  80. package/lib/src/outbound-mixed-media.js +0 -78
  81. package/lib/src/outbound-mixed-media.js.map +0 -1
  82. package/lib/src/platform-permit.js.map +0 -1
  83. package/lib/src/qq-agent-deps.js +0 -10
  84. package/lib/src/qq-agent-deps.js.map +0 -1
  85. package/lib/src/qq-bind-api.js +0 -90
  86. package/lib/src/qq-bind-api.js.map +0 -1
  87. package/lib/src/qq-bind-flow.js +0 -86
  88. package/lib/src/qq-bind-flow.js.map +0 -1
  89. package/lib/src/qq-bind-persist.js +0 -52
  90. package/lib/src/qq-bind-persist.js.map +0 -1
  91. package/lib/src/qq-endpoint-manager.js +0 -159
  92. package/lib/src/qq-endpoint-manager.js.map +0 -1
  93. package/lib/src/sdk-version.js +0 -19
  94. package/lib/src/sdk-version.js.map +0 -1
  95. package/lib/src/segment-mapper.js +0 -2
  96. package/lib/src/segment-mapper.js.map +0 -1
  97. package/lib/src/types.js +0 -2
  98. package/lib/src/types.js.map +0 -1
  99. package/plugin.yml +0 -3
  100. package/src/adapter.ts +0 -97
  101. package/src/gateway-config.ts +0 -48
  102. package/src/group-at-normalize.ts +0 -75
  103. package/src/inbound-action.ts +0 -87
  104. package/src/inbound-normalize.ts +0 -57
  105. package/src/mention-enrich.ts +0 -71
  106. package/src/outbound-keyboard.ts +0 -117
  107. package/src/outbound-markdown.ts +0 -212
  108. package/src/outbound-media.ts +0 -58
  109. package/src/outbound-mixed-media.ts +0 -101
  110. package/src/qq-bind-api.ts +0 -145
  111. package/src/qq-bind-flow.ts +0 -127
  112. package/src/qq-bind-persist.ts +0 -71
  113. package/src/qq-endpoint-manager.ts +0 -199
  114. package/src/sdk-version.ts +0 -21
  115. package/src/segment-mapper.ts +0 -1
  116. package/src/types.ts +0 -52
package/src/endpoint.ts CHANGED
@@ -1,675 +1,389 @@
1
1
  /**
2
- * QQ 官方 Endpoint 实现
2
+ * QQ endpoints — lifecycle, outbound, admit, agent tool surface.
3
3
  */
4
+ import type { EndpointInstance } from '@zhin.js/adapter';
5
+ import type { MessageGateway } from '@zhin.js/core/runtime';
6
+ import type { HttpHost } from '@zhin.js/host-http';
7
+ import { formatCompact, getLogger } from '@zhin.js/logger';
8
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
9
+ import { registerQqAgentEndpoint } from './qq-agent-deps.js';
4
10
  import {
5
- Bot as QQOfficialClient,
6
- PrivateMessageEvent,
7
- GroupMessageEvent,
8
- } from "qq-official-bot";
9
- import path from "path";
10
- import { formatCompact } from "@zhin.js/logger";
11
- import { registerFetchRoute, type RouterContext } from "@zhin.js/host-router/router";
12
- import { Endpoint, Message, SendOptions, SendContent, segment, type MessageElement } from 'zhin.js';
13
-
14
- import { ReceiverMode, type QQEndpointConfig, type ApplicationPlatform } from "./types.js";
15
- import type { QQAdapter } from "./adapter.js";
16
- import { normalizeQqInboundWsPayload, type QqWsPacket } from "./inbound-normalize.js";
17
- import { normalizeGroupAtPrefix } from "./group-at-normalize.js";
18
- import { SDK_VERSION, SDK_VERSION_HEADER } from "./sdk-version.js";
19
- import { applyCustomAuthEndpoints } from "./gateway-config.js";
20
- import { normalizeOutboundMarkdown, asOutboundSegments, splitReplyPrefix } from "./outbound-markdown.js";
21
- import { normalizeOutboundMedia } from "./outbound-media.js";
22
- import { fromCanonicalSegments, toCanonicalSegments } from "./segment-mapper.js";
23
- import { enrichCanonicalMentionNames } from "./mention-enrich.js";
11
+ formatInboundContent,
12
+ formatInboundTarget,
13
+ formatOutboundText,
14
+ parseSendTarget,
15
+ resolveOutboundMessageId,
16
+ senderDisplayName,
17
+ type QqInboundMessage,
18
+ type ResolvedQqHttpConfig,
19
+ type ResolvedQqWebsocketConfig,
20
+ } from './protocol.js';
24
21
  import {
25
- buildMixedMediaMessagePayload,
26
- buildQqImageUploadPayload,
27
- firstImageSegment,
28
- shouldSendTextImageMixedMedia,
29
- } from "./outbound-mixed-media.js";
30
- import { normalizeQqGuildSenderForPermit } from "./platform-permit.js";
31
- import { expandKeyboardSegmentsForQq } from "./outbound-keyboard.js";
32
- import { formatQqActionMessage, type QqActionNoticeEvent } from "./inbound-action.js";
33
-
34
- /** 从 qq-official-bot SendResult / 审核回包中解析出站消息 ID */
35
- export function resolveOutboundMessageId(result: unknown): string {
36
- if (!result || typeof result !== "object") {
37
- throw new Error("QQ 发送消息失败:响应为空");
38
- }
39
- const row = result as Record<string, unknown>;
40
- const nested = row.data && typeof row.data === "object"
41
- ? row.data as Record<string, unknown>
42
- : undefined;
43
- const audit = (row.message_audit ?? nested?.message_audit) as Record<string, unknown> | undefined;
44
- const id = row.id ?? row.message_id ?? audit?.audit_id;
45
- if (id == null || id === "") {
46
- const code = row.code;
47
- const msg = row.message;
48
- throw new Error(
49
- code != null
50
- ? `QQ 发送消息失败(${String(code)})${msg ? `: ${String(msg)}` : ""}`
51
- : "QQ 发送消息失败:响应缺少消息 ID",
52
- );
53
- }
54
- return String(id);
22
+ defaultCreateHttpBot,
23
+ registerQqWebhookRoutes,
24
+ type CreateQqHttpBot,
25
+ type QqHttpBotTransport,
26
+ } from './webhook.js';
27
+ import {
28
+ bindQqBotInboundEvents,
29
+ defaultCreateBot,
30
+ normalizeQqMessage,
31
+ type CreateQqBot,
32
+ type QqBotTransport,
33
+ } from './ws.js';
34
+
35
+ const logger = getLogger('qq');
36
+
37
+ export type { CreateQqBot, QqBotTransport } from './ws.js';
38
+ export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
39
+
40
+ export interface QqEndpointOptions {
41
+ readonly id: CapabilityId;
42
+ readonly gateway: MessageGateway;
43
+ readonly config: ResolvedQqWebsocketConfig;
44
+ readonly createBot?: CreateQqBot;
55
45
  }
56
46
 
57
- export class QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform = ApplicationPlatform>
58
- extends QQOfficialClient
59
- implements Endpoint<QQEndpointConfig<T, M>, PrivateMessageEvent | GroupMessageEvent>
60
- {
61
- $connected: boolean = false;
62
- declare $config: QQEndpointConfig<T, M>;
63
- /** 平台侧机器人 user_id,用于 @ 触发匹配 */
64
- $platformUserId?: string;
65
-
66
- get pluginLogger() {
67
- return this.adapter.plugin.logger;
68
- }
47
+ export class QqWebsocketEndpoint implements EndpointInstance {
48
+ readonly #options: QqEndpointOptions;
49
+ readonly #createBot: CreateQqBot;
50
+ #bot: QqBotTransport | null = null;
51
+ #open = false;
52
+ #started = false;
53
+ #unregisterAgent?: () => void;
69
54
 
70
- get $id() {
71
- return this.$config.name;
55
+ constructor(options: QqEndpointOptions) {
56
+ this.#options = options;
57
+ this.#createBot = options.createBot ?? defaultCreateBot;
72
58
  }
73
59
 
74
- constructor(public adapter: QQAdapter, config: QQEndpointConfig<T, M>) {
75
- if (!config.data_dir) config.data_dir = path.join(process.cwd(), "data");
76
- if (config.mode === ReceiverMode.MIDDLEWARE) {
77
- const mw = config as QQEndpointConfig<ReceiverMode.MIDDLEWARE, M> & {
78
- platform?: ApplicationPlatform;
79
- };
80
- if (!mw.platform) {
81
- mw.platform = (mw.application ?? "koa") as ApplicationPlatform;
82
- }
60
+ async start(): Promise<void> {
61
+ if (this.#started) return;
62
+ this.#started = true;
63
+ try {
64
+ this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
65
+ this.#bot = this.#createBot(this.#options.config);
66
+ this.#bindBot(this.#bot);
67
+ await this.#bot.start();
68
+ logger.info(formatCompact({
69
+ op: 'connect',
70
+ endpoint: this.#options.config.name,
71
+ mode: 'websocket',
72
+ appid: this.#options.config.appid,
73
+ }));
74
+ } catch (error) {
75
+ await this.stop();
76
+ logger.error('Failed to connect QQ websocket:', error);
77
+ throw error;
83
78
  }
84
- super(config);
85
- this.$config = config;
86
- this.attachSdkVersionHeader();
87
- applyCustomAuthEndpoints(this, config, this.pluginLogger);
88
79
  }
89
80
 
90
- /** 出站 QQ API 请求附带 SDK 版本,便于平台侧日志排查 */
91
- private attachSdkVersionHeader(): void {
92
- this.request.interceptors.request.use((reqConfig) => {
93
- reqConfig.headers[SDK_VERSION_HEADER] = SDK_VERSION;
94
- return reqConfig;
95
- });
81
+ open(): void {
82
+ this.#open = true;
96
83
  }
97
84
 
98
- /** 归一化 QQ API v2 群聊字段后再交给 qq-official-bot 解析 */
99
- dispatchEvent(event: string, wsRes: QqWsPacket): void {
100
- this.pluginLogger.debug({
101
- op: 'inbound_ws',
102
- event,
103
- group: String(wsRes.d?.group_openid ?? wsRes.d?.group_id ?? '?'),
104
- });
105
- if (event === 'GROUP_AT_MESSAGE_CREATE' || event === 'GROUP_MESSAGE_CREATE') {
106
- this.pluginLogger.info(
107
- `qq inbound ws: ${event} group=${String(wsRes.d?.group_openid ?? wsRes.d?.group_id ?? '?')}`,
108
- );
109
- }
110
- normalizeQqInboundWsPayload(event, wsRes);
111
- super.dispatchEvent(event, wsRes);
112
- }
113
-
114
- private guildMemberCache = new Map<string, { at: number; role?: string; permissions: string[] }>();
115
- private guildOwnerCache = new Map<string, { at: number; ownerId?: string }>();
116
-
117
- private async enrichGuildSender(
118
- message: ReturnType<QQEndpoint<ReceiverMode>["$formatMessage"]>,
119
- guildId: string,
120
- userId: string,
121
- ): Promise<void> {
122
- const key = `${guildId}:${userId}`;
123
- const now = Date.now();
124
- const cached = this.guildMemberCache.get(key);
125
- if (cached && now - cached.at < 60_000) {
126
- message.$sender.role = cached.role;
127
- message.$sender.permissions = cached.permissions;
128
- return;
129
- }
130
- try {
131
- let ownerId: string | undefined;
132
- const ownerCached = this.guildOwnerCache.get(guildId);
133
- if (ownerCached && now - ownerCached.at < 300_000) {
134
- ownerId = ownerCached.ownerId;
135
- } else {
136
- const guild = await this.getGuildInfo(guildId);
137
- ownerId = guild?.owner_id?.toString?.() ?? guild?.owner?.id?.toString?.();
138
- this.guildOwnerCache.set(guildId, { at: now, ownerId });
139
- }
140
- const member = await this.getGuildMember(guildId, userId);
141
- const roleIds = Array.isArray(member?.roles)
142
- ? member.roles.map((r: unknown) =>
143
- typeof r === "string" ? r : (r as { id?: string })?.id ?? String(r),
144
- )
145
- : [];
146
- const isOwner = ownerId === userId;
147
- const isAdmin = !isOwner && roleIds.length > 0;
148
- const normalized = normalizeQqGuildSenderForPermit({ roles: roleIds, isOwner, isAdmin });
149
- const entry = {
150
- at: now,
151
- role: normalized.role,
152
- permissions: normalized.permissions ?? [],
153
- };
154
- this.guildMemberCache.set(key, entry);
155
- message.$sender.role = entry.role;
156
- message.$sender.permissions = entry.permissions;
157
- } catch {
158
- // 保守拒绝
159
- }
85
+ close(): void {
86
+ this.#open = false;
160
87
  }
161
88
 
162
- private async handleQQMessage(msg: PrivateMessageEvent | GroupMessageEvent): Promise<void> {
163
- try {
164
- const message = this.$formatMessage(msg);
165
- if (msg.message_type === "guild" && msg.guild_id && message.$sender.id) {
166
- await this.enrichGuildSender(message, String(msg.guild_id), message.$sender.id);
89
+ async stop(): Promise<void> {
90
+ this.#open = false;
91
+ this.#unregisterAgent?.();
92
+ this.#unregisterAgent = undefined;
93
+ if (this.#bot) {
94
+ try {
95
+ this.#bot.removeAllListeners();
96
+ await this.#bot.stop();
97
+ } catch {
98
+ /* ignore */
167
99
  }
168
- this.adapter.emit("message.receive", message);
169
- this.pluginLogger.debug(
170
- `${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
171
- );
172
- } catch (err) {
173
- this.pluginLogger.warn(
174
- `qq format inbound failed (${msg.message_type}): ${err instanceof Error ? err.message : String(err)}`,
175
- );
100
+ this.#bot = null;
176
101
  }
177
- }
178
-
179
- private async handleQQAction(event: QqActionNoticeEvent): Promise<void> {
180
- try {
181
- await event.reply(0);
182
- const channel = event.notice_type === 'group'
183
- ? { id: String(event.group_id), type: 'group' as const }
184
- : event.notice_type === 'guild'
185
- ? { id: String(event.channel_id), type: 'channel' as const }
186
- : { id: String(event.operator_id), type: 'private' as const };
187
- const message = formatQqActionMessage(event, {
188
- endpointName: this.$config.name,
189
- send: async (content) => this.adapter.sendMessage({
190
- ...channel,
191
- context: 'qq',
192
- endpoint: this.$config.name,
193
- content,
194
- }),
195
- });
196
- await this.adapter.receiveMessageAwait(message);
197
- this.pluginLogger.debug(
198
- `qq action ${message.$channel.type}(${message.$channel.id}): ${message.$raw}`,
199
- );
200
- } catch (err) {
201
- this.pluginLogger.warn(
202
- `qq format action failed: ${err instanceof Error ? err.message : String(err)}`,
203
- );
102
+ this.#started = false;
103
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
104
+ }
105
+
106
+ async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
107
+ const body = formatOutboundText(payload);
108
+ const parsed = parseSendTarget(target);
109
+ const bot = this.#requireBot();
110
+ let result: unknown;
111
+ switch (parsed.kind) {
112
+ case 'private':
113
+ result = await bot.sendPrivateMessage(parsed.id, body);
114
+ break;
115
+ case 'group':
116
+ result = await bot.sendGroupMessage(parsed.id, body);
117
+ break;
118
+ case 'channel':
119
+ result = await bot.sendGuildMessage(parsed.id, body);
120
+ break;
121
+ case 'direct':
122
+ if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
123
+ result = await bot.sendDirectMessage(parsed.id, body);
124
+ break;
125
+ default:
126
+ throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
204
127
  }
128
+ const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
129
+ logger.debug(formatCompact({
130
+ op: 'qq_send',
131
+ endpoint: this.#options.config.name,
132
+ target,
133
+ messageId,
134
+ }));
135
+ return messageId;
136
+ }
137
+
138
+ /** Test / internal: admit a message when open. */
139
+ admit(msg: QqInboundMessage): void {
140
+ if (!this.#open) return;
141
+ const target = formatInboundTarget(msg);
142
+ void this.#options.gateway.receive({
143
+ adapter: this.#options.id,
144
+ target,
145
+ content: formatInboundContent(msg),
146
+ sender: senderDisplayName(msg),
147
+ id: msg.id,
148
+ metadata: Object.freeze({
149
+ endpoint: this.#options.config.name,
150
+ channelKind: msg.channelKind,
151
+ userId: msg.authorId,
152
+ guildId: msg.guildId,
153
+ roles: msg.authorRoles,
154
+ // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
155
+ ...(msg.mentioned ? { mentioned: true } : {}),
156
+ }),
157
+ }).catch((err) => {
158
+ logger.warn(formatCompact({
159
+ op: 'qq_gateway_receive_failed',
160
+ target,
161
+ error: err instanceof Error ? err.message : String(err),
162
+ }));
163
+ });
205
164
  }
206
165
 
207
- private handleGroupNotice(event: string, payload: { group_id?: string; operator_id?: string }): void {
208
- this.pluginLogger.info(
209
- `qq notice ${event}: group=${payload.group_id ?? '?'} operator=${payload.operator_id ?? '?'}`,
210
- );
211
- }
212
-
213
- async $connect(): Promise<void> {
214
- this.on("message.group", this.handleQQMessage.bind(this));
215
- this.on("message.guild", this.handleQQMessage.bind(this));
216
- this.on("message.private", this.handleQQMessage.bind(this));
217
- this.on("notice.friend.action", this.handleQQAction.bind(this));
218
- this.on("notice.group.action", this.handleQQAction.bind(this));
219
- this.on("notice.guild.action", this.handleQQAction.bind(this));
220
- this.on("notice.group.increase", (e) => this.handleGroupNotice('group.add_robot', e));
221
- this.on("notice.group.decrease", (e) => this.handleGroupNotice('group.del_robot', e));
222
- this.on("notice.group.receive_open", (e) => this.handleGroupNotice('group.msg_receive_open', e));
223
- this.on("notice.group.receive_close", (e) => this.handleGroupNotice('group.msg_receive_close', e));
224
- await this.start();
225
- this.mountWebhookReceiver();
226
- this.$connected = true;
227
- try {
228
- const self = await this.getSelfInfo();
229
- const uid = (self as { id?: string; user_id?: string; username?: string })?.id
230
- ?? (self as { user_id?: string })?.user_id;
231
- if (uid) this.$platformUserId = String(uid);
232
- } catch (err) {
233
- this.pluginLogger.debug(
234
- `${this.$config.name} getSelfInfo failed: ${err instanceof Error ? err.message : String(err)}`,
235
- );
236
- }
166
+ getGuilds() {
167
+ return this.#requireBot().getGuilds();
237
168
  }
238
169
 
239
- async $disconnect(): Promise<void> {
240
- // Endpoint 继承自 EventEmitter,清除 $connect() 注册的所有监听器
241
- (this as unknown as import('node:events').EventEmitter).removeAllListeners();
242
- await this.stop();
243
- this.$connected = false;
170
+ getChannels(guildId: string) {
171
+ return this.#requireBot().getChannels(guildId);
244
172
  }
245
173
 
246
- private resolveWebhookPath(): string {
247
- const raw = this.$config.webhookPath ?? "/qq/webhook";
248
- return raw.startsWith("/") ? raw : `/${raw}`;
174
+ getChannelInfo(channelId: string) {
175
+ return this.#requireBot().getChannelInfo(channelId);
249
176
  }
250
177
 
251
- /**
252
- * Webhook 入站:middleware(挂 host-router)或独立 HTTP 端口(qq-official-bot 内置服务)。
253
- */
254
- private mountWebhookReceiver(): void {
255
- const mode = this.$config.mode;
256
- if (mode === ReceiverMode.MIDDLEWARE) {
257
- const router = this.adapter.getRouter();
258
- if (!router) {
259
- throw new Error("QQ mode=middleware 需要启用 @zhin.js/host-router 插件");
260
- }
261
- const webhookPath = this.resolveWebhookPath();
262
- const mw = this.middleware as (
263
- ctx: RouterContext,
264
- next: () => Promise<void>,
265
- ) => Promise<unknown>;
266
- router.post(webhookPath, async (ctx: RouterContext) => {
267
- this.pluginLogger.debug(ctx.body);
268
- await mw(ctx, async () => {});
269
- });
270
- this.pluginLogger.info(formatCompact({
271
- op: "webhook",
272
- mode: "middleware",
273
- path: webhookPath,
274
- url: `POST ${webhookPath}`,
275
- note: "无 /api 前缀;Host API 才走 /api/*",
276
- }));
277
- return;
278
- }
279
- if (mode === ReceiverMode.WEBHOOK) {
280
- const cfg = this.$config as QQEndpointConfig<ReceiverMode.WEBHOOK>;
281
- this.pluginLogger.info(formatCompact({
282
- op: "webhook",
283
- mode: "standalone",
284
- port: cfg.port,
285
- path: cfg.path,
286
- url: `http://127.0.0.1:${cfg.port}${cfg.path}`,
287
- }));
288
- }
178
+ getGuildMember(guildId: string, userId: string) {
179
+ return this.#requireBot().getGuildMember(guildId, userId);
289
180
  }
290
181
 
291
- private toWireSendContent(content: SendContent): MessageElement[] {
292
- const items = (Array.isArray(content) ? content : [content]).map((item) =>
293
- typeof item === "string" ? { type: "text", data: { text: item } } : item as MessageElement,
294
- );
295
- return fromCanonicalSegments(toCanonicalSegments(items as never));
182
+ getGuildRoles(guildId: string) {
183
+ return this.#requireBot().getGuildRoles(guildId);
296
184
  }
297
185
 
298
- $formatMessage(msg: PrivateMessageEvent | GroupMessageEvent) {
299
- const raw = msg as PrivateMessageEvent & GroupMessageEvent & {
300
- group_openid?: string;
301
- author?: { member_openid?: string; user_openid?: string; id?: string; username?: string };
302
- mentions?: Array<{ id?: string; member_openid?: string; username?: string; nickname?: string }>;
303
- __zhin_group_at?: boolean;
304
- };
305
-
306
- if (msg.message_type === "group") {
307
- if (!raw.group_id && raw.group_openid) {
308
- raw.group_id = raw.group_openid;
309
- }
310
- if (!msg.user_id && raw.author) {
311
- const uid = raw.author.member_openid ?? raw.author.user_openid ?? raw.author.id;
312
- if (uid) msg.user_id = String(uid);
313
- }
314
- }
186
+ createGuildRole(guildId: string, name: string, color?: number) {
187
+ return this.#requireBot().createGuildRole(guildId, name, color);
188
+ }
315
189
 
316
- let target_id = msg.user_id;
317
- if (msg.message_type === "guild") target_id = msg.channel_id!;
318
- if (msg.message_type === "group") target_id = raw.group_id ?? msg.group_id ?? "";
319
- if (msg.sub_type === "direct") target_id = `direct:${msg.guild_id}`;
320
-
321
- let content = msg.message;
322
- if (msg.message_type === "group" && Array.isArray(content)) {
323
- const endpointAtIds = [this.$platformUserId, this.$config.appid]
324
- .filter((id): id is string => Boolean(id))
325
- .map(String);
326
- content = normalizeGroupAtPrefix(content, endpointAtIds, raw.__zhin_group_at === true);
327
- }
190
+ addMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
191
+ return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
192
+ }
328
193
 
329
- if (Array.isArray(content)) {
330
- content = enrichCanonicalMentionNames(
331
- toCanonicalSegments(content),
332
- this.adapter,
333
- raw.mentions,
334
- );
335
- }
194
+ removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
195
+ return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
196
+ }
336
197
 
337
- const result = Message.from(msg, {
338
- $id: msg.message_id?.toString(),
339
- $adapter: "qq" as const,
340
- $endpoint: this.$config.name,
341
- $sender: {
342
- id: msg.sender.user_id?.toString(),
343
- name: msg.sender.user_name?.toString(),
344
- },
345
- $channel: {
346
- id: target_id,
347
- type: msg.message_type === "guild" ? "channel" : msg.message_type,
348
- },
349
- $content: content,
350
- $raw: msg.raw_message,
351
- $timestamp: Date.now(),
352
- $recall: async () => {
353
- await this.$recallMessage(result.$id);
354
- },
355
- $reply: async (content: SendContent, quote: boolean | string = true): Promise<string> => {
356
- if (!Array.isArray(content)) content = [content];
357
- if (quote) content.unshift({ type: "reply", data: { id: typeof quote === "boolean" ? result.$id : quote } });
358
- return await this.adapter.sendMessage({
359
- ...result.$channel,
360
- context: "qq",
361
- endpoint: this.$config.name,
362
- content,
363
- });
364
- },
198
+ #bindBot(bot: QqBotTransport): void {
199
+ bindQqBotInboundEvents(bot, (raw) => {
200
+ const msg = normalizeQqMessage(raw);
201
+ if (msg) this.admit(msg);
365
202
  });
366
- return result;
367
203
  }
368
204
 
369
- async $sendMessage(options: SendOptions): Promise<string> {
370
- const expanded = expandKeyboardSegmentsForQq(options.content);
371
- const canonicalized = this.toWireSendContent(expanded);
372
- const mediaNorm = normalizeOutboundMedia(canonicalized);
373
- const markdownMode = this.$config.outboundMarkdown;
374
-
375
- if (
376
- (options.type === "group" || options.type === "private")
377
- && !options.id.startsWith("direct:")
378
- && shouldSendTextImageMixedMedia(mediaNorm, markdownMode)
379
- ) {
380
- return await this.$sendTextImageMixedMedia(options, mediaNorm);
381
- }
382
-
383
- const content = normalizeOutboundMarkdown(mediaNorm, markdownMode);
384
- switch (options.type) {
385
- case "private": {
386
- if (options.id.startsWith("direct:")) {
387
- const id = options.id.replace("direct:", "");
388
- const result = await this.sendDirectMessage(id, content);
389
- this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
390
- return `direct-${options.id}:${resolveOutboundMessageId(result)}`;
391
- } else {
392
- const result = await this.sendPrivateMessage(options.id, content);
393
- this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
394
- return `private-${options.id}:${resolveOutboundMessageId(result)}`;
395
- }
396
- }
397
- case "group": {
398
- const result = await this.sendGroupMessage(options.id, content);
399
- this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
400
- return `group-${options.id}:${resolveOutboundMessageId(result)}`;
401
- }
402
- case "channel": {
403
- const result = await this.sendGuildMessage(options.id, content);
404
- this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
405
- return `channel-${options.id}:${resolveOutboundMessageId(result)}`;
406
- }
407
- default:
408
- throw new Error(`unsupported channel type ${options.type}`);
409
- }
205
+ #requireBot(): QqBotTransport {
206
+ if (!this.#bot) throw new Error('QQ bot not connected');
207
+ return this.#bot;
410
208
  }
209
+ }
411
210
 
412
- /** 内联图 + 文本:上传 file_info 后以 msg_type=7 图文混排发送(二维码等场景) */
413
- private async $sendTextImageMixedMedia(
414
- options: SendOptions,
415
- content: SendContent,
416
- ): Promise<string> {
417
- const endpointPath = this.resolveV2EndpointPath(options);
418
- const { body } = splitReplyPrefix(asOutboundSegments(content));
419
- const image = firstImageSegment(body);
420
- if (!image) {
421
- throw new Error("QQ 图文混排:未找到图片段");
422
- }
423
-
424
- const uploadPayload = buildQqImageUploadPayload(image);
425
- const { data: uploadResult } = await this.request.post(
426
- `${endpointPath}/files`,
427
- { ...uploadPayload, srv_send_msg: false },
428
- ) as { data: { file_info?: string } };
211
+ type ParsedSendTarget = ReturnType<typeof parseSendTarget>;
429
212
 
430
- if (!uploadResult?.file_info) {
431
- throw new Error("QQ 图文混排:图片上传未返回 file_info");
432
- }
213
+ export interface QqHttpEndpointOptions {
214
+ readonly id: CapabilityId;
215
+ readonly gateway: MessageGateway;
216
+ readonly http: HttpHost;
217
+ readonly config: ResolvedQqHttpConfig;
218
+ readonly createBot?: CreateQqHttpBot;
219
+ }
433
220
 
434
- const messagePayload = buildMixedMediaMessagePayload(content, uploadResult.file_info);
435
- const { data: result } = await this.request.post(
436
- `${endpointPath}/messages`,
437
- messagePayload,
438
- );
439
- this.pluginLogger.debug(
440
- `${this.$config.name} send mixed-media ${options.type}(${options.id}):${segment.raw(content)}`,
441
- );
442
- const idPrefix = options.type === "group" ? "group" : "private";
443
- return `${idPrefix}-${options.id}:${resolveOutboundMessageId(result)}`;
444
- }
445
-
446
- private resolveV2EndpointPath(options: SendOptions): string {
447
- switch (options.type) {
448
- case "group":
449
- return `/v2/groups/${options.id}`;
450
- case "private":
451
- return `/v2/users/${options.id}`;
452
- default:
453
- throw new Error(`QQ 图文混排不支持 ${options.type}`);
454
- }
455
- }
221
+ /** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
222
+ export class QqHttpEndpoint implements EndpointInstance {
223
+ readonly #options: QqHttpEndpointOptions;
224
+ readonly #createBot: CreateQqHttpBot;
225
+ #bot: QqHttpBotTransport | null = null;
226
+ #routeReleases: ReturnType<typeof registerQqWebhookRoutes> = [];
227
+ #open = false;
228
+ #started = false;
229
+ #unregisterAgent?: () => void;
456
230
 
457
- async $recallMessage(id: string): Promise<void> {
458
- if (!/^(private|group|channel|direct)-([^\:]+):(.+)$/.test(id)) throw new Error(`invalid message id ${id}`);
459
- const match = id.match(/^(private|group|channel|direct)-([^\:]+):(.+)$/);
460
- if (!match) return;
461
- const [, target_type, target_id, message_id] = match;
462
- if (target_type === "private") await this.recallPrivateMessage(target_id, message_id);
463
- if (target_type === "group") await this.recallGroupMessage(target_id, message_id);
464
- if (target_type === "channel") await this.recallGuildMessage(target_id, message_id);
465
- if (target_type === "direct") await this.recallDirectMessage(target_id, message_id);
231
+ constructor(options: QqHttpEndpointOptions) {
232
+ this.#options = options;
233
+ this.#createBot = options.createBot ?? defaultCreateHttpBot;
466
234
  }
467
235
 
468
- // ==================== 频道管理 API ====================
469
-
470
- /**
471
- * 获取频道列表
472
- */
473
- async getGuilds(): Promise<any[]> {
236
+ async start(): Promise<void> {
237
+ if (this.#started) return;
238
+ this.#started = true;
474
239
  try {
475
- return await this.guildService.getList();
240
+ this.#unregisterAgent = registerQqAgentEndpoint(
241
+ this.#options.config.name,
242
+ this as unknown as QqWebsocketEndpoint,
243
+ );
244
+ this.#setupRoutes();
245
+ this.#bot = this.#createBot(this.#options.config);
246
+ this.#bindBot(this.#bot);
247
+ await this.#bot.start();
248
+ logger.info(formatCompact({
249
+ op: 'connect',
250
+ endpoint: this.#options.config.name,
251
+ mode: this.#options.config.mode,
252
+ path: this.#options.config.webhookPath,
253
+ }));
476
254
  } catch (error) {
477
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取频道列表失败:`, error);
255
+ await this.stop();
256
+ logger.error('Failed to connect QQ HTTP receiver:', error);
478
257
  throw error;
479
258
  }
480
259
  }
481
260
 
482
- /**
483
- * 获取频道详情
484
- * @param guildId 频道 ID
485
- */
486
- async getGuildInfo(guildId: string): Promise<any> {
487
- try {
488
- return await this.guildService.getInfo(guildId);
489
- } catch (error) {
490
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取频道详情失败:`, error);
491
- throw error;
492
- }
261
+ open(): void {
262
+ this.#open = true;
493
263
  }
494
264
 
495
- /**
496
- * 获取子频道列表
497
- * @param guildId 频道 ID
498
- */
499
- async getChannels(guildId: string): Promise<any[]> {
500
- try {
501
- return await this.channelService.getList(guildId);
502
- } catch (error) {
503
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取子频道列表失败:`, error);
504
- throw error;
505
- }
265
+ close(): void {
266
+ this.#open = false;
506
267
  }
507
268
 
508
- /**
509
- * 获取子频道详情
510
- * @param channelId 子频道 ID
511
- */
512
- async getChannelInfo(channelId: string): Promise<any> {
513
- try {
514
- return await this.channelService.getInfo(channelId);
515
- } catch (error) {
516
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取子频道详情失败:`, error);
517
- throw error;
269
+ async stop(): Promise<void> {
270
+ this.#open = false;
271
+ for (const release of this.#routeReleases.splice(0)) release();
272
+ this.#unregisterAgent?.();
273
+ this.#unregisterAgent = undefined;
274
+ if (this.#bot) {
275
+ try {
276
+ this.#bot.removeAllListeners();
277
+ await this.#bot.stop();
278
+ } catch {
279
+ /* ignore */
280
+ }
281
+ this.#bot = null;
282
+ }
283
+ this.#started = false;
284
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
285
+ }
286
+
287
+ async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
288
+ const body = formatOutboundText(payload);
289
+ const parsed = parseSendTarget(target);
290
+ const bot = this.#requireBot();
291
+ let result: unknown;
292
+ switch (parsed.kind) {
293
+ case 'private':
294
+ result = await bot.sendPrivateMessage(parsed.id, body);
295
+ break;
296
+ case 'group':
297
+ result = await bot.sendGroupMessage(parsed.id, body);
298
+ break;
299
+ case 'channel':
300
+ result = await bot.sendGuildMessage(parsed.id, body);
301
+ break;
302
+ case 'direct':
303
+ if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
304
+ result = await bot.sendDirectMessage(parsed.id, body);
305
+ break;
306
+ default:
307
+ throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
518
308
  }
309
+ return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
310
+ }
311
+
312
+ admit(msg: QqInboundMessage): void {
313
+ if (!this.#open) return;
314
+ const target = formatInboundTarget(msg);
315
+ void this.#options.gateway.receive({
316
+ adapter: this.#options.id,
317
+ target,
318
+ content: formatInboundContent(msg),
319
+ sender: senderDisplayName(msg),
320
+ id: msg.id,
321
+ metadata: Object.freeze({
322
+ endpoint: this.#options.config.name,
323
+ channelKind: msg.channelKind,
324
+ userId: msg.authorId,
325
+ guildId: msg.guildId,
326
+ roles: msg.authorRoles,
327
+ // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
328
+ ...(msg.mentioned ? { mentioned: true } : {}),
329
+ }),
330
+ }).catch((err) => {
331
+ logger.warn(formatCompact({
332
+ op: 'qq_gateway_receive_failed',
333
+ target,
334
+ error: err instanceof Error ? err.message : String(err),
335
+ }));
336
+ });
519
337
  }
520
338
 
521
- /**
522
- * 获取频道成员列表
523
- * @param guildId 频道 ID
524
- */
525
- async getGuildMembers(guildId: string): Promise<any[]> {
526
- try {
527
- return await this.memberService.getGuildMemberList(guildId);
528
- } catch (error) {
529
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取频道成员列表失败:`, error);
530
- throw error;
531
- }
339
+ getGuilds() {
340
+ return this.#requireBot().getGuilds();
532
341
  }
533
342
 
534
- /**
535
- * 获取频道成员详情
536
- * @param guildId 频道 ID
537
- * @param userId 用户 ID
538
- */
539
- async getGuildMember(guildId: string, userId: string): Promise<any> {
540
- try {
541
- return await this.memberService.getGuildMemberInfo(guildId, userId);
542
- } catch (error) {
543
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取成员详情失败:`, error);
544
- throw error;
545
- }
343
+ getChannels(guildId: string) {
344
+ return this.#requireBot().getChannels(guildId);
546
345
  }
547
346
 
548
- /**
549
- * 删除频道成员(踢出)
550
- * @param guildId 频道 ID
551
- * @param userId 用户 ID
552
- * @param addBlacklist 是否加入黑名单
553
- * @param deleteHistoryMsg 删除历史消息天数 (-1不删除,0全部删除,3/7/15/30)
554
- */
555
- async removeGuildMember(guildId: string, userId: string, addBlacklist?: boolean, deleteHistoryMsg?: -1 | 0 | 3 | 7 | 15 | 30): Promise<boolean> {
556
- try {
557
- await this.memberService.kickMember(guildId, userId, deleteHistoryMsg, addBlacklist);
558
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 踢出成员 ${userId}(频道 ${guildId})`);
559
- return true;
560
- } catch (error) {
561
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 踢出成员失败:`, error);
562
- throw error;
563
- }
347
+ getChannelInfo(channelId: string) {
348
+ return this.#requireBot().getChannelInfo(channelId);
564
349
  }
565
350
 
566
- /**
567
- * 获取频道角色列表
568
- * @param guildId 频道 ID
569
- */
570
- async getGuildRoles(guildId: string): Promise<any[]> {
571
- try {
572
- return await this.guildService.getRoles(guildId);
573
- } catch (error) {
574
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取角色列表失败:`, error);
575
- throw error;
576
- }
351
+ getGuildMember(guildId: string, userId: string) {
352
+ return this.#requireBot().getGuildMember(guildId, userId);
577
353
  }
578
354
 
579
- /**
580
- * 创建频道角色
581
- * @param guildId 频道 ID
582
- * @param name 角色名
583
- * @param color 颜色
584
- * @param hoist 是否在成员列表中单独展示 (0 或 1)
585
- */
586
- async createGuildRole(guildId: string, name: string, color?: number, hoist?: 0 | 1): Promise<any> {
587
- try {
588
- const result = await this.guildService.createRole(guildId, { name, color: color || 0, hoist: hoist ?? 0 });
589
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 创建角色 "${name}"(频道 ${guildId})`);
590
- return result;
591
- } catch (error) {
592
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 创建角色失败:`, error);
593
- throw error;
594
- }
355
+ getGuildRoles(guildId: string) {
356
+ return this.#requireBot().getGuildRoles(guildId);
595
357
  }
596
358
 
597
- /**
598
- * 给成员添加角色
599
- * @param guildId 频道 ID
600
- * @param channelId 子频道 ID
601
- * @param userId 用户 ID
602
- * @param roleId 角色 ID
603
- */
604
- async addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean> {
605
- try {
606
- await this.memberService.addMemberRole(guildId, channelId, userId, roleId);
607
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 给成员 ${userId} 添加角色 ${roleId}(频道 ${guildId})`);
608
- return true;
609
- } catch (error) {
610
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 添加角色失败:`, error);
611
- throw error;
612
- }
359
+ createGuildRole(guildId: string, name: string, color?: number) {
360
+ return this.#requireBot().createGuildRole(guildId, name, color);
613
361
  }
614
362
 
615
- /**
616
- * 移除成员角色
617
- * @param guildId 频道 ID
618
- * @param channelId 子频道 ID
619
- * @param userId 用户 ID
620
- * @param roleId 角色 ID
621
- */
622
- async removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean> {
623
- try {
624
- await this.memberService.removeMemberRole(guildId, channelId, userId, roleId);
625
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 移除成员 ${userId} 的角色 ${roleId}(频道 ${guildId})`);
626
- return true;
627
- } catch (error) {
628
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 移除角色失败:`, error);
629
- throw error;
630
- }
363
+ addMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
364
+ return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
631
365
  }
632
366
 
633
- /**
634
- * 禁言成员(批量)
635
- * @param guildId 频道 ID
636
- * @param userIds 用户 ID 列表
637
- * @param muteSeconds 禁言时长(秒),0 表示解除禁言
638
- */
639
- async muteMembers(guildId: string, userIds: string[], muteSeconds: number): Promise<boolean> {
640
- try {
641
- if (muteSeconds > 0) {
642
- await this.memberService.muteMembers(guildId, userIds, muteSeconds);
643
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 禁言成员 ${userIds.join(',')} ${muteSeconds}秒(频道 ${guildId})`);
644
- } else {
645
- await this.memberService.unmuteMembers(guildId, userIds);
646
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 解除成员 ${userIds.join(',')} 禁言(频道 ${guildId})`);
647
- }
648
- return true;
649
- } catch (error) {
650
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 禁言操作失败:`, error);
651
- throw error;
652
- }
367
+ removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
368
+ return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
653
369
  }
654
370
 
655
- /**
656
- * 全员禁言
657
- * @param guildId 频道 ID
658
- * @param muteSeconds 禁言时长(秒),0 表示解除禁言
659
- */
660
- async muteAll(guildId: string, muteSeconds: number): Promise<boolean> {
661
- try {
662
- if (muteSeconds > 0) {
663
- await this.guildService.mute(guildId, muteSeconds);
664
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 开启全员禁言(频道 ${guildId})`);
665
- } else {
666
- await this.guildService.unmute(guildId);
667
- this.pluginLogger.info(`QQ Endpoint ${this.$id} 关闭全员禁言(频道 ${guildId})`);
668
- }
669
- return true;
670
- } catch (error) {
671
- this.pluginLogger.error(`QQ Endpoint ${this.$id} 全员禁言操作失败:`, error);
672
- throw error;
673
- }
371
+ #setupRoutes(): void {
372
+ this.#routeReleases.push(...registerQqWebhookRoutes(this.#options.http, {
373
+ config: this.#options.config,
374
+ getBot: () => this.#bot,
375
+ }));
376
+ }
377
+
378
+ #bindBot(bot: QqBotTransport): void {
379
+ bindQqBotInboundEvents(bot, (raw) => {
380
+ const msg = normalizeQqMessage(raw);
381
+ if (msg) this.admit(msg);
382
+ });
383
+ }
384
+
385
+ #requireBot(): QqBotTransport {
386
+ if (!this.#bot) throw new Error('QQ bot not connected');
387
+ return this.#bot;
674
388
  }
675
389
  }