@zhin.js/adapter-qq 1.1.18 → 1.1.20

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 (102) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +21 -18
  3. package/adapters/qq/index.js +4 -38
  4. package/adapters/qq/index.ts +3 -46
  5. package/agents/qq/conversation.js +1 -1
  6. package/agents/qq/conversation.ts +1 -1
  7. package/agents/qq/skills/qq/tools/add_role/index.js +2 -1
  8. package/agents/qq/skills/qq/tools/add_role/index.ts +2 -1
  9. package/agents/qq/skills/qq/tools/channel_info/index.js +1 -1
  10. package/agents/qq/skills/qq/tools/channel_info/index.ts +1 -1
  11. package/agents/qq/skills/qq/tools/create_role/index.js +1 -1
  12. package/agents/qq/skills/qq/tools/create_role/index.ts +1 -1
  13. package/agents/qq/skills/qq/tools/guild_kick_member/index.js +1 -1
  14. package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +1 -1
  15. package/agents/qq/skills/qq/tools/guild_mute_member/index.js +1 -1
  16. package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +1 -1
  17. package/agents/qq/skills/qq/tools/list_channels/index.js +1 -1
  18. package/agents/qq/skills/qq/tools/list_channels/index.ts +1 -1
  19. package/agents/qq/skills/qq/tools/list_roles/index.js +1 -1
  20. package/agents/qq/skills/qq/tools/list_roles/index.ts +1 -1
  21. package/agents/qq/skills/qq/tools/member_detail/index.js +1 -1
  22. package/agents/qq/skills/qq/tools/member_detail/index.ts +1 -1
  23. package/agents/qq/skills/qq/tools/remove_role/index.js +2 -1
  24. package/agents/qq/skills/qq/tools/remove_role/index.ts +2 -1
  25. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +1 -1
  26. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +1 -1
  27. package/agents/qq/skills/qq-directory/tools/list_guilds/index.js +1 -1
  28. package/agents/qq/skills/qq-directory/tools/list_guilds/index.ts +1 -1
  29. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +2 -2
  30. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +2 -2
  31. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +1 -1
  32. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +1 -1
  33. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +2 -2
  34. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +2 -2
  35. package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +1 -1
  36. package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +1 -1
  37. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +1 -1
  38. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +1 -1
  39. package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +1 -1
  40. package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +1 -1
  41. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +1 -1
  42. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +1 -1
  43. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +1 -1
  44. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +1 -1
  45. package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +1 -1
  46. package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +1 -1
  47. package/commands/qq/endpoint/definition.js +41 -36
  48. package/commands/qq/endpoint/definition.ts +41 -67
  49. package/lib/adapter.d.ts +13 -0
  50. package/lib/adapter.js +27 -0
  51. package/lib/client.d.ts +7 -9
  52. package/lib/client.js +1 -2
  53. package/lib/console.d.ts +1 -6
  54. package/lib/console.js +29 -20
  55. package/lib/endpoint.d.ts +8 -27
  56. package/lib/endpoint.js +218 -165
  57. package/lib/group-admin.d.ts +14 -9
  58. package/lib/group-admin.js +21 -20
  59. package/lib/i18n.js +4 -4
  60. package/lib/image-size.d.ts +6 -1
  61. package/lib/image-size.js +2 -3
  62. package/lib/index.d.ts +5 -4
  63. package/lib/index.js +3 -3
  64. package/lib/interaction.js +1 -1
  65. package/lib/outbound.d.ts +4 -1
  66. package/lib/outbound.js +3 -2
  67. package/lib/protocol.d.ts +2 -0
  68. package/lib/qq-console-spec.js +12 -1
  69. package/lib/qq-console.d.ts +3 -4
  70. package/lib/qq-console.js +16 -6
  71. package/lib/qq-endpoint-write.d.ts +6 -10
  72. package/lib/qq-endpoint-write.js +5 -32
  73. package/lib/qq-runtime-state.d.ts +11 -10
  74. package/lib/qq-runtime-state.js +4 -5
  75. package/lib/qq-send.d.ts +7 -14
  76. package/lib/qq-send.js +20 -6
  77. package/lib/webhook.d.ts +3 -6
  78. package/lib/webhook.js +6 -49
  79. package/lib/ws.d.ts +10 -33
  80. package/lib/ws.js +21 -70
  81. package/middlewares/bot-kind-reply/index.js +1 -2
  82. package/middlewares/bot-kind-reply/index.ts +0 -2
  83. package/package.json +19 -19
  84. package/plugin.js +5 -5
  85. package/src/adapter.ts +23 -0
  86. package/src/client.ts +4 -12
  87. package/src/console.ts +27 -20
  88. package/src/endpoint.ts +218 -160
  89. package/src/group-admin.ts +28 -26
  90. package/src/i18n.ts +4 -4
  91. package/src/image-size.ts +7 -3
  92. package/src/index.ts +5 -5
  93. package/src/interaction.ts +1 -1
  94. package/src/outbound.ts +3 -2
  95. package/src/protocol.ts +2 -0
  96. package/src/qq-console-spec.ts +12 -1
  97. package/src/qq-console.ts +14 -8
  98. package/src/qq-endpoint-write.ts +10 -38
  99. package/src/qq-runtime-state.ts +11 -16
  100. package/src/qq-send.ts +28 -12
  101. package/src/webhook.ts +7 -57
  102. package/src/ws.ts +27 -105
package/lib/webhook.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * QQ HTTP webhook / middleware inbound via httpHostToken POST.
3
3
  */
4
4
  import path from 'node:path';
5
- import { Bot, ReceiverMode } from 'qq-official-bot';
5
+ import { ReceiverMode } from 'qq-official-bot';
6
6
  import { getLogger } from '@zhin.js/logger';
7
7
  const logger = getLogger('qq');
8
8
  export function registerQqWebhookRoutes(http, handler) {
@@ -28,6 +28,7 @@ export async function handleQqWebhookRequest(request, response, handler) {
28
28
  req: request,
29
29
  res: response,
30
30
  request: { body: undefined },
31
+ response: {},
31
32
  }, async () => undefined);
32
33
  }
33
34
  catch (error) {
@@ -52,52 +53,8 @@ export async function readRequestBodyText(request) {
52
53
  }
53
54
  return Buffer.concat(chunks).toString('utf8');
54
55
  }
55
- export function defaultCreateHttpBot(config) {
56
- const bot = new Bot({
57
- appid: config.appid,
58
- secret: config.secret,
59
- mode: ReceiverMode.MIDDLEWARE,
60
- platform: 'koa',
61
- sandbox: config.sandbox,
62
- dataDir: path.join(process.cwd(), 'data', 'qq'),
63
- // 关闭内置 log4js,与 WebSocket 模式一致走 zhin adapter logger
64
- logLevel: 'off',
65
- });
66
- return {
67
- api: bot,
68
- start: () => bot.start().then(() => undefined),
69
- stop: () => bot.stop().then(() => undefined),
70
- on: (event, listener) => {
71
- bot.on(event, listener);
72
- },
73
- removeAllListeners: () => {
74
- bot.removeAllListeners(undefined);
75
- },
76
- sendPrivateMessage: (userId, message, source, options) => bot.sendPrivateMessage(userId, message, source, options),
77
- sendGroupMessage: (groupId, message, source, options) => bot.sendGroupMessage(groupId, message, source, options),
78
- sendGuildMessage: (channelId, message, source, options) => bot.sendGuildMessage(channelId, message, source, options),
79
- sendDirectMessage: (guildId, message, source, options) => bot.sendDirectMessage(guildId, message, source, options),
80
- recallPrivateMessage: (userId, messageId) => bot.recallPrivateMessage(userId, messageId),
81
- recallGroupMessage: (groupId, messageId) => bot.recallGroupMessage(groupId, messageId),
82
- recallGuildMessage: (channelId, messageId) => bot.recallGuildMessage(channelId, messageId),
83
- recallDirectMessage: (guildId, messageId) => bot.recallDirectMessage(guildId, messageId),
84
- getGuilds: () => bot.guildService.getList(),
85
- getChannels: (guildId) => bot.channelService.getList(guildId),
86
- getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
87
- getGuildMember: (guildId, userId) => bot.memberService.getGuildMemberInfo(guildId, userId),
88
- getGuildRoles: (guildId) => bot.guildService.getRoles(guildId),
89
- createGuildRole: (guildId, name, color) => bot.guildService.createRole(guildId, { name, color: color || 0, hoist: 0 }),
90
- addMemberRole: async (guildId, channelId, userId, roleId) => {
91
- await bot.memberService.addMemberRole(guildId, channelId, userId, roleId);
92
- return true;
93
- },
94
- removeMemberRole: async (guildId, channelId, userId, roleId) => {
95
- await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
96
- return true;
97
- },
98
- approveGroupJoinRequest: (groupId, userId, options) => bot
99
- .approveGroupJoinRequest?.(groupId, userId, options)
100
- ?? Promise.reject(new Error('approveGroupJoinRequest is not available')),
101
- middleware: (ctx, next) => bot.middleware(ctx, next),
102
- };
56
+ export function qqHttpBotOptions(config) {
57
+ return { handleProcessErrors: false, appid: config.appid, secret: config.secret,
58
+ mode: ReceiverMode.MIDDLEWARE, platform: 'koa', sandbox: config.sandbox,
59
+ dataDir: path.join(process.cwd(), 'data', 'qq'), logLevel: 'off' };
103
60
  }
package/lib/ws.d.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { Bot, type ApproveJoinRequestOptions } from 'qq-official-bot';
1
+ import { Bot, ReceiverMode } from 'qq-official-bot';
2
2
  import type { EndpointTransportState } from 'zhin.js/adapter';
3
- import type { QqOutboundMessage } from './outbound.js';
4
- import type { QqReplySource } from './passive-reply.js';
5
3
  /** Per-send options. `msgSeq` numbers replies to one passive source (qq-official-bot ≥ 1.3.3). */
6
4
  export interface QqSendOptions {
7
5
  readonly msgSeq?: number;
@@ -9,35 +7,7 @@ export interface QqSendOptions {
9
7
  import type { QqInboundMessage, ResolvedQqWebsocketConfig } from './protocol.js';
10
8
  /** SDK business surface exposed to plugins without bypassing Endpoint lifecycle ownership. */
11
9
  export type QqOfficialApi = Pick<Bot, 'messageService' | 'fileProcessor' | 'guildService' | 'channelService' | 'memberService' | 'permissionService' | 'reactionService' | 'scheduleService' | 'threadService' | 'audioService' | 'botService' | 'groupService' | 'menuPanelService' | 'group' | 'user' | 'channel' | 'direct' | 'guild'>;
12
- /** Minimal bot surface used by the endpoint (real qq-official-bot or test mock). */
13
- export interface QqBotTransport {
14
- readonly transportState?: EndpointTransportState;
15
- /** Full qq-official-bot business API; lifecycle remains owned by the Endpoint. */
16
- readonly api: QqOfficialApi;
17
- start(): Promise<void>;
18
- stop(): Promise<void>;
19
- on(event: string, listener: (...args: unknown[]) => void): void;
20
- removeAllListeners(): void;
21
- /** `source` makes it a passive reply; without it the platform treats the message as active. */
22
- sendPrivateMessage(userId: string, message: QqOutboundMessage, source?: QqReplySource, options?: QqSendOptions): Promise<unknown>;
23
- sendGroupMessage(groupId: string, message: QqOutboundMessage, source?: QqReplySource, options?: QqSendOptions): Promise<unknown>;
24
- sendGuildMessage(channelId: string, message: QqOutboundMessage, source?: QqReplySource, options?: QqSendOptions): Promise<unknown>;
25
- sendDirectMessage?(guildId: string, message: QqOutboundMessage, source?: QqReplySource, options?: QqSendOptions): Promise<unknown>;
26
- recallPrivateMessage?(userId: string, messageId: string): Promise<unknown>;
27
- recallGroupMessage?(groupId: string, messageId: string): Promise<unknown>;
28
- recallGuildMessage?(channelId: string, messageId: string): Promise<unknown>;
29
- recallDirectMessage?(guildId: string, messageId: string): Promise<unknown>;
30
- getGuilds(): Promise<unknown[]>;
31
- getChannels(guildId: string): Promise<unknown[]>;
32
- getChannelInfo(channelId: string): Promise<unknown>;
33
- getGuildMember(guildId: string, userId: string): Promise<unknown>;
34
- getGuildRoles(guildId: string): Promise<unknown[]>;
35
- createGuildRole(guildId: string, name: string, color?: number): Promise<unknown>;
36
- addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
37
- removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
38
- approveGroupJoinRequest?(groupId: string, userId: string, options: ApproveJoinRequestOptions): Promise<unknown>;
39
- }
40
- export type CreateQqBot = (config: ResolvedQqWebsocketConfig) => QqBotTransport;
10
+ export type QqBotTransport = Bot;
41
11
  interface QqOfficialBotLifecycle {
42
12
  stop(): Promise<unknown>;
43
13
  readonly sessionManager: {
@@ -59,5 +29,12 @@ interface QqOfficialBotLifecycle {
59
29
  export declare function stopQqOfficialBot(bot: QqOfficialBotLifecycle): Promise<void>;
60
30
  export declare function bindQqBotInboundEvents(bot: QqBotTransport, admitRaw: (raw: unknown) => void): void;
61
31
  export declare function normalizeQqMessage(raw: unknown): QqInboundMessage | null;
62
- export declare function defaultCreateBot(config: ResolvedQqWebsocketConfig): QqBotTransport;
32
+ export declare function qqWebsocketBotOptions(config: ResolvedQqWebsocketConfig): ConstructorParameters<typeof Bot<ReceiverMode.WEBSOCKET>>[0];
33
+ /** SDK 1.3.3 has no public receiver configuration/state accessor. This seam only reads its verified native fields. */
34
+ export declare function configureQqWebsocketReceiver(bot: Bot<ReceiverMode.WEBSOCKET>, config: ResolvedQqWebsocketConfig): {
35
+ state(): EndpointTransportState;
36
+ dispose(): void;
37
+ };
38
+ /** Session.stop leaves Auth refresh alive for reconnect; actual instance retirement destroys it. */
39
+ export declare function disposeQqAuthorization(bot: Bot): void;
63
40
  export {};
package/lib/ws.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import path from 'node:path';
5
5
  import { createQqStreamAgent } from './stream-proxy.js';
6
- import { Bot, ReceiverMode, } from 'qq-official-bot';
6
+ import { ReceiverMode, } from 'qq-official-bot';
7
7
  /**
8
8
  * qq-official-bot 1.2 used a separate connection manager; 1.3 moved WebSocket
9
9
  * ownership into the receiver, but Session.stop() still leaves the auth refresh
@@ -72,6 +72,8 @@ export function normalizeQqMessage(raw) {
72
72
  && msg.mentions.some((m) => m?.is_you === true || (m?.bot === true && channelKind !== 'group'));
73
73
  return {
74
74
  id: String(msg.message_id),
75
+ event: raw,
76
+ ...(typeof msg.timestamp === 'number' && Number.isFinite(msg.timestamp) ? { platformSentAt: msg.timestamp * 1_000 } : {}),
75
77
  content,
76
78
  channelKind,
77
79
  channelId,
@@ -183,87 +185,36 @@ function extractTextContent(message) {
183
185
  })
184
186
  .join('');
185
187
  }
186
- export function defaultCreateBot(config) {
187
- const bot = new Bot({
188
- ...{ handleProcessErrors: false },
189
- appid: config.appid,
190
- secret: config.secret,
191
- mode: ReceiverMode.WEBSOCKET,
192
- sandbox: config.sandbox,
193
- dataDir: path.join(process.cwd(), 'data', 'qq'),
194
- // 关闭 qq-official-bot 内置 log4js,入/出站由 getAdapterLogger('qq', …) 统一格式
195
- logLevel: 'off',
188
+ export function qqWebsocketBotOptions(config) {
189
+ return { handleProcessErrors: false, appid: config.appid, secret: config.secret,
190
+ mode: ReceiverMode.WEBSOCKET, sandbox: config.sandbox,
191
+ dataDir: path.join(process.cwd(), 'data', 'qq'), logLevel: 'off',
196
192
  ...(config.intents ? { intents: config.intents } : {}),
197
193
  ...(config.accessTokenUrl ? { accessTokenUrl: config.accessTokenUrl } : {}),
198
- ...(config.gatewayUrl ? { gatewayUrl: config.gatewayUrl } : {}),
199
- });
194
+ ...(config.gatewayUrl ? { gatewayUrl: config.gatewayUrl } : {}), };
195
+ }
196
+ /** SDK 1.3.3 has no public receiver configuration/state accessor. This seam only reads its verified native fields. */
197
+ export function configureQqWebsocketReceiver(bot, config) {
200
198
  const receiver = bot.receiver;
201
199
  receiver.config.autoReconnect = false;
202
200
  const agent = config.streamProxy ? createQqStreamAgent(config.streamProxy) : undefined;
203
201
  receiver.config.agent = agent;
204
- const receiverListeners = [];
205
- // Keep receiver error ownership after Session.start removes its temporary listener.
206
- const receiverError = () => { };
207
- receiver.on('error', receiverError);
202
+ const observeError = () => undefined;
203
+ bot.receiver.on('error', observeError);
208
204
  return {
209
- get transportState() {
210
- const receiver = bot.receiver;
205
+ state() {
211
206
  const state = receiver.handler?.ws?.readyState;
212
207
  if (state === 1 && receiver.isReady && receiver.isClosed === false)
213
208
  return 'open';
214
209
  if (state === 0)
215
210
  return 'connecting';
216
- if (receiver.isStarted)
217
- return 'reconnecting';
218
- return 'closed';
219
- },
220
- api: bot,
221
- start: () => bot.start().then(() => undefined),
222
- stop: async () => {
223
- try {
224
- await stopQqOfficialBot(bot);
225
- }
226
- finally {
227
- agent?.destroy();
228
- }
229
- },
230
- on: (event, listener) => {
231
- if (event === 'transport.close') {
232
- receiver.on('close', listener);
233
- receiverListeners.push({ event: 'close', listener });
234
- }
235
- else
236
- bot.on(event, listener);
211
+ return receiver.isStarted ? 'reconnecting' : 'closed';
237
212
  },
238
- removeAllListeners: () => {
239
- for (const entry of receiverListeners.splice(0))
240
- receiver.removeListener(entry.event, entry.listener);
241
- bot.removeAllListeners(undefined);
242
- },
243
- sendPrivateMessage: (userId, message, source, options) => bot.sendPrivateMessage(userId, message, source, options),
244
- sendGroupMessage: (groupId, message, source, options) => bot.sendGroupMessage(groupId, message, source, options),
245
- sendGuildMessage: (channelId, message, source, options) => bot.sendGuildMessage(channelId, message, source, options),
246
- sendDirectMessage: (guildId, message, source, options) => bot.sendDirectMessage(guildId, message, source, options),
247
- recallPrivateMessage: (userId, messageId) => bot.recallPrivateMessage(userId, messageId),
248
- recallGroupMessage: (groupId, messageId) => bot.recallGroupMessage(groupId, messageId),
249
- recallGuildMessage: (channelId, messageId) => bot.recallGuildMessage(channelId, messageId),
250
- recallDirectMessage: (guildId, messageId) => bot.recallDirectMessage(guildId, messageId),
251
- getGuilds: () => bot.guildService.getList(),
252
- getChannels: (guildId) => bot.channelService.getList(guildId),
253
- getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
254
- getGuildMember: (guildId, userId) => bot.memberService.getGuildMemberInfo(guildId, userId),
255
- getGuildRoles: (guildId) => bot.guildService.getRoles(guildId),
256
- createGuildRole: (guildId, name, color) => bot.guildService.createRole(guildId, { name, color: color || 0, hoist: 0 }),
257
- addMemberRole: async (guildId, channelId, userId, roleId) => {
258
- await bot.memberService.addMemberRole(guildId, channelId, userId, roleId);
259
- return true;
260
- },
261
- removeMemberRole: async (guildId, channelId, userId, roleId) => {
262
- await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
263
- return true;
264
- },
265
- approveGroupJoinRequest: (groupId, userId, options) => bot
266
- .approveGroupJoinRequest?.(groupId, userId, options)
267
- ?? Promise.reject(new Error('approveGroupJoinRequest is not available')),
213
+ dispose() { bot.receiver.off('error', observeError); agent?.destroy(); },
268
214
  };
269
215
  }
216
+ /** Session.stop leaves Auth refresh alive for reconnect; actual instance retirement destroys it. */
217
+ export function disposeQqAuthorization(bot) {
218
+ const session = bot.sessionManager;
219
+ session.authManager.destroy();
220
+ }
@@ -1,7 +1,6 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../lib/i18n.js";
3
3
  import { defineMiddleware } from 'zhin.js/middleware';
4
- import { endpointConfigurationStoreToken } from 'zhin.js/adapter';
5
4
  import { completeQqPendingBotKind, } from '../../commands/qq/endpoint/definition.js';
6
5
  import { parseQqBotKindAnswer, qqCommandSessionKey, qqBotKindPrompt, } from "../../lib/qq-bot-kind-prompt.js";
7
6
  import { qqRuntimeStateToken } from "../../lib/qq-runtime-state.js";
@@ -37,7 +36,7 @@ export default defineMiddleware({
37
36
  return;
38
37
  }
39
38
  try {
40
- const text = await completeQqPendingBotKind(pending, botKind, context.use(endpointConfigurationStoreToken));
39
+ const text = await completeQqPendingBotKind(pending, botKind);
41
40
  state.pendingBotKind = null;
42
41
  await context.input.$reply?.(text);
43
42
  }
@@ -1,7 +1,6 @@
1
1
  import { messages as t } from '../../src/i18n.js';
2
2
  import { defineMiddleware } from 'zhin.js/middleware';
3
3
  import type { CommandMessage } from 'zhin.js/command';
4
- import { endpointConfigurationStoreToken } from 'zhin.js/adapter';
5
4
  import {
6
5
  completeQqPendingBotKind,
7
6
  } from '../../commands/qq/endpoint/definition.js';
@@ -53,7 +52,6 @@ export default defineMiddleware<CommandMessage>({
53
52
  const text = await completeQqPendingBotKind(
54
53
  pending,
55
54
  botKind,
56
- context.use(endpointConfigurationStoreToken),
57
55
  );
58
56
  state.pendingBotKind = null;
59
57
  await context.input.$reply?.(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-qq",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,27 +34,27 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "qq-official-bot": "^1.3.3",
37
- "@zhin.js/adapter": "1.1.25",
38
- "@zhin.js/agent-feature": "1.1.10",
39
- "@zhin.js/client": "1.1.21",
37
+ "@zhin.js/adapter": "1.1.26",
38
+ "@zhin.js/agent-feature": "1.1.11",
39
+ "@zhin.js/client": "1.1.22",
40
40
  "@zhin.js/console-contract": "1.1.3",
41
- "@zhin.js/core": "1.1.53",
42
- "@zhin.js/feature-kit": "1.1.9",
43
- "@zhin.js/host-http": "1.1.11",
41
+ "@zhin.js/core": "1.1.54",
42
+ "@zhin.js/feature-kit": "1.1.10",
43
+ "@zhin.js/host-http": "1.1.12",
44
44
  "@zhin.js/im-contract": "1.1.9",
45
45
  "@zhin.js/logger": "1.1.2",
46
- "@zhin.js/page": "1.1.10",
47
- "@zhin.js/permission": "1.1.9",
48
- "@zhin.js/skill": "1.1.10",
49
- "@zhin.js/tool": "1.1.12"
46
+ "@zhin.js/page": "1.1.12",
47
+ "@zhin.js/permission": "1.1.10",
48
+ "@zhin.js/skill": "1.1.11",
49
+ "@zhin.js/tool": "1.1.13"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "zod": "^4.0.0",
53
- "@zhin.js/adapter": "^1.1.25",
54
- "@zhin.js/command": "^1.1.11",
55
- "@zhin.js/core": "^1.1.53",
56
- "@zhin.js/middleware": "^1.1.10",
57
- "zhin.js": "^1.1.18"
53
+ "@zhin.js/adapter": "^1.1.26",
54
+ "@zhin.js/command": "^1.1.12",
55
+ "@zhin.js/core": "^1.1.54",
56
+ "@zhin.js/middleware": "^1.1.11",
57
+ "zhin.js": "^1.1.19"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "@zhin.js/command": {
@@ -75,8 +75,8 @@
75
75
  "typescript": "^6.0.3",
76
76
  "vitest": "^4.1.10",
77
77
  "zod": "^4.4.3",
78
- "@zhin.js/host-http": "1.1.11",
79
- "zhin.js": "1.1.18"
78
+ "@zhin.js/host-http": "1.1.12",
79
+ "zhin.js": "1.1.19"
80
80
  },
81
81
  "files": [
82
82
  "adapters",
@@ -109,7 +109,7 @@
109
109
  "features": [
110
110
  {
111
111
  "package": "@zhin.js/adapter",
112
- "api": "^1.0.0"
112
+ "api": "^2.0.0"
113
113
  },
114
114
  {
115
115
  "package": "@zhin.js/command",
package/plugin.js CHANGED
@@ -4,6 +4,7 @@ import { permissionHostToken } from '@zhin.js/permission';
4
4
  import { checkQqPlatformPermit } from "./lib/platform-permit.js";
5
5
  import { createQqRuntimeState, qqRuntimeStateToken } from "./lib/qq-runtime-state.js";
6
6
  import { createQqConsoleRpc } from "./lib/console.js";
7
+ import { cancelQqConsoleBind } from "./lib/qq-console.js";
7
8
  export default definePlugin({
8
9
  name: 'qq',
9
10
  metadata: {
@@ -12,10 +13,9 @@ export default definePlugin({
12
13
  setup(context) {
13
14
  const state = createQqRuntimeState();
14
15
  context.resources.provide(qqRuntimeStateToken, state);
15
- context.resources.provide(pluginConsoleRpcToken, createQqConsoleRpc(state, context.resources, () => context.config.get()));
16
- if (context.resources.has(permissionHostToken)) {
17
- const host = context.resources.use(permissionHostToken);
18
- return host.registerPlatform('qq', checkQqPlatformPermit);
19
- }
16
+ context.resources.provide(pluginConsoleRpcToken, createQqConsoleRpc(state, () => context.config.get()));
17
+ const releasePermission = context.resources.has(permissionHostToken)
18
+ ? context.resources.use(permissionHostToken).registerPlatform('qq', checkQqPlatformPermit) : undefined;
19
+ return () => { cancelQqConsoleBind(state); releasePermission?.(); };
20
20
  },
21
21
  });
package/src/adapter.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Adapter, createAdapterOutageNotifier, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
2
+ import { outboundHostToken } from 'zhin.js';
3
+ import { httpHostToken } from '@zhin.js/host-http';
4
+ import { QqHttpEndpoint, QqWebsocketEndpoint } from './endpoint.js';
5
+ import { resolveQqConfig, type QqEndpointConfig } from './protocol.js';
6
+ import type { QqClientEventMap } from './client.js';
7
+
8
+ export class QqAdapter extends Adapter<QqHttpEndpoint | QqWebsocketEndpoint, QqEndpointConfig, QqClientEventMap> {
9
+ constructor(private readonly context: AdapterContext<QqEndpointConfig>) { super(); }
10
+ override get configurationIdentityFields(): readonly string[] { return ['id', 'appid']; }
11
+ override get configurationIdentityProbeFields(): readonly string[] { return ['accessTokenUrl', 'gatewayUrl']; }
12
+ override get configurationUpdateFields(): readonly string[] { return ['secret', 'mode', 'sandbox', 'intents', 'botKind',
13
+ 'accessTokenUrl', 'gatewayUrl', 'streamProxy', 'streamProxy.port', 'streamProxy.serverName', 'webhookPath']; }
14
+ protected override validateConfiguration(input: QqEndpointConfig): void { resolveQqConfig(input); }
15
+ protected createEndpoint(input: QqEndpointConfig, execution: EndpointCreationContext): QqHttpEndpoint | QqWebsocketEndpoint {
16
+ const config = resolveQqConfig(input);
17
+ const outbound = this.context.has?.(outboundHostToken) ? this.context.use(outboundHostToken) : undefined;
18
+ const notifier = createAdapterOutageNotifier(outbound, config.notify);
19
+ return config.mode === 'websocket'
20
+ ? new QqWebsocketEndpoint({ id: execution.canonicalId, config, notifier })
21
+ : new QqHttpEndpoint({ id: execution.canonicalId, config, notifier, http: this.context.use(httpHostToken) });
22
+ }
23
+ }
package/src/client.ts CHANGED
@@ -1,16 +1,8 @@
1
- import { defineEndpointClient } from 'zhin.js/adapter';
2
- import type { QqInboundMessage } from './protocol.js';
3
- import type { QqBotTransport } from './ws.js';
1
+ import type { EventMap } from 'qq-official-bot';
2
+ import type { QqAdapter } from './adapter.js';
4
3
 
5
- export type QqClientEventMap = Record<string, QqInboundMessage | unknown>;
4
+ export type QqClientEventMap = { readonly [K in keyof EventMap]: EventMap[K] extends (event: infer E, ...args: never[]) => unknown ? E : never };
6
5
 
7
6
  declare module '@zhin.js/feature-kit' {
8
- interface AdapterClientRegistry {
9
- readonly qq: {
10
- readonly client: QqBotTransport;
11
- readonly events: QqClientEventMap;
12
- };
13
- }
7
+ interface AdapterRegistry { readonly qq: QqAdapter; }
14
8
  }
15
-
16
- export const qqClient = defineEndpointClient<QqBotTransport, QqClientEventMap>('qq');
package/src/console.ts CHANGED
@@ -3,11 +3,10 @@
3
3
  * (以及公域 / 私域选择、取消)搬到 Console 页面。
4
4
  */
5
5
  import {
6
- endpointConfigurationStoreToken,
6
+ prepareConsoleAdapterMutation,
7
+ ownedConsoleAdapter,
7
8
  createAdapterConsoleRpc,
8
- type AdapterConsoleResources,
9
9
  type AdapterConsoleSpec,
10
- type EndpointConfigurationStore,
11
10
  } from 'zhin.js/adapter';
12
11
  import { PluginConsoleRpcError, type PluginConsoleRpc } from 'zhin.js';
13
12
  import { qqConsoleSpec } from './qq-console-spec.js';
@@ -20,16 +19,10 @@ import type { QqRuntimeState } from './qq-runtime-state.js';
20
19
 
21
20
  export function createQqConsoleRpc(
22
21
  state: QqRuntimeState,
23
- resources: AdapterConsoleResources,
24
22
  getConfig: () => unknown,
25
23
  ): PluginConsoleRpc {
26
- const store: EndpointConfigurationStore | undefined = resources.has(endpointConfigurationStoreToken)
27
- ? resources.use(endpointConfigurationStoreToken)
28
- : undefined;
29
-
30
24
  const spec: AdapterConsoleSpec = {
31
25
  ...qqConsoleSpec,
32
- running: () => state.endpoints.values(),
33
26
  getConfig,
34
27
  extraStatus: () => {
35
28
  const bind = state.consoleBind;
@@ -45,7 +38,6 @@ export function createQqConsoleRpc(
45
38
  };
46
39
  },
47
40
  availableActions: () => {
48
- if (!store) return [];
49
41
  if (state.consoleBind?.pending) return ['qq-bind-kind', 'qq-bind-cancel'];
50
42
  if (state.bindFlow || state.consoleBind?.state === 'waiting') return ['qq-bind-cancel'];
51
43
  return ['qq-bind-start'];
@@ -54,15 +46,23 @@ export function createQqConsoleRpc(
54
46
  'qq-bind-start': {
55
47
  label: '扫码绑定新机器人',
56
48
  labelI18n: { en: 'Bind a bot with QR' },
57
- description: '用手机 QQ 扫码,成功后选择公域 / 私域,再一次性写入 .env 与配置。',
58
- descriptionI18n: { en: 'Scan with mobile QQ, choose public/private, then save credentials to .env and the config.' },
59
- handler: async () => {
60
- if (!store) throw new PluginConsoleRpcError('unsupported', '当前运行环境不支持写入 endpoint 配置');
61
- const { url } = await startQqConsoleBind(state);
49
+ description: '用手机 QQ 扫码,选择公域 / 私域后保存到账号库;保存后需手动启动。',
50
+ descriptionI18n: { en: 'Scan with mobile QQ, choose public/private and save the account; start it explicitly afterwards.' },
51
+ handler: async (input, call) => {
52
+ if (!call.runtime) throw new PluginConsoleRpcError('unsupported', '账号运行目录不可用');
53
+ const adapter = ownedConsoleAdapter(spec, call);
54
+ if (!adapter.canManage) throw new PluginConsoleRpcError('unsupported', '此环境未提供持久化账号管理');
55
+ if (input.expectedGeneration !== call.runtime.generation || input.expectedRevision !== adapter.revision) {
56
+ throw new PluginConsoleRpcError('conflict', '账号清单已变化,请刷新核实');
57
+ }
58
+ const starting = startQqConsoleBind(state);
59
+ state.consoleAdmission = { adapterId: adapter.descriptor.id, generation: call.runtime.generation, revision: adapter.revision };
60
+ const { url } = await starting;
62
61
  return { message: '请用手机 QQ 扫码;扫码成功后选择公域或私域', url };
63
62
  },
64
63
  },
65
64
  'qq-bind-kind': {
65
+ accountMutation: true,
66
66
  label: '选择公域 / 私域',
67
67
  labelI18n: { en: 'Choose public/private' },
68
68
  description: '公域订阅 PUBLIC_GUILD_MESSAGES;私域订阅 GUILD_MESSAGES。',
@@ -71,11 +71,18 @@ export function createQqConsoleRpc(
71
71
  { value: 'public', label: '公域', labelI18n: { en: 'Public' } },
72
72
  { value: 'private', label: '私域', labelI18n: { en: 'Private' } },
73
73
  ],
74
- handler: async (input) => {
75
- if (!store) throw new PluginConsoleRpcError('unsupported', '当前运行环境不支持写入 endpoint 配置');
76
- const result = await completeQqConsoleBind(state, store, input.value);
74
+ handler: async (input, call) => {
75
+ const captured = state.consoleAdmission;
76
+ if (!captured || captured.generation !== input.expectedGeneration || captured.revision !== input.expectedRevision) {
77
+ throw new PluginConsoleRpcError('conflict', '扫码期间账号清单已变化,请刷新核实');
78
+ }
79
+ const admission = prepareConsoleAdapterMutation(spec, call, input);
80
+ if (admission.adapter.descriptor.id !== captured.adapterId) throw new PluginConsoleRpcError('conflict', '扫码所属 Adapter 已变化');
81
+ const result = await completeQqConsoleBind(state, config => admission.adapter.add(admission.revision,
82
+ { endpointId: config.id, config: { ...config } }, admission.options), input.value);
77
83
  return {
78
- message: `已写入 endpoint「${result.endpointId}」(${result.botKind}),重启或热重载后生效`,
84
+ message: `已保存账号「${result.endpointId}」(${result.botKind}),尚未启动`,
85
+ saved: true, restartRequired: false, execution: { ...result.mutation.execution },
79
86
  details: { endpoint: result.endpointId, botKind: result.botKind, intents: result.intents.join(', ') },
80
87
  };
81
88
  },
@@ -92,5 +99,5 @@ export function createQqConsoleRpc(
92
99
  },
93
100
  },
94
101
  };
95
- return createAdapterConsoleRpc(spec, resources);
102
+ return createAdapterConsoleRpc(spec);
96
103
  }