@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.
- package/CHANGELOG.md +36 -0
- package/adapters/qq.ts +22 -9
- package/commands/endpoint/add/[name:string=].ts +2 -20
- package/commands/endpoint/list.ts +2 -9
- package/commands/endpoint/remove/[name:string].ts +2 -14
- package/lib/endpoint.js +6 -4
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/outbound.d.ts +17 -0
- package/lib/outbound.js +241 -0
- package/lib/protocol.js +15 -2
- package/lib/qq-bind-persist.d.ts +2 -2
- package/lib/qq-bind-persist.js +6 -35
- package/lib/qq-endpoint-commands.d.ts +16 -3
- package/lib/qq-endpoint-commands.js +40 -65
- package/lib/qq-endpoint-config.js +7 -95
- package/lib/webhook.js +4 -11
- package/lib/ws.d.ts +25 -4
- package/lib/ws.js +18 -1
- package/package.json +14 -15
- package/src/endpoint.ts +8 -4
- package/src/index.ts +7 -0
- package/src/outbound.ts +262 -0
- package/src/protocol.ts +16 -2
- package/src/qq-bind-persist.ts +16 -43
- package/src/qq-endpoint-commands.ts +52 -66
- package/src/qq-endpoint-config.ts +12 -103
- package/src/webhook.ts +9 -15
- package/src/ws.ts +37 -10
|
@@ -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
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
3
|
+
* 实现已泛化到 @zhin.js/adapter 的 endpoint-commands 套件(Document 节点级保留注释);
|
|
4
|
+
* 本文件仅保留 QQ 专用签名以兼容既有调用与测试。
|
|
4
5
|
*/
|
|
5
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
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:
|
|
9
|
-
sendGroupMessage(groupId: string, message:
|
|
10
|
-
sendGuildMessage(channelId: string, message:
|
|
11
|
-
sendDirectMessage?(guildId: string, message:
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/core": "1.4.
|
|
41
|
-
"@zhin.js/host-http": "1.0.
|
|
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.
|
|
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.
|
|
49
|
-
"@zhin.js/agent": "1.0.
|
|
50
|
-
"@zhin.js/core": "1.4.
|
|
51
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
52
|
-
"zhin.js": "5.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.
|
|
71
|
-
"@zhin.js/host-http": "1.0.
|
|
72
|
-
"zhin.js": "5.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
|
-
|
|
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 =
|
|
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 =
|
|
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,
|