@zhin.js/adapter-qq 6.0.0 → 6.0.1

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.
@@ -1,74 +1,44 @@
1
1
  /**
2
2
  * `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
3
+ * 通用部分(权限 / list / remove / 配置写回 / .env 持久化)已迁移到
4
+ * @zhin.js/adapter 的 createEndpointCommands 套件;本文件保留 QQ 特化的
5
+ * 扫码绑定 add 流程(经套件 bindFlow 钩子接入)与 cancel,并导出
6
+ * qqEndpointCommands 供 commands/endpoint/ 下的命令文件默认导出。
3
7
  */
8
+ import { createEndpointCommands, endpointCommandForbidden, extractEndpointCommandReply, formatEndpointList, isEndpointOperator, removeEndpointByName, } from '@zhin.js/adapter';
9
+ import { defineCommand } from '@zhin.js/command';
4
10
  import { startQqBindFlow } from './qq-bind-flow.js';
5
11
  import { persistQqCredentialsToEnv } from './qq-bind-persist.js';
6
- import { addQqEndpointToConfig, listQqEndpointEntries, removeQqEndpointFromConfig, } from './qq-endpoint-config.js';
12
+ import { addQqEndpointToConfig, listQqEndpointEntries } from './qq-endpoint-config.js';
13
+ import { qqRuntimeStateToken } from './qq-runtime-state.js';
7
14
  /**
8
15
  * endpoint 管理命令的操作者校验:实例配置声明了 master(顶层或任一端点项)时
9
16
  * 仅 master 可执行 add/cancel/remove;未配置则放行(首个扫码绑定者即为 owner,
10
17
  * legacy applyBindOwnership 会把 operator 写为新端点的 master)。
11
18
  */
12
19
  export function isQqEndpointOperator(config, input) {
13
- const cfg = (config ?? {});
14
- const masters = new Set();
15
- const collect = (value) => {
16
- if (value === undefined || value === null)
17
- return;
18
- const text = String(value).trim();
19
- if (text)
20
- masters.add(text);
21
- };
22
- collect(cfg.master);
23
- if (Array.isArray(cfg.endpoints)) {
24
- for (const entry of cfg.endpoints) {
25
- collect(entry?.master);
26
- }
27
- }
28
- if (masters.size === 0)
29
- return true;
30
- const sender = String(input?.sender ?? '').trim();
31
- return !!sender && masters.has(sender);
20
+ return isEndpointOperator(config, input);
32
21
  }
33
- export const QQ_ENDPOINT_FORBIDDEN = '仅 master 可执行 QQ endpoint 管理命令';
22
+ export const QQ_ENDPOINT_FORBIDDEN = endpointCommandForbidden('QQ');
34
23
  /**
35
24
  * 从命令 input(Runtime Message)提取 $reply;非消息来源(如 Host API 调用)降级为 no-op。
36
25
  */
37
26
  export function extractQqCommandReply(input) {
38
- const reply = input?.$reply;
39
- if (typeof reply === 'function') {
40
- return (text) => reply.call(input, text);
41
- }
42
- return async () => undefined;
27
+ return extractEndpointCommandReply(input);
43
28
  }
44
29
  /** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
45
30
  export function runQqEndpointList(state, projectRoot) {
46
- const running = [...state.endpoints.values()];
47
- const configured = listQqEndpointEntries(projectRoot);
48
- const lines = [];
49
- lines.push('【运行中的 QQ endpoints】');
50
- if (running.length === 0) {
51
- lines.push(' (无)');
52
- }
53
- else {
54
- for (const endpoint of running) {
55
- lines.push(` - ${endpoint.name}(${endpoint.mode})`);
56
- }
57
- }
58
- lines.push('【配置中的 QQ endpoints】(zhin.config.yml → plugins.qq.endpoints)');
59
- if (configured.length === 0) {
60
- lines.push(' (无)');
61
- }
62
- else {
63
- for (const entry of configured) {
64
- lines.push(` - ${entry.name}(appid: ${entry.appid})`);
65
- }
66
- }
67
- if (state.bindFlow) {
68
- lines.push('⚠️ 有进行中的扫码绑定,可用 qq endpoint cancel 取消');
69
- }
70
- return lines.join('\n');
31
+ return formatEndpointList(qqEndpointListSpec, {
32
+ running: state.endpoints.values(),
33
+ configured: listQqEndpointEntries(projectRoot),
34
+ footer: state.bindFlow ? '⚠️ 有进行中的扫码绑定,可用 qq endpoint cancel 取消' : undefined,
35
+ });
71
36
  }
37
+ const qqEndpointListSpec = {
38
+ adapterKey: 'qq',
39
+ adapterDisplayName: 'QQ',
40
+ describeEntry: (entry) => `appid: ${String(entry.appid)}`,
41
+ };
72
42
  /**
73
43
  * `qq endpoint add [name]`:启动扫码绑定流程。
74
44
  * 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
@@ -130,18 +100,23 @@ export function runQqEndpointCancel(state) {
130
100
  }
131
101
  /** `qq endpoint remove <name>`:从 zhin.config.yml 移除对应 endpoints 项 */
132
102
  export function runQqEndpointRemove(_state, name, projectRoot) {
133
- const trimmed = name.trim();
134
- if (!trimmed)
135
- return '用法:qq endpoint remove <name>';
136
- try {
137
- const { removed, filePath } = removeQqEndpointFromConfig(trimmed, projectRoot);
138
- if (!removed) {
139
- return `配置中不存在 qq endpoint「${trimmed}」(${filePath} → plugins.qq.endpoints)`;
140
- }
141
- return (`已从 ${filePath} 的 plugins.qq.endpoints 移除「${trimmed}」。\n` +
142
- '⚠️ 需重启 zhin 后生效(运行中的连接届时才会断开);.env 中的凭据键未删除,可手动清理。');
143
- }
144
- catch (error) {
145
- return `移除失败:${error instanceof Error ? error.message : String(error)}`;
146
- }
103
+ return removeEndpointByName(qqEndpointListSpec, name, projectRoot);
147
104
  }
105
+ /**
106
+ * 通用套件生成的 QQ endpoint 命令(add 经 bindFlow 钩子走扫码绑定)。
107
+ * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项;cancel 为 QQ 特化,单独定义。
108
+ */
109
+ export const qqEndpointCommands = createEndpointCommands({
110
+ ...qqEndpointListSpec,
111
+ fields: [
112
+ { key: 'appid', required: true, env: true, description: 'QQ 机器人 appid' },
113
+ { key: 'secret', required: true, env: true, description: 'QQ 机器人 secret' },
114
+ ],
115
+ running: (use) => use(qqRuntimeStateToken).endpoints.values(),
116
+ listFooter: (use) => {
117
+ const state = use(qqRuntimeStateToken);
118
+ return state.bindFlow ? '⚠️ 有进行中的扫码绑定,可用 qq endpoint cancel 取消' : undefined;
119
+ },
120
+ addDescription: '手机 QQ 扫码绑定机器人,凭据写入 .env 并追加到 zhin.config.yml(重启生效)',
121
+ bindFlow: ({ name, reply, use }) => runQqEndpointAdd(use(qqRuntimeStateToken), name, reply),
122
+ }, defineCommand);
@@ -1,110 +1,22 @@
1
1
  /**
2
2
  * zhin.config.yml 中 plugins.qq.endpoints 的读写(Plugin Runtime 版)。
3
- * 用 yaml Document 操作以保留现有注释;仅支持 .yml/.yaml,其它格式报错提示手动处理。
3
+ * 实现已泛化到 @zhin.js/adapter 的 endpoint-commands 套件(Document 节点级保留注释);
4
+ * 本文件仅保留 QQ 专用签名以兼容既有调用与测试。
4
5
  */
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import { isMap, isSeq, parseDocument } from 'yaml';
8
- import { resolveProjectRoot } from './qq-bind-persist.js';
9
- const CONFIG_BASENAME = 'zhin.config';
10
- const YAML_EXTENSIONS = ['.yml', '.yaml'];
6
+ import { addEndpointToConfig, findEndpointConfigFile, listConfiguredEndpoints, removeEndpointFromConfig, } from '@zhin.js/adapter';
11
7
  /** 定位项目配置文件:ZHIN_CONFIG 指定优先,否则发现 zhin.config.yml/.yaml,都没有则默认新建 zhin.config.yml */
12
8
  export function findQqConfigFile(projectRoot) {
13
- const root = projectRoot ?? resolveProjectRoot();
14
- const envConfig = process.env.ZHIN_CONFIG?.trim();
15
- if (envConfig)
16
- return path.resolve(root, envConfig);
17
- for (const ext of YAML_EXTENSIONS) {
18
- const candidate = path.join(root, `${CONFIG_BASENAME}${ext}`);
19
- if (fs.existsSync(candidate))
20
- return candidate;
21
- }
22
- for (const ext of ['.json', '.toml', '.ts']) {
23
- const candidate = path.join(root, `${CONFIG_BASENAME}${ext}`);
24
- if (fs.existsSync(candidate)) {
25
- throw new Error(`暂不支持写入 ${ext} 配置文件,请手动在 ${CONFIG_BASENAME}${ext} 的 plugins.qq.endpoints 中维护`);
26
- }
27
- }
28
- return path.join(root, `${CONFIG_BASENAME}.yml`);
29
- }
30
- function readConfigDocument(projectRoot) {
31
- const filePath = findQqConfigFile(projectRoot);
32
- const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
33
- const doc = parseDocument(content || '{}');
34
- return { filePath, doc };
35
- }
36
- function writeConfigDocument({ filePath, doc }) {
37
- fs.writeFileSync(filePath, doc.toString());
9
+ return findEndpointConfigFile('qq', projectRoot);
38
10
  }
39
11
  /** 读取 plugins.qq.endpoints(plain JS);plugins/qq 缺失或形态不符时返回 [] */
40
12
  export function listQqEndpointEntries(projectRoot) {
41
- const { doc } = readConfigDocument(projectRoot);
42
- const plugins = doc.toJS()?.plugins;
43
- if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins))
44
- return [];
45
- const endpoints = plugins.qq?.endpoints;
46
- if (!Array.isArray(endpoints))
47
- return [];
48
- return endpoints.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.name === 'string');
49
- }
50
- function entryName(item) {
51
- if (!isMap(item))
52
- return undefined;
53
- const name = item.get('name');
54
- return typeof name === 'string' && name ? name : undefined;
55
- }
56
- /**
57
- * 确保 plugins.qq.endpoints 存在并返回其 YAMLSeq(节点级操作,保留既有条目与注释)。
58
- * `plugins: []`(legacy 空列表,Runtime 忽略)可直接替换为 map;非空数组拒绝写入。
59
- */
60
- function ensureEndpointsSeq(doc) {
61
- const plugins = doc.get('plugins');
62
- if (isSeq(plugins) && plugins.items.length > 0) {
63
- throw new Error('配置的 plugins 是数组形态(legacy 插件名列表),请手动迁移为 map 后再试');
64
- }
65
- if (plugins !== undefined && !isMap(plugins) && !isSeq(plugins)) {
66
- throw new Error('配置的 plugins 字段形态异常,请手动检查 zhin.config.yml');
67
- }
68
- if (!isMap(doc.get('plugins'))) {
69
- // 注意:空对象 {} 不会被 doc.set 自动包装为 YAMLMap,必须显式 createNode
70
- doc.set('plugins', doc.createNode({}));
71
- }
72
- const qq = doc.getIn(['plugins', 'qq']);
73
- if (qq !== undefined && !isMap(qq)) {
74
- throw new Error('配置的 plugins.qq 字段形态异常,请手动检查 zhin.config.yml');
75
- }
76
- if (!isMap(doc.getIn(['plugins', 'qq']))) {
77
- doc.setIn(['plugins', 'qq'], doc.createNode({}));
78
- }
79
- const endpoints = doc.getIn(['plugins', 'qq', 'endpoints']);
80
- if (endpoints !== undefined && !isSeq(endpoints)) {
81
- throw new Error('配置的 plugins.qq.endpoints 字段形态异常,请手动检查 zhin.config.yml');
82
- }
83
- if (!isSeq(doc.getIn(['plugins', 'qq', 'endpoints']))) {
84
- doc.setIn(['plugins', 'qq', 'endpoints'], doc.createNode([]));
85
- }
86
- return doc.getIn(['plugins', 'qq', 'endpoints']);
13
+ return listConfiguredEndpoints('qq', projectRoot);
87
14
  }
88
15
  /** 追加 endpoint 到 plugins.qq.endpoints;name 已存在时报错 */
89
16
  export function addQqEndpointToConfig(entry, projectRoot) {
90
- const document = readConfigDocument(projectRoot);
91
- const seq = ensureEndpointsSeq(document.doc);
92
- if (seq.items.some((item) => entryName(item) === entry.name)) {
93
- throw new Error(`配置中已存在 qq endpoint「${entry.name}」,可先 qq endpoint remove ${entry.name} 再重新添加`);
94
- }
95
- seq.items.push(document.doc.createNode(entry));
96
- writeConfigDocument(document);
97
- return document.filePath;
17
+ return addEndpointToConfig('qq', entry, projectRoot);
98
18
  }
99
19
  /** 按 name 移除 plugins.qq.endpoints 项;不存在返回 false */
100
20
  export function removeQqEndpointFromConfig(name, projectRoot) {
101
- const document = readConfigDocument(projectRoot);
102
- const seq = ensureEndpointsSeq(document.doc);
103
- const next = seq.items.filter((item) => entryName(item) !== name);
104
- if (next.length === seq.items.length) {
105
- return { removed: false, filePath: document.filePath };
106
- }
107
- seq.items = next;
108
- writeConfigDocument(document);
109
- return { removed: true, filePath: document.filePath };
21
+ return removeEndpointFromConfig('qq', name, projectRoot);
110
22
  }
package/lib/webhook.js CHANGED
@@ -21,20 +21,13 @@ export async function handleQqWebhookRequest(request, response, handler) {
21
21
  return;
22
22
  }
23
23
  try {
24
- let parsedBody;
25
- const raw = await readRequestBodyText(request);
26
- if (raw) {
27
- try {
28
- parsedBody = JSON.parse(raw);
29
- }
30
- catch {
31
- parsedBody = undefined;
32
- }
33
- }
24
+ // 不预消费请求流:库的验签必须基于原始字节(JSON.parse → JSON.stringify
25
+ // 会改变空白/转义/键序导致 401),body 留 undefined 让库自己读流;
26
+ // 预消费还会让非 JSON body 时库监听已结束的 end 事件而挂死。
34
27
  await bot.middleware({
35
28
  req: request,
36
29
  res: response,
37
- request: { body: parsedBody },
30
+ request: { body: undefined },
38
31
  }, async () => undefined);
39
32
  }
40
33
  catch (error) {
package/lib/ws.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { QqOutboundMessage } from './outbound.js';
1
2
  import type { QqInboundMessage, ResolvedQqWebsocketConfig } from './protocol.js';
2
3
  /** Minimal bot surface used by the endpoint (real qq-official-bot or test mock). */
3
4
  export interface QqBotTransport {
@@ -5,10 +6,10 @@ export interface QqBotTransport {
5
6
  stop(): Promise<void>;
6
7
  on(event: string, listener: (...args: unknown[]) => void): void;
7
8
  removeAllListeners(): void;
8
- sendPrivateMessage(userId: string, message: string): Promise<unknown>;
9
- sendGroupMessage(groupId: string, message: string): Promise<unknown>;
10
- sendGuildMessage(channelId: string, message: string): Promise<unknown>;
11
- sendDirectMessage?(guildId: string, message: string): Promise<unknown>;
9
+ sendPrivateMessage(userId: string, message: QqOutboundMessage): Promise<unknown>;
10
+ sendGroupMessage(groupId: string, message: QqOutboundMessage): Promise<unknown>;
11
+ sendGuildMessage(channelId: string, message: QqOutboundMessage): Promise<unknown>;
12
+ sendDirectMessage?(guildId: string, message: QqOutboundMessage): Promise<unknown>;
12
13
  getGuilds(): Promise<unknown[]>;
13
14
  getChannels(guildId: string): Promise<unknown[]>;
14
15
  getChannelInfo(channelId: string): Promise<unknown>;
@@ -19,6 +20,26 @@ export interface QqBotTransport {
19
20
  removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
20
21
  }
21
22
  export type CreateQqBot = (config: ResolvedQqWebsocketConfig) => QqBotTransport;
23
+ interface QqOfficialBotLifecycle {
24
+ stop(): Promise<unknown>;
25
+ readonly sessionManager: {
26
+ userClose: boolean;
27
+ readonly connectionManager?: {
28
+ destroy(): void;
29
+ };
30
+ readonly authManager?: {
31
+ destroy(): void;
32
+ };
33
+ };
34
+ }
35
+ /**
36
+ * qq-official-bot 1.2.x keeps a second `userClose` flag in Connection.
37
+ * Session.stop() does not update it, so a normal shutdown is treated as an
38
+ * unexpected disconnect and starts a reconnect loop. Destroy both managers at
39
+ * the SDK boundary until upstream exposes a complete public shutdown method.
40
+ */
41
+ export declare function stopQqOfficialBot(bot: QqOfficialBotLifecycle): Promise<void>;
22
42
  export declare function bindQqBotInboundEvents(bot: QqBotTransport, admitRaw: (raw: unknown) => void): void;
23
43
  export declare function normalizeQqMessage(raw: unknown): QqInboundMessage | null;
24
44
  export declare function defaultCreateBot(config: ResolvedQqWebsocketConfig): QqBotTransport;
45
+ export {};
package/lib/ws.js CHANGED
@@ -4,6 +4,23 @@
4
4
  import path from 'node:path';
5
5
  import { Bot, ReceiverMode } from 'qq-official-bot';
6
6
  import { getLevel, toLog4jsLevel } from '@zhin.js/logger';
7
+ /**
8
+ * qq-official-bot 1.2.x keeps a second `userClose` flag in Connection.
9
+ * Session.stop() does not update it, so a normal shutdown is treated as an
10
+ * unexpected disconnect and starts a reconnect loop. Destroy both managers at
11
+ * the SDK boundary until upstream exposes a complete public shutdown method.
12
+ */
13
+ export async function stopQqOfficialBot(bot) {
14
+ const session = bot.sessionManager;
15
+ session.userClose = true;
16
+ session.connectionManager?.destroy();
17
+ try {
18
+ await bot.stop();
19
+ }
20
+ finally {
21
+ session.authManager?.destroy();
22
+ }
23
+ }
7
24
  export function bindQqBotInboundEvents(bot, admitRaw) {
8
25
  bot.on('message.group', admitRaw);
9
26
  bot.on('message.guild', admitRaw);
@@ -89,7 +106,7 @@ export function defaultCreateBot(config) {
89
106
  });
90
107
  return {
91
108
  start: () => bot.start().then(() => undefined),
92
- stop: () => bot.stop().then(() => undefined),
109
+ stop: () => stopQqOfficialBot(bot),
93
110
  on: (event, listener) => {
94
111
  bot.on(event, listener);
95
112
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-qq",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -35,21 +35,20 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "qq-official-bot": "^1.2.3",
38
- "yaml": "^2.9.0",
39
- "@zhin.js/adapter": "1.1.0",
40
- "@zhin.js/core": "1.4.0",
41
- "@zhin.js/host-http": "1.0.2",
38
+ "@zhin.js/adapter": "1.1.1",
39
+ "@zhin.js/command": "1.0.3",
40
+ "@zhin.js/core": "1.4.1",
41
+ "@zhin.js/host-http": "1.0.3",
42
42
  "@zhin.js/logger": "1.0.75",
43
- "@zhin.js/plugin-runtime": "1.1.0",
44
- "@zhin.js/command": "1.0.2"
43
+ "@zhin.js/plugin-runtime": "1.1.1"
45
44
  },
46
45
  "peerDependencies": {
47
46
  "zod": "^4.0.0",
48
- "@zhin.js/adapter": "1.1.0",
49
- "@zhin.js/agent": "1.0.5",
50
- "@zhin.js/core": "1.4.0",
51
- "@zhin.js/plugin-runtime": "1.1.0",
52
- "zhin.js": "5.0.0"
47
+ "@zhin.js/adapter": "1.1.1",
48
+ "@zhin.js/agent": "1.0.6",
49
+ "@zhin.js/core": "1.4.1",
50
+ "@zhin.js/plugin-runtime": "1.1.1",
51
+ "zhin.js": "5.0.1"
53
52
  },
54
53
  "peerDependenciesMeta": {
55
54
  "zhin.js": {
@@ -67,9 +66,9 @@
67
66
  "typescript": "^6.0.3",
68
67
  "vitest": "^4.1.10",
69
68
  "zod": "^4.4.3",
70
- "@zhin.js/agent": "1.0.5",
71
- "@zhin.js/host-http": "1.0.2",
72
- "zhin.js": "5.0.0"
69
+ "@zhin.js/agent": "1.0.6",
70
+ "@zhin.js/host-http": "1.0.3",
71
+ "zhin.js": "5.0.1"
73
72
  },
74
73
  "files": [
75
74
  "adapters",
package/src/endpoint.ts CHANGED
@@ -10,11 +10,11 @@ import type { MessageGateway } from '@zhin.js/core/runtime';
10
10
  import type { HttpHost } from '@zhin.js/host-http';
11
11
  import { formatCompact, getLogger } from '@zhin.js/logger';
12
12
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
13
+ import { formatOutbound } from './outbound.js';
13
14
  import { registerQqAgentEndpoint } from './qq-agent-deps.js';
14
15
  import {
15
16
  formatInboundContent,
16
17
  formatInboundTarget,
17
- formatOutboundText,
18
18
  parseSendTarget,
19
19
  resolveOutboundMessageId,
20
20
  senderDisplayName,
@@ -84,7 +84,11 @@ export class QqWebsocketEndpoint implements EndpointInstance {
84
84
  } catch (error) {
85
85
  await this.stop();
86
86
  logger.error('Failed to connect QQ websocket:', error);
87
- throw error;
87
+ const raw = error instanceof Error ? error.message : String(error);
88
+ throw new Error(
89
+ `QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`,
90
+ { cause: error },
91
+ );
88
92
  }
89
93
  }
90
94
 
@@ -114,7 +118,7 @@ export class QqWebsocketEndpoint implements EndpointInstance {
114
118
  }
115
119
 
116
120
  async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
117
- const body = formatOutboundText(payload);
121
+ const body = formatOutbound(payload);
118
122
  const parsed = parseSendTarget(target);
119
123
  const bot = this.#requireBot();
120
124
  let result: unknown;
@@ -301,7 +305,7 @@ export class QqHttpEndpoint implements EndpointInstance {
301
305
  }
302
306
 
303
307
  async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
304
- const body = formatOutboundText(payload);
308
+ const body = formatOutbound(payload);
305
309
  const parsed = parseSendTarget(target);
306
310
  const bot = this.#requireBot();
307
311
  let result: unknown;
package/src/index.ts CHANGED
@@ -16,6 +16,13 @@ export {
16
16
  type ResolvedQqWebsocketConfig,
17
17
  } from './protocol.js';
18
18
 
19
+ export {
20
+ formatOutbound,
21
+ resolveMediaFile,
22
+ type QqOutboundElem,
23
+ type QqOutboundMessage,
24
+ } from './outbound.js';
25
+
19
26
  export {
20
27
  QqHttpEndpoint,
21
28
  QqWebsocketEndpoint,