@zhin.js/adapter-qq 1.0.84 → 1.1.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +844 -17
  2. package/README.md +136 -233
  3. package/adapters/qq.js +50 -0
  4. package/adapters/qq.ts +71 -0
  5. package/agent/PERMITS.md +25 -0
  6. package/{skills/qq/SKILL.md → agent/skills/qq.md} +1 -1
  7. package/agent/tools/add_role.ts +21 -0
  8. package/agent/tools/channel_info.ts +17 -0
  9. package/agent/tools/create_role.ts +20 -0
  10. package/agent/tools/list_channels.ts +18 -0
  11. package/agent/tools/list_guilds.ts +14 -0
  12. package/agent/tools/list_roles.ts +18 -0
  13. package/agent/tools/member_detail.ts +18 -0
  14. package/agent/tools/remove_role.ts +21 -0
  15. package/commands/endpoint/add/[[id]].js +3 -0
  16. package/commands/endpoint/add/[[id]].ts +3 -0
  17. package/commands/endpoint/cancel.js +12 -0
  18. package/commands/endpoint/cancel.ts +15 -0
  19. package/commands/endpoint/list.js +3 -0
  20. package/commands/endpoint/list.ts +3 -0
  21. package/commands/endpoint/remove/[id].js +3 -0
  22. package/commands/endpoint/remove/[id].ts +3 -0
  23. package/lib/client.d.ts +12 -0
  24. package/lib/client.js +2 -0
  25. package/lib/endpoint.d.ts +56 -0
  26. package/lib/endpoint.js +456 -0
  27. package/lib/index.d.ts +8 -18
  28. package/lib/index.js +8 -338
  29. package/lib/outbound.d.ts +17 -0
  30. package/lib/outbound.js +234 -0
  31. package/lib/platform-permit.d.ts +15 -0
  32. package/lib/platform-permit.js +41 -0
  33. package/lib/protocol.d.ts +111 -0
  34. package/lib/protocol.js +166 -0
  35. package/lib/qq-bind-api.d.ts +36 -0
  36. package/lib/qq-bind-api.js +89 -0
  37. package/lib/qq-bind-flow.d.ts +19 -0
  38. package/lib/qq-bind-flow.js +85 -0
  39. package/lib/qq-bind-persist.d.ts +12 -0
  40. package/lib/qq-bind-persist.js +22 -0
  41. package/lib/qq-bot-kind-prompt.d.ts +9 -0
  42. package/lib/qq-bot-kind-prompt.js +37 -0
  43. package/lib/qq-endpoint-commands.d.ts +45 -0
  44. package/lib/qq-endpoint-commands.js +180 -0
  45. package/lib/qq-endpoint-config.d.ts +17 -0
  46. package/lib/qq-endpoint-config.js +22 -0
  47. package/lib/qq-intents.d.ts +27 -0
  48. package/lib/qq-intents.js +51 -0
  49. package/lib/qq-runtime-state.d.ts +30 -0
  50. package/lib/qq-runtime-state.js +14 -0
  51. package/lib/side-event-dispatch.d.ts +10 -0
  52. package/lib/side-event-dispatch.js +114 -0
  53. package/lib/webhook.d.ts +18 -0
  54. package/lib/webhook.js +103 -0
  55. package/lib/ws.d.ts +55 -0
  56. package/lib/ws.js +234 -0
  57. package/middlewares/bot-kind-reply.js +46 -0
  58. package/middlewares/bot-kind-reply.ts +60 -0
  59. package/package.json +69 -28
  60. package/plugin.js +18 -0
  61. package/schema.json +142 -0
  62. package/src/client.ts +16 -0
  63. package/src/endpoint.ts +534 -0
  64. package/src/index.ts +66 -326
  65. package/src/outbound.ts +255 -0
  66. package/src/platform-permit.ts +56 -0
  67. package/src/protocol.ts +273 -0
  68. package/src/qq-bind-api.ts +145 -0
  69. package/src/qq-bind-flow.ts +127 -0
  70. package/src/qq-bind-persist.ts +44 -0
  71. package/src/qq-bot-kind-prompt.ts +47 -0
  72. package/src/qq-endpoint-commands.ts +251 -0
  73. package/src/qq-endpoint-config.ts +44 -0
  74. package/src/qq-intents.ts +77 -0
  75. package/src/qq-runtime-state.ts +51 -0
  76. package/src/side-event-dispatch.ts +140 -0
  77. package/src/webhook.ts +132 -0
  78. package/src/ws.ts +331 -0
  79. package/client/Dashboard.tsx +0 -230
  80. package/client/index.tsx +0 -11
  81. package/client/tsconfig.json +0 -7
  82. package/client/utils/api.ts +0 -17
  83. package/dist/index.js +0 -30
  84. package/lib/adapter.d.ts +0 -17
  85. package/lib/adapter.d.ts.map +0 -1
  86. package/lib/adapter.js +0 -49
  87. package/lib/adapter.js.map +0 -1
  88. package/lib/bot.d.ts +0 -102
  89. package/lib/bot.d.ts.map +0 -1
  90. package/lib/bot.js +0 -337
  91. package/lib/bot.js.map +0 -1
  92. package/lib/index.d.ts.map +0 -1
  93. package/lib/index.js.map +0 -1
  94. package/lib/types.d.ts +0 -16
  95. package/lib/types.d.ts.map +0 -1
  96. package/lib/types.js +0 -2
  97. package/lib/types.js.map +0 -1
  98. package/plugin.yml +0 -3
  99. package/src/adapter.ts +0 -58
  100. package/src/bot.ts +0 -347
  101. package/src/types.ts +0 -24
@@ -0,0 +1,18 @@
1
+ import { defineAgentTool } from '@zhin.js/agent/tools';
2
+ import { z } from 'zod';
3
+ import { platformPermit } from '../../src/platform-permit.js';
4
+
5
+ export default defineAgentTool<{ guild_id: string }>({
6
+ description: '获取 QQ 频道角色列表',
7
+ inputSchema: z.object({
8
+ guild_id: z.string().describe('频道 ID'),
9
+ }),
10
+ adapter: 'qq',
11
+ tags: ['qq'],
12
+ permissions: [platformPermit('manage_roles')],
13
+ async execute({ guild_id }: { guild_id: string }, context) {
14
+ const client = context.$client;
15
+ const roles = await client.getGuildRoles(guild_id);
16
+ return { roles, count: roles.length };
17
+ },
18
+ });
@@ -0,0 +1,18 @@
1
+ import { defineAgentTool } from '@zhin.js/agent/tools';
2
+ import { z } from 'zod';
3
+ import { platformPermit } from '../../src/platform-permit.js';
4
+
5
+ export default defineAgentTool<{ guild_id: string; user_id: string }>({
6
+ description: '获取 QQ 频道中指定成员的详细信息',
7
+ inputSchema: z.object({
8
+ guild_id: z.string().describe('频道 ID'),
9
+ user_id: z.string().describe('用户 ID'),
10
+ }),
11
+ adapter: 'qq',
12
+ tags: ['qq'],
13
+ permissions: [platformPermit('guild_admin')],
14
+ async execute({ guild_id, user_id }: { guild_id: string; user_id: string }, context) {
15
+ const client = context.$client;
16
+ return client.getGuildMember(guild_id, user_id);
17
+ },
18
+ });
@@ -0,0 +1,21 @@
1
+ import { defineAgentTool } from '@zhin.js/agent/tools';
2
+ import { z } from 'zod';
3
+ import { platformPermit } from '../../src/platform-permit.js';
4
+
5
+ export default defineAgentTool<{ guild_id: string; channel_id: string; user_id: string; role_id: string }>({
6
+ description: '移除成员的 QQ 频道角色',
7
+ inputSchema: z.object({
8
+ guild_id: z.string().describe('频道 ID'),
9
+ channel_id: z.string().describe('子频道 ID'),
10
+ user_id: z.string().describe('用户 ID'),
11
+ role_id: z.string().describe('角色 ID'),
12
+ }),
13
+ adapter: 'qq',
14
+ tags: ['qq'],
15
+ permissions: [platformPermit('manage_roles')],
16
+ async execute({ guild_id, channel_id, user_id, role_id }: { guild_id: string; channel_id: string; user_id: string; role_id: string }, context) {
17
+ const client = context.$client;
18
+ const success = await client.removeMemberRole(guild_id, channel_id, user_id, role_id);
19
+ return { success, message: success ? '已移除成员的角色' : '操作失败' };
20
+ },
21
+ });
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { qqEndpointCommands } from "../../../lib/qq-endpoint-commands.js";
3
+ export default qqEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { qqEndpointCommands } from '../../../src/qq-endpoint-commands.js';
2
+
3
+ export default qqEndpointCommands.add;
@@ -0,0 +1,12 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { isQqEndpointOperator, QQ_ENDPOINT_FORBIDDEN, runQqEndpointCancel, } from "../../lib/qq-endpoint-commands.js";
4
+ import { qqRuntimeStateToken } from "../../lib/qq-runtime-state.js";
5
+ export default defineCommand({
6
+ description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
7
+ execute({ config, input, use }) {
8
+ if (!isQqEndpointOperator(config, input))
9
+ return QQ_ENDPOINT_FORBIDDEN;
10
+ return runQqEndpointCancel(use(qqRuntimeStateToken));
11
+ },
12
+ });
@@ -0,0 +1,15 @@
1
+ import { defineCommand } from 'zhin.js/command';
2
+ import {
3
+ isQqEndpointOperator,
4
+ QQ_ENDPOINT_FORBIDDEN,
5
+ runQqEndpointCancel,
6
+ } from '../../src/qq-endpoint-commands.js';
7
+ import { qqRuntimeStateToken } from '../../src/qq-runtime-state.js';
8
+
9
+ export default defineCommand({
10
+ description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
11
+ execute({ config, input, use }) {
12
+ if (!isQqEndpointOperator(config, input)) return QQ_ENDPOINT_FORBIDDEN;
13
+ return runQqEndpointCancel(use(qqRuntimeStateToken));
14
+ },
15
+ });
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { qqEndpointCommands } from "../../lib/qq-endpoint-commands.js";
3
+ export default qqEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { qqEndpointCommands } from '../../src/qq-endpoint-commands.js';
2
+
3
+ export default qqEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { qqEndpointCommands } from "../../../lib/qq-endpoint-commands.js";
3
+ export default qqEndpointCommands.remove;
@@ -0,0 +1,3 @@
1
+ import { qqEndpointCommands } from '../../../src/qq-endpoint-commands.js';
2
+
3
+ export default qqEndpointCommands.remove;
@@ -0,0 +1,12 @@
1
+ import type { QqInboundMessage } from './protocol.js';
2
+ import type { QqBotTransport } from './ws.js';
3
+ export type QqClientEventMap = Record<string, QqInboundMessage | unknown>;
4
+ declare module '@zhin.js/feature-kit' {
5
+ interface AdapterClientRegistry {
6
+ readonly qq: {
7
+ readonly client: QqBotTransport;
8
+ readonly events: QqClientEventMap;
9
+ };
10
+ }
11
+ }
12
+ export declare const qqClient: import("@zhin.js/adapter").EndpointClientToken<QqBotTransport, QqClientEventMap>;
package/lib/client.js ADDED
@@ -0,0 +1,2 @@
1
+ import { defineEndpointClient } from 'zhin.js/adapter';
2
+ export const qqClient = defineEndpointClient('qq');
@@ -0,0 +1,56 @@
1
+ /**
2
+ * QQ endpoints — lifecycle, outbound, admit, agent tool surface.
3
+ */
4
+ import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
5
+ import type { HttpHost } from '@zhin.js/host-http';
6
+ import type { CapabilityId } from 'zhin.js';
7
+ import { type QqInboundMessage, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig } from './protocol.js';
8
+ import { type CreateQqHttpBot, type QqHttpBotTransport } from './webhook.js';
9
+ import { type CreateQqBot, type QqBotTransport } from './ws.js';
10
+ export type { CreateQqBot, QqBotTransport, QqOfficialApi } from './ws.js';
11
+ export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
12
+ export interface QqEndpointOptions {
13
+ readonly id: CapabilityId;
14
+ readonly config: ResolvedQqWebsocketConfig;
15
+ readonly createBot?: CreateQqBot;
16
+ }
17
+ export declare class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
18
+ #private;
19
+ readonly management: EndpointManagement;
20
+ readonly control: EndpointControl;
21
+ constructor(options: QqEndpointOptions);
22
+ get client(): QqBotTransport;
23
+ /** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
24
+ get name(): string;
25
+ start(): Promise<void>;
26
+ open(): void;
27
+ close(): void;
28
+ stop(): Promise<void>;
29
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
30
+ recallMessage(messageId: string): Promise<void>;
31
+ /** Test / internal: admit a message when open. */
32
+ admit(msg: QqInboundMessage): void;
33
+ }
34
+ export interface QqHttpEndpointOptions {
35
+ readonly id: CapabilityId;
36
+ readonly http: HttpHost;
37
+ readonly config: ResolvedQqHttpConfig;
38
+ readonly createBot?: CreateQqHttpBot;
39
+ }
40
+ /** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
41
+ export declare class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
42
+ #private;
43
+ readonly management: EndpointManagement;
44
+ readonly control: EndpointControl;
45
+ constructor(options: QqHttpEndpointOptions);
46
+ get client(): QqHttpBotTransport;
47
+ /** Live endpoint 名(Console/AdapterIndex 展示用)。 */
48
+ get name(): string;
49
+ start(): Promise<void>;
50
+ open(): void;
51
+ close(): void;
52
+ stop(): Promise<void>;
53
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
54
+ recallMessage(messageId: string): Promise<void>;
55
+ admit(msg: QqInboundMessage): void;
56
+ }
@@ -0,0 +1,456 @@
1
+ /**
2
+ * QQ endpoints — lifecycle, outbound, admit, agent tool surface.
3
+ */
4
+ import { Endpoint, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
5
+ import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
6
+ import { formatOutbound } from './outbound.js';
7
+ import { formatInboundContent, parseCompoundMessageId, qqInboundConversation, qqOutboundKind, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
8
+ import { defaultCreateHttpBot, registerQqWebhookRoutes, } from './webhook.js';
9
+ import { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
10
+ import { bindQqBotSideEvents, receiveQqSideEvent, } from './side-event-dispatch.js';
11
+ export class QqWebsocketEndpoint extends Endpoint {
12
+ #logger;
13
+ #options;
14
+ #createBot;
15
+ #bot = null;
16
+ #open = false;
17
+ #lifecycle;
18
+ #cleanup;
19
+ management = createQqEndpointManagement(() => this.#requireBot());
20
+ control = createRecallEndpointControl((id) => this.recallMessage(id));
21
+ constructor(options) {
22
+ super();
23
+ this.#logger = getAdapterLogger('qq', options.config.id);
24
+ this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
25
+ this.#options = options;
26
+ this.#createBot = options.createBot ?? defaultCreateBot;
27
+ }
28
+ get client() {
29
+ return this.#requireBot();
30
+ }
31
+ /** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
32
+ get name() {
33
+ return this.#options.config.id;
34
+ }
35
+ async start() {
36
+ await this.#lifecycle.start(async (handle) => {
37
+ const bot = this.#createBot(this.#options.config);
38
+ this.#bot = bot;
39
+ let cancelled = false;
40
+ let stopping;
41
+ const stopBot = async () => {
42
+ try {
43
+ bot.removeAllListeners();
44
+ }
45
+ catch { /* continue SDK cleanup */ }
46
+ try {
47
+ await bot.stop();
48
+ }
49
+ catch { /* best-effort SDK cleanup */ }
50
+ };
51
+ const cleanup = () => {
52
+ cancelled = true;
53
+ if (this.#bot === bot)
54
+ this.#bot = null;
55
+ return stopping ??= stopBot();
56
+ };
57
+ this.#cleanup = cleanup;
58
+ handle.onForceClose(() => { void cleanup(); });
59
+ try {
60
+ this.#bindBot(bot);
61
+ await bot.start();
62
+ if (cancelled) {
63
+ // SDK startup may create resources after stop; close this exact old client again.
64
+ await stopping;
65
+ await stopBot();
66
+ return;
67
+ }
68
+ this.#logger.info(`connected (websocket) | appid: ${this.#options.config.appid}`);
69
+ }
70
+ catch (error) {
71
+ const wasCancelled = cancelled;
72
+ await cleanup();
73
+ if (wasCancelled) {
74
+ await stopBot();
75
+ return;
76
+ }
77
+ const raw = error instanceof Error ? error.message : String(error);
78
+ throw new Error(`QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`, { cause: error });
79
+ }
80
+ });
81
+ }
82
+ open() {
83
+ this.#open = true;
84
+ }
85
+ close() {
86
+ this.#open = false;
87
+ }
88
+ async stop() {
89
+ this.#open = false;
90
+ const cleanup = this.#cleanup;
91
+ this.#cleanup = undefined;
92
+ await this.#lifecycle.stop();
93
+ await cleanup?.();
94
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
95
+ }
96
+ async send({ conversation, payload }) {
97
+ const body = formatOutbound(payload);
98
+ const kind = qqOutboundKind(conversation);
99
+ const bot = this.#requireBot();
100
+ let result;
101
+ switch (kind) {
102
+ case 'private':
103
+ result = await bot.sendPrivateMessage(conversation.id, body);
104
+ break;
105
+ case 'group':
106
+ result = await bot.sendGroupMessage(conversation.id, body);
107
+ break;
108
+ case 'channel':
109
+ result = await bot.sendGuildMessage(conversation.id, body);
110
+ break;
111
+ case 'direct':
112
+ if (!bot.sendDirectMessage)
113
+ throw new Error('QQ direct message not supported by transport');
114
+ result = await bot.sendDirectMessage(conversation.id, body);
115
+ break;
116
+ }
117
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
118
+ this.#logger.info(`send ${kind}:${conversation.id}`
119
+ + ` | id: ${messageId}`
120
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`);
121
+ return messageId;
122
+ }
123
+ async recallMessage(messageId) {
124
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
125
+ const bot = this.#requireBot();
126
+ switch (kind) {
127
+ case 'private':
128
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
129
+ break;
130
+ case 'group':
131
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
132
+ break;
133
+ case 'channel':
134
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
135
+ break;
136
+ case 'direct':
137
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
138
+ break;
139
+ }
140
+ this.#logger.debug(formatCompact({
141
+ op: 'qq_recall',
142
+ endpoint: this.#options.config.id,
143
+ kind,
144
+ messageId,
145
+ }));
146
+ }
147
+ /** Test / internal: admit a message when open. */
148
+ admit(msg) {
149
+ if (!this.#open)
150
+ return;
151
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
152
+ const content = formatInboundContent(msg);
153
+ this.#logger.info(`recv ${conversation.kind}:${conversation.id}`
154
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
155
+ + (msg.mentioned ? ' (mentioned)' : '')
156
+ + ` | ${truncatePreview(content, 80)}`);
157
+ void this.emit('message.receive', {
158
+ conversation,
159
+ message: { conversation, id: msg.id },
160
+ content,
161
+ ...(msg.segments?.length ? { segments: msg.segments } : {}),
162
+ sender: {
163
+ id: msg.authorId,
164
+ name: senderDisplayName(msg) || undefined,
165
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
166
+ },
167
+ endpointId: this.#options.config.id,
168
+ ...(msg.mentioned ? { mentioned: true } : {}),
169
+ metadata: Object.freeze({
170
+ channelKind: msg.channelKind,
171
+ userId: msg.authorId,
172
+ guildId: msg.guildId,
173
+ roles: msg.authorRoles,
174
+ }),
175
+ }).catch((err) => {
176
+ this.#logger.warn(formatCompact({
177
+ op: 'qq_gateway_receive_failed',
178
+ target: `${conversation.kind}:${conversation.id}`,
179
+ error: err instanceof Error ? err.message : String(err),
180
+ }));
181
+ });
182
+ }
183
+ #bindBot(bot) {
184
+ bindQqBotInboundEvents(bot, (raw) => {
185
+ if (this.#bot !== bot)
186
+ return;
187
+ this.#emitPlatformEvent('message', raw);
188
+ const msg = normalizeQqMessage(raw);
189
+ if (msg)
190
+ this.admit(msg);
191
+ });
192
+ bindQqBotSideEvents(bot, (eventName, raw) => {
193
+ if (this.#bot !== bot)
194
+ return;
195
+ this.#emitPlatformEvent(eventName, raw);
196
+ receiveQqSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, bot, eventName, raw, this.#logger);
197
+ });
198
+ }
199
+ #emitPlatformEvent(name, event) {
200
+ void this.emitPlatform(name, event).catch((error) => {
201
+ this.#logger.warn(formatCompact({
202
+ op: 'qq_platform_event_failed',
203
+ event: name,
204
+ error: error instanceof Error ? error.message : String(error),
205
+ }));
206
+ });
207
+ }
208
+ #requireBot() {
209
+ if (!this.#bot)
210
+ throw new Error('QQ bot not connected');
211
+ return this.#bot;
212
+ }
213
+ }
214
+ /** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
215
+ export class QqHttpEndpoint extends Endpoint {
216
+ #logger;
217
+ #options;
218
+ #createBot;
219
+ #bot = null;
220
+ #open = false;
221
+ #lifecycle;
222
+ #cleanup;
223
+ management = createQqEndpointManagement(() => this.#requireBot());
224
+ control = createRecallEndpointControl((id) => this.recallMessage(id));
225
+ constructor(options) {
226
+ super();
227
+ this.#logger = getAdapterLogger('qq', options.config.id);
228
+ this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
229
+ this.#options = options;
230
+ this.#createBot = options.createBot ?? defaultCreateHttpBot;
231
+ }
232
+ get client() {
233
+ return this.#requireBot();
234
+ }
235
+ /** Live endpoint 名(Console/AdapterIndex 展示用)。 */
236
+ get name() {
237
+ return this.#options.config.id;
238
+ }
239
+ async start() {
240
+ await this.#lifecycle.start(async (handle) => {
241
+ const bot = this.#createBot(this.#options.config);
242
+ this.#bot = bot;
243
+ let cancelled = false;
244
+ let stopping;
245
+ const routes = [];
246
+ const stopBot = async () => {
247
+ try {
248
+ bot.removeAllListeners();
249
+ }
250
+ catch { /* continue SDK cleanup */ }
251
+ try {
252
+ await bot.stop();
253
+ }
254
+ catch { /* best-effort SDK cleanup */ }
255
+ };
256
+ const cleanup = () => {
257
+ cancelled = true;
258
+ if (this.#bot === bot)
259
+ this.#bot = null;
260
+ for (const release of routes.splice(0))
261
+ release();
262
+ return stopping ??= stopBot();
263
+ };
264
+ this.#cleanup = cleanup;
265
+ handle.onForceClose(() => { void cleanup(); });
266
+ try {
267
+ this.#bindBot(bot);
268
+ routes.push(...registerQqWebhookRoutes(this.#options.http, {
269
+ config: this.#options.config,
270
+ getBot: () => cancelled ? null : bot,
271
+ }));
272
+ await bot.start();
273
+ if (cancelled) {
274
+ // SDK startup may create resources after stop; close this exact old client again.
275
+ await stopping;
276
+ await stopBot();
277
+ return;
278
+ }
279
+ this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
280
+ }
281
+ catch (error) {
282
+ const wasCancelled = cancelled;
283
+ await cleanup();
284
+ if (wasCancelled) {
285
+ await stopBot();
286
+ return;
287
+ }
288
+ throw error;
289
+ }
290
+ });
291
+ }
292
+ open() {
293
+ this.#open = true;
294
+ }
295
+ close() {
296
+ this.#open = false;
297
+ }
298
+ async stop() {
299
+ this.#open = false;
300
+ const cleanup = this.#cleanup;
301
+ this.#cleanup = undefined;
302
+ await this.#lifecycle.stop();
303
+ await cleanup?.();
304
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
305
+ }
306
+ async send({ conversation, payload }) {
307
+ const body = formatOutbound(payload);
308
+ const kind = qqOutboundKind(conversation);
309
+ const bot = this.#requireBot();
310
+ let result;
311
+ switch (kind) {
312
+ case 'private':
313
+ result = await bot.sendPrivateMessage(conversation.id, body);
314
+ break;
315
+ case 'group':
316
+ result = await bot.sendGroupMessage(conversation.id, body);
317
+ break;
318
+ case 'channel':
319
+ result = await bot.sendGuildMessage(conversation.id, body);
320
+ break;
321
+ case 'direct':
322
+ if (!bot.sendDirectMessage)
323
+ throw new Error('QQ direct message not supported by transport');
324
+ result = await bot.sendDirectMessage(conversation.id, body);
325
+ break;
326
+ }
327
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
328
+ this.#logger.info(`send ${kind}:${conversation.id}`
329
+ + ` | id: ${messageId}`
330
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`);
331
+ return messageId;
332
+ }
333
+ async recallMessage(messageId) {
334
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
335
+ const bot = this.#requireBot();
336
+ switch (kind) {
337
+ case 'private':
338
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
339
+ break;
340
+ case 'group':
341
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
342
+ break;
343
+ case 'channel':
344
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
345
+ break;
346
+ case 'direct':
347
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
348
+ break;
349
+ }
350
+ this.#logger.debug(formatCompact({
351
+ op: 'qq_recall',
352
+ endpoint: this.#options.config.id,
353
+ kind,
354
+ messageId,
355
+ }));
356
+ }
357
+ admit(msg) {
358
+ if (!this.#open)
359
+ return;
360
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
361
+ const content = formatInboundContent(msg);
362
+ this.#logger.info(`recv ${conversation.kind}:${conversation.id}`
363
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
364
+ + (msg.mentioned ? ' (mentioned)' : '')
365
+ + ` | ${truncatePreview(content, 80)}`);
366
+ void this.emit('message.receive', {
367
+ conversation,
368
+ message: { conversation, id: msg.id },
369
+ content,
370
+ ...(msg.segments?.length ? { segments: msg.segments } : {}),
371
+ sender: {
372
+ id: msg.authorId,
373
+ name: senderDisplayName(msg) || undefined,
374
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
375
+ },
376
+ endpointId: this.#options.config.id,
377
+ ...(msg.mentioned ? { mentioned: true } : {}),
378
+ metadata: Object.freeze({
379
+ channelKind: msg.channelKind,
380
+ userId: msg.authorId,
381
+ guildId: msg.guildId,
382
+ roles: msg.authorRoles,
383
+ }),
384
+ }).catch((err) => {
385
+ this.#logger.warn(formatCompact({
386
+ op: 'qq_gateway_receive_failed',
387
+ target: `${conversation.kind}:${conversation.id}`,
388
+ error: err instanceof Error ? err.message : String(err),
389
+ }));
390
+ });
391
+ }
392
+ #bindBot(bot) {
393
+ bindQqBotInboundEvents(bot, (raw) => {
394
+ if (this.#bot !== bot)
395
+ return;
396
+ this.#emitPlatformEvent('message', raw);
397
+ const msg = normalizeQqMessage(raw);
398
+ if (msg)
399
+ this.admit(msg);
400
+ });
401
+ bindQqBotSideEvents(bot, (eventName, raw) => {
402
+ if (this.#bot !== bot)
403
+ return;
404
+ this.#emitPlatformEvent(eventName, raw);
405
+ receiveQqSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, bot, eventName, raw, this.#logger);
406
+ });
407
+ }
408
+ #emitPlatformEvent(name, event) {
409
+ void this.emitPlatform(name, event).catch((error) => {
410
+ this.#logger.warn(formatCompact({
411
+ op: 'qq_platform_event_failed',
412
+ event: name,
413
+ error: error instanceof Error ? error.message : String(error),
414
+ }));
415
+ });
416
+ }
417
+ #requireBot() {
418
+ if (!this.#bot)
419
+ throw new Error('QQ bot not connected');
420
+ return this.#bot;
421
+ }
422
+ }
423
+ function createQqEndpointManagement(requireClient) {
424
+ return Object.freeze({
425
+ async listChannels() {
426
+ const channels = [];
427
+ const client = requireClient();
428
+ const guilds = await client.getGuilds();
429
+ for (const guildValue of Array.isArray(guilds) ? guilds : []) {
430
+ const guild = asRecord(guildValue);
431
+ const guildId = String(guild.id ?? guild.guild_id ?? guildValue ?? '');
432
+ if (!guildId)
433
+ continue;
434
+ const guildName = String(guild.name ?? guild.guild_name ?? guildId);
435
+ const rows = await client.getChannels(guildId);
436
+ for (const channelValue of Array.isArray(rows) ? rows : []) {
437
+ const channel = asRecord(channelValue);
438
+ const id = String(channel.id ?? channel.channel_id ?? channelValue ?? '');
439
+ if (!id)
440
+ continue;
441
+ channels.push({
442
+ id,
443
+ name: String(channel.name ?? channel.channel_name ?? id),
444
+ parent: { type: 'guild', id: guildId, name: guildName },
445
+ });
446
+ }
447
+ }
448
+ return channels;
449
+ },
450
+ });
451
+ }
452
+ function asRecord(value) {
453
+ return value !== null && typeof value === 'object'
454
+ ? value
455
+ : {};
456
+ }
package/lib/index.d.ts CHANGED
@@ -1,18 +1,8 @@
1
- import { QQAdapter } from "./adapter.js";
2
- import { PageManager } from "@zhin.js/console";
3
- declare module "zhin.js" {
4
- namespace Plugin {
5
- interface Contexts {
6
- web: PageManager;
7
- }
8
- }
9
- }
10
- declare module "zhin.js" {
11
- interface Adapters {
12
- qq: QQAdapter;
13
- }
14
- }
15
- export * from "./types.js";
16
- export { QQBot } from "./bot.js";
17
- export { QQAdapter } from "./adapter.js";
18
- //# sourceMappingURL=index.d.ts.map
1
+ export { formatInboundContent, formatOutboundText, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, type QqAdapterConfig, type QqChannelKind, type QqInboundMessage, type QqWireSegment, type ResolvedQqConfig, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig, } from './protocol.js';
2
+ export { DEFAULT_QQ_BOT_KIND, QQ_INTENTS_BY_KIND, defaultQqEndpointIntentFields, parseQqBotKind, resolveQqIntents, type QqBotKind, type QqIntent, } from './qq-intents.js';
3
+ export { formatOutbound, resolveMediaFile, type QqOutboundElem, type QqOutboundMessage, } from './outbound.js';
4
+ export { QqHttpEndpoint, QqWebsocketEndpoint, type CreateQqBot, type CreateQqHttpBot, type QqBotTransport, type QqOfficialApi, type QqEndpointOptions, type QqHttpBotTransport, type QqHttpEndpointOptions, } from './endpoint.js';
5
+ export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, type QqWebhookHandler, } from './webhook.js';
6
+ export { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
7
+ export { qqClient, type QqClientEventMap } from './client.js';
8
+ export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, } from './platform-permit.js';