@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.
- package/CHANGELOG.md +844 -17
- package/README.md +136 -233
- package/adapters/qq.js +50 -0
- package/adapters/qq.ts +71 -0
- package/agent/PERMITS.md +25 -0
- package/{skills/qq/SKILL.md → agent/skills/qq.md} +1 -1
- package/agent/tools/add_role.ts +21 -0
- package/agent/tools/channel_info.ts +17 -0
- package/agent/tools/create_role.ts +20 -0
- package/agent/tools/list_channels.ts +18 -0
- package/agent/tools/list_guilds.ts +14 -0
- package/agent/tools/list_roles.ts +18 -0
- package/agent/tools/member_detail.ts +18 -0
- package/agent/tools/remove_role.ts +21 -0
- package/commands/endpoint/add/[[id]].js +3 -0
- package/commands/endpoint/add/[[id]].ts +3 -0
- package/commands/endpoint/cancel.js +12 -0
- package/commands/endpoint/cancel.ts +15 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +12 -0
- package/lib/client.js +2 -0
- package/lib/endpoint.d.ts +56 -0
- package/lib/endpoint.js +456 -0
- package/lib/index.d.ts +8 -18
- package/lib/index.js +8 -338
- package/lib/outbound.d.ts +17 -0
- package/lib/outbound.js +234 -0
- package/lib/platform-permit.d.ts +15 -0
- package/lib/platform-permit.js +41 -0
- package/lib/protocol.d.ts +111 -0
- package/lib/protocol.js +166 -0
- package/lib/qq-bind-api.d.ts +36 -0
- package/lib/qq-bind-api.js +89 -0
- package/lib/qq-bind-flow.d.ts +19 -0
- package/lib/qq-bind-flow.js +85 -0
- package/lib/qq-bind-persist.d.ts +12 -0
- package/lib/qq-bind-persist.js +22 -0
- package/lib/qq-bot-kind-prompt.d.ts +9 -0
- package/lib/qq-bot-kind-prompt.js +37 -0
- package/lib/qq-endpoint-commands.d.ts +45 -0
- package/lib/qq-endpoint-commands.js +180 -0
- package/lib/qq-endpoint-config.d.ts +17 -0
- package/lib/qq-endpoint-config.js +22 -0
- package/lib/qq-intents.d.ts +27 -0
- package/lib/qq-intents.js +51 -0
- package/lib/qq-runtime-state.d.ts +30 -0
- package/lib/qq-runtime-state.js +14 -0
- package/lib/side-event-dispatch.d.ts +10 -0
- package/lib/side-event-dispatch.js +114 -0
- package/lib/webhook.d.ts +18 -0
- package/lib/webhook.js +103 -0
- package/lib/ws.d.ts +55 -0
- package/lib/ws.js +234 -0
- package/middlewares/bot-kind-reply.js +46 -0
- package/middlewares/bot-kind-reply.ts +60 -0
- package/package.json +69 -28
- package/plugin.js +18 -0
- package/schema.json +142 -0
- package/src/client.ts +16 -0
- package/src/endpoint.ts +534 -0
- package/src/index.ts +66 -326
- package/src/outbound.ts +255 -0
- package/src/platform-permit.ts +56 -0
- package/src/protocol.ts +273 -0
- package/src/qq-bind-api.ts +145 -0
- package/src/qq-bind-flow.ts +127 -0
- package/src/qq-bind-persist.ts +44 -0
- package/src/qq-bot-kind-prompt.ts +47 -0
- package/src/qq-endpoint-commands.ts +251 -0
- package/src/qq-endpoint-config.ts +44 -0
- package/src/qq-intents.ts +77 -0
- package/src/qq-runtime-state.ts +51 -0
- package/src/side-event-dispatch.ts +140 -0
- package/src/webhook.ts +132 -0
- package/src/ws.ts +331 -0
- package/client/Dashboard.tsx +0 -230
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -17
- package/dist/index.js +0 -30
- package/lib/adapter.d.ts +0 -17
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -49
- package/lib/adapter.js.map +0 -1
- package/lib/bot.d.ts +0 -102
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -337
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types.d.ts +0 -16
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -58
- package/src/bot.ts +0 -347
- package/src/types.ts +0 -24
package/lib/ws.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ WebSocket transport: bot factory and inbound message normalization.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Bot, ReceiverMode, } from 'qq-official-bot';
|
|
6
|
+
/**
|
|
7
|
+
* qq-official-bot 1.2 used a separate connection manager; 1.3 moved WebSocket
|
|
8
|
+
* ownership into the receiver, but Session.stop() still leaves the auth refresh
|
|
9
|
+
* manager alive. Mark the session closed, clean up legacy connections when
|
|
10
|
+
* present, stop the receiver, then destroy auth refresh at the SDK boundary.
|
|
11
|
+
*/
|
|
12
|
+
export async function stopQqOfficialBot(bot) {
|
|
13
|
+
const session = bot.sessionManager;
|
|
14
|
+
session.userClose = true;
|
|
15
|
+
session.connectionManager?.destroy();
|
|
16
|
+
try {
|
|
17
|
+
await bot.stop();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
session.authManager?.destroy();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function bindQqBotInboundEvents(bot, admitRaw) {
|
|
24
|
+
bot.on('message.group', admitRaw);
|
|
25
|
+
bot.on('message.guild', admitRaw);
|
|
26
|
+
bot.on('message.private', admitRaw);
|
|
27
|
+
}
|
|
28
|
+
export function normalizeQqMessage(raw) {
|
|
29
|
+
if (!raw || typeof raw !== 'object')
|
|
30
|
+
return null;
|
|
31
|
+
const msg = raw;
|
|
32
|
+
if (msg.message_id == null)
|
|
33
|
+
return null;
|
|
34
|
+
let channelKind = 'private';
|
|
35
|
+
let channelId = '';
|
|
36
|
+
const isGroup = msg.message_type === 'group'
|
|
37
|
+
|| (msg.message_type == null && (msg.group_id != null || msg.group_openid != null));
|
|
38
|
+
const isGuild = msg.message_type === 'guild'
|
|
39
|
+
|| (msg.message_type == null && !isGroup && msg.guild_id != null && msg.channel_id != null);
|
|
40
|
+
if (isGuild) {
|
|
41
|
+
channelKind = msg.sub_type === 'direct' ? 'direct' : 'channel';
|
|
42
|
+
channelId = channelKind === 'direct'
|
|
43
|
+
? String(msg.guild_id ?? '')
|
|
44
|
+
: String(msg.channel_id ?? '');
|
|
45
|
+
}
|
|
46
|
+
else if (isGroup) {
|
|
47
|
+
channelKind = 'group';
|
|
48
|
+
channelId = String(msg.group_id ?? msg.group_openid ?? '');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
channelKind = 'private';
|
|
52
|
+
channelId = String(msg.user_id ?? msg.author?.user_openid ?? msg.author?.id ?? '');
|
|
53
|
+
}
|
|
54
|
+
if (!channelId)
|
|
55
|
+
return null;
|
|
56
|
+
const authorId = String(msg.sender?.user_id
|
|
57
|
+
?? msg.user_id
|
|
58
|
+
?? msg.author?.member_openid
|
|
59
|
+
?? msg.author?.user_openid
|
|
60
|
+
?? msg.author?.id
|
|
61
|
+
?? '');
|
|
62
|
+
const content = extractTextContent(msg.message) || msg.raw_message || '';
|
|
63
|
+
const segments = extractCanonicalSegments(msg.message);
|
|
64
|
+
const rawRoles = msg.member?.roles ?? msg.sender?.roles;
|
|
65
|
+
const authorRoles = Array.isArray(rawRoles) ? rawRoles.map(String) : undefined;
|
|
66
|
+
// 统一按 mentions 判定 @:is_you 精确标识当前机器人(群载荷实测字段)。
|
|
67
|
+
// 群里 @ 另一个机器人时 mentions 带 bot:true 但无 is_you,不能误判为 @ 当前机器人,
|
|
68
|
+
// 故群场景只认 is_you;bot 回退仅限频道 AT_MESSAGE_CREATE(该事件仅 @ 当前
|
|
69
|
+
// 机器人时下发,其 mentions 无 is_you)。
|
|
70
|
+
const mentioned = Array.isArray(msg.mentions)
|
|
71
|
+
&& msg.mentions.some((m) => m?.is_you === true || (m?.bot === true && channelKind !== 'group'));
|
|
72
|
+
return {
|
|
73
|
+
id: String(msg.message_id),
|
|
74
|
+
content,
|
|
75
|
+
channelKind,
|
|
76
|
+
channelId,
|
|
77
|
+
authorId,
|
|
78
|
+
authorName: msg.sender?.user_name || msg.author?.username || authorId,
|
|
79
|
+
authorRoles,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
guildId: msg.guild_id != null ? String(msg.guild_id) : undefined,
|
|
82
|
+
rawMessage: msg.raw_message,
|
|
83
|
+
...(segments.length > 0 ? { segments } : {}),
|
|
84
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* SDK 解析后的消息段(text/at/face/image/video/audio/file/reply)→ canonical Segment[]。
|
|
89
|
+
* 附件段的 url 已由 SDK 归一(补 https:// 前缀);mime 无法从载荷得知时按类型给默认。
|
|
90
|
+
*/
|
|
91
|
+
function extractCanonicalSegments(message) {
|
|
92
|
+
if (!Array.isArray(message))
|
|
93
|
+
return [];
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const seg of message) {
|
|
96
|
+
if (!seg || typeof seg !== 'object')
|
|
97
|
+
continue;
|
|
98
|
+
const item = seg;
|
|
99
|
+
const type = item.type ?? '';
|
|
100
|
+
const data = item.data ?? {};
|
|
101
|
+
switch (type) {
|
|
102
|
+
case 'text': {
|
|
103
|
+
const text = String(data.text ?? '');
|
|
104
|
+
if (text)
|
|
105
|
+
out.push({ type: 'text', data: { text } });
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case 'at': {
|
|
109
|
+
const target = String(data.user_id ?? data.id ?? '');
|
|
110
|
+
if (!target)
|
|
111
|
+
break;
|
|
112
|
+
const name = typeof data.username === 'string' && data.username
|
|
113
|
+
? data.username
|
|
114
|
+
: undefined;
|
|
115
|
+
out.push({ type: 'mention', data: name ? { target, name } : { target } });
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'face': {
|
|
119
|
+
const id = data.id;
|
|
120
|
+
if (id == null)
|
|
121
|
+
break;
|
|
122
|
+
out.push({ type: 'face', data: { id: id } });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'image':
|
|
126
|
+
case 'video':
|
|
127
|
+
case 'audio':
|
|
128
|
+
case 'file': {
|
|
129
|
+
const url = typeof data.url === 'string' ? data.url : '';
|
|
130
|
+
if (!url)
|
|
131
|
+
break;
|
|
132
|
+
const media = {
|
|
133
|
+
kind: 'url',
|
|
134
|
+
value: url,
|
|
135
|
+
mime_type: defaultMimeForSegment(type),
|
|
136
|
+
...(typeof data.name === 'string' && data.name ? { file_name: data.name } : {}),
|
|
137
|
+
...(typeof data.size === 'number' ? { size: data.size } : {}),
|
|
138
|
+
};
|
|
139
|
+
if (type === 'image')
|
|
140
|
+
out.push({ type: 'image', data: { media } });
|
|
141
|
+
else if (type === 'video')
|
|
142
|
+
out.push({ type: 'video', data: { media } });
|
|
143
|
+
else if (type === 'audio')
|
|
144
|
+
out.push({ type: 'audio', data: { media } });
|
|
145
|
+
else
|
|
146
|
+
out.push({ type: 'file', data: { media, ...(media.file_name ? { name: media.file_name } : {}) } });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'reply': {
|
|
150
|
+
const messageId = String(data.id ?? data.message_id ?? '');
|
|
151
|
+
if (messageId)
|
|
152
|
+
out.push({ type: 'reply', data: { message_id: messageId } });
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function defaultMimeForSegment(type) {
|
|
162
|
+
switch (type) {
|
|
163
|
+
case 'image': return 'image/jpeg';
|
|
164
|
+
case 'video': return 'video/mp4';
|
|
165
|
+
case 'audio': return 'audio/mpeg';
|
|
166
|
+
default: return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function extractTextContent(message) {
|
|
170
|
+
if (typeof message === 'string')
|
|
171
|
+
return message;
|
|
172
|
+
if (!Array.isArray(message))
|
|
173
|
+
return '';
|
|
174
|
+
return message
|
|
175
|
+
.map((seg) => {
|
|
176
|
+
if (!seg || typeof seg !== 'object')
|
|
177
|
+
return '';
|
|
178
|
+
const item = seg;
|
|
179
|
+
if (item.type === 'text')
|
|
180
|
+
return String(item.data?.text ?? item.text ?? '');
|
|
181
|
+
return '';
|
|
182
|
+
})
|
|
183
|
+
.join('');
|
|
184
|
+
}
|
|
185
|
+
export function defaultCreateBot(config) {
|
|
186
|
+
const bot = new Bot({
|
|
187
|
+
appid: config.appid,
|
|
188
|
+
secret: config.secret,
|
|
189
|
+
mode: ReceiverMode.WEBSOCKET,
|
|
190
|
+
sandbox: config.sandbox,
|
|
191
|
+
dataDir: path.join(process.cwd(), 'data', 'qq'),
|
|
192
|
+
// 关闭 qq-official-bot 内置 log4js,入/出站由 getAdapterLogger('qq', …) 统一格式
|
|
193
|
+
logLevel: 'off',
|
|
194
|
+
...(config.intents ? { intents: config.intents } : {}),
|
|
195
|
+
...(config.accessTokenUrl ? { accessTokenUrl: config.accessTokenUrl } : {}),
|
|
196
|
+
...(config.gatewayUrl ? { gatewayUrl: config.gatewayUrl } : {}),
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
api: bot,
|
|
200
|
+
start: () => bot.start().then(() => undefined),
|
|
201
|
+
stop: () => stopQqOfficialBot(bot),
|
|
202
|
+
on: (event, listener) => {
|
|
203
|
+
bot.on(event, listener);
|
|
204
|
+
},
|
|
205
|
+
removeAllListeners: () => {
|
|
206
|
+
bot.removeAllListeners(undefined);
|
|
207
|
+
},
|
|
208
|
+
sendPrivateMessage: (userId, message) => bot.sendPrivateMessage(userId, message),
|
|
209
|
+
sendGroupMessage: (groupId, message) => bot.sendGroupMessage(groupId, message),
|
|
210
|
+
sendGuildMessage: (channelId, message) => bot.sendGuildMessage(channelId, message),
|
|
211
|
+
sendDirectMessage: (guildId, message) => bot.sendDirectMessage(guildId, message),
|
|
212
|
+
recallPrivateMessage: (userId, messageId) => bot.recallPrivateMessage(userId, messageId),
|
|
213
|
+
recallGroupMessage: (groupId, messageId) => bot.recallGroupMessage(groupId, messageId),
|
|
214
|
+
recallGuildMessage: (channelId, messageId) => bot.recallGuildMessage(channelId, messageId),
|
|
215
|
+
recallDirectMessage: (guildId, messageId) => bot.recallDirectMessage(guildId, messageId),
|
|
216
|
+
getGuilds: () => bot.guildService.getList(),
|
|
217
|
+
getChannels: (guildId) => bot.channelService.getList(guildId),
|
|
218
|
+
getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
|
|
219
|
+
getGuildMember: (guildId, userId) => bot.memberService.getGuildMemberInfo(guildId, userId),
|
|
220
|
+
getGuildRoles: (guildId) => bot.guildService.getRoles(guildId),
|
|
221
|
+
createGuildRole: (guildId, name, color) => bot.guildService.createRole(guildId, { name, color: color || 0, hoist: 0 }),
|
|
222
|
+
addMemberRole: async (guildId, channelId, userId, roleId) => {
|
|
223
|
+
await bot.memberService.addMemberRole(guildId, channelId, userId, roleId);
|
|
224
|
+
return true;
|
|
225
|
+
},
|
|
226
|
+
removeMemberRole: async (guildId, channelId, userId, roleId) => {
|
|
227
|
+
await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
|
|
228
|
+
return true;
|
|
229
|
+
},
|
|
230
|
+
approveGroupJoinRequest: (groupId, userId, options) => bot
|
|
231
|
+
.approveGroupJoinRequest?.(groupId, userId, options)
|
|
232
|
+
?? Promise.reject(new Error('approveGroupJoinRequest is not available')),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
3
|
+
import { completeQqPendingBotKind, } from "../lib/qq-endpoint-commands.js";
|
|
4
|
+
import { parseQqBotKindAnswer, qqCommandSessionKey, QQ_BOT_KIND_PROMPT, } from "../lib/qq-bot-kind-prompt.js";
|
|
5
|
+
import { qqRuntimeStateToken } from "../lib/qq-runtime-state.js";
|
|
6
|
+
/**
|
|
7
|
+
* 扫码绑定成功后,在同一会话收取 public/private(或 公域/私域)回复并写入 intents。
|
|
8
|
+
* 非待确认会话、或内容无法识别为 botKind 时放行后续中间件/命令。
|
|
9
|
+
*/
|
|
10
|
+
export default defineMiddleware({
|
|
11
|
+
target: 'inbound',
|
|
12
|
+
phase: 'before-dispatch',
|
|
13
|
+
order: -50,
|
|
14
|
+
async handle(context, next) {
|
|
15
|
+
const state = context.use(qqRuntimeStateToken);
|
|
16
|
+
const pending = state.pendingBotKind;
|
|
17
|
+
if (!pending) {
|
|
18
|
+
await next();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const sessionKey = qqCommandSessionKey(context.input);
|
|
22
|
+
if (!sessionKey || sessionKey !== pending.sessionKey) {
|
|
23
|
+
await next();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const raw = String(context.input.content ?? '').trim();
|
|
27
|
+
// 让 qq.endpoint cancel 等命令继续走命令链
|
|
28
|
+
if (raw.toLowerCase().startsWith('qq.endpoint')) {
|
|
29
|
+
await next();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const botKind = parseQqBotKindAnswer(raw);
|
|
33
|
+
if (!botKind) {
|
|
34
|
+
await context.input.$reply?.(`未识别「${raw}」。${QQ_BOT_KIND_PROMPT}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const text = completeQqPendingBotKind(pending, botKind);
|
|
39
|
+
state.pendingBotKind = null;
|
|
40
|
+
await context.input.$reply?.(text);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
await context.input.$reply?.(`写入配置失败:${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
2
|
+
import type { CommandMessage } from 'zhin.js/command';
|
|
3
|
+
import {
|
|
4
|
+
completeQqPendingBotKind,
|
|
5
|
+
} from '../src/qq-endpoint-commands.js';
|
|
6
|
+
import {
|
|
7
|
+
parseQqBotKindAnswer,
|
|
8
|
+
qqCommandSessionKey,
|
|
9
|
+
QQ_BOT_KIND_PROMPT,
|
|
10
|
+
} from '../src/qq-bot-kind-prompt.js';
|
|
11
|
+
import { qqRuntimeStateToken } from '../src/qq-runtime-state.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 扫码绑定成功后,在同一会话收取 public/private(或 公域/私域)回复并写入 intents。
|
|
15
|
+
* 非待确认会话、或内容无法识别为 botKind 时放行后续中间件/命令。
|
|
16
|
+
*/
|
|
17
|
+
export default defineMiddleware<CommandMessage>({
|
|
18
|
+
target: 'inbound',
|
|
19
|
+
phase: 'before-dispatch',
|
|
20
|
+
order: -50,
|
|
21
|
+
async handle(context, next) {
|
|
22
|
+
const state = context.use(qqRuntimeStateToken);
|
|
23
|
+
const pending = state.pendingBotKind;
|
|
24
|
+
if (!pending) {
|
|
25
|
+
await next();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sessionKey = qqCommandSessionKey(context.input);
|
|
30
|
+
if (!sessionKey || sessionKey !== pending.sessionKey) {
|
|
31
|
+
await next();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const raw = String(context.input.content ?? '').trim();
|
|
36
|
+
// 让 qq.endpoint cancel 等命令继续走命令链
|
|
37
|
+
if (raw.toLowerCase().startsWith('qq.endpoint')) {
|
|
38
|
+
await next();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const botKind = parseQqBotKindAnswer(raw);
|
|
43
|
+
if (!botKind) {
|
|
44
|
+
await context.input.$reply?.(
|
|
45
|
+
`未识别「${raw}」。${QQ_BOT_KIND_PROMPT}`,
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const text = completeQqPendingBotKind(pending, botKind);
|
|
52
|
+
state.pendingBotKind = null;
|
|
53
|
+
await context.input.$reply?.(text);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
await context.input.$reply?.(
|
|
56
|
+
`写入配置失败:${error instanceof Error ? error.message : String(error)}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-qq",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Zhin.js adapter for
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -34,53 +34,94 @@
|
|
|
34
34
|
"directory": "plugins/adapters/qq"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"qq-official-bot": "^1.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"@zhin.js/
|
|
45
|
-
"@zhin.js/console": "2.0.24",
|
|
46
|
-
"zhin.js": "1.0.81"
|
|
37
|
+
"qq-official-bot": "^1.3.0",
|
|
38
|
+
"@zhin.js/adapter": "1.1.12",
|
|
39
|
+
"@zhin.js/core": "1.1.35",
|
|
40
|
+
"@zhin.js/feature-kit": "1.1.0",
|
|
41
|
+
"@zhin.js/host-http": "1.1.0",
|
|
42
|
+
"@zhin.js/im-contract": "1.1.0",
|
|
43
|
+
"@zhin.js/logger": "1.1.0",
|
|
44
|
+
"@zhin.js/permission": "1.1.0"
|
|
47
45
|
},
|
|
48
46
|
"peerDependencies": {
|
|
49
|
-
"
|
|
50
|
-
"@zhin.js/
|
|
51
|
-
"@zhin.js/
|
|
52
|
-
"@zhin.js/
|
|
53
|
-
"zhin.js": "1.
|
|
47
|
+
"zod": "^4.0.0",
|
|
48
|
+
"@zhin.js/adapter": "^1.1.12",
|
|
49
|
+
"@zhin.js/agent": "^1.1.23",
|
|
50
|
+
"@zhin.js/command": "^1.1.0",
|
|
51
|
+
"@zhin.js/core": "^1.1.35",
|
|
52
|
+
"@zhin.js/middleware": "^1.1.0",
|
|
53
|
+
"zhin.js": "^1.1.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
|
-
"@zhin.js/
|
|
56
|
+
"@zhin.js/agent": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@zhin.js/command": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@zhin.js/middleware": {
|
|
57
63
|
"optional": true
|
|
58
64
|
},
|
|
59
|
-
"
|
|
65
|
+
"zhin.js": {
|
|
60
66
|
"optional": true
|
|
61
67
|
},
|
|
62
|
-
"
|
|
68
|
+
"zod": {
|
|
63
69
|
"optional": true
|
|
64
70
|
}
|
|
65
71
|
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "^26.1.2",
|
|
74
|
+
"typescript": "^6.0.3",
|
|
75
|
+
"vitest": "^4.1.10",
|
|
76
|
+
"zod": "^4.4.3",
|
|
77
|
+
"@zhin.js/agent": "1.1.23",
|
|
78
|
+
"@zhin.js/host-http": "1.1.0",
|
|
79
|
+
"zhin.js": "1.1.0"
|
|
80
|
+
},
|
|
66
81
|
"files": [
|
|
82
|
+
"adapters",
|
|
83
|
+
"commands",
|
|
84
|
+
"middlewares",
|
|
85
|
+
"plugin.js",
|
|
86
|
+
"schema.json",
|
|
67
87
|
"src",
|
|
68
88
|
"lib",
|
|
69
|
-
"
|
|
70
|
-
"dist",
|
|
71
|
-
"skills",
|
|
72
|
-
"plugin.yml",
|
|
89
|
+
"agent",
|
|
73
90
|
"README.md",
|
|
74
|
-
"node",
|
|
75
91
|
"CHANGELOG.md"
|
|
76
92
|
],
|
|
77
93
|
"publishConfig": {
|
|
78
94
|
"access": "public",
|
|
79
95
|
"registry": "https://registry.npmjs.org"
|
|
80
96
|
},
|
|
97
|
+
"engines": {
|
|
98
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
99
|
+
},
|
|
100
|
+
"zhin": {
|
|
101
|
+
"protocol": 1,
|
|
102
|
+
"type": "plugin",
|
|
103
|
+
"entry": "./plugin.js",
|
|
104
|
+
"engine": "^1.0.0",
|
|
105
|
+
"runtime": "trusted",
|
|
106
|
+
"features": [
|
|
107
|
+
{
|
|
108
|
+
"package": "@zhin.js/adapter",
|
|
109
|
+
"api": "^1.0.0"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"package": "@zhin.js/command",
|
|
113
|
+
"api": "^1.0.0"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"package": "@zhin.js/middleware",
|
|
117
|
+
"api": "^1.0.0"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"plugins": []
|
|
121
|
+
},
|
|
81
122
|
"scripts": {
|
|
82
|
-
"build": "
|
|
83
|
-
"clean": "rimraf lib
|
|
84
|
-
"
|
|
123
|
+
"build": "tsc",
|
|
124
|
+
"clean": "rimraf lib",
|
|
125
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/qq/tests"
|
|
85
126
|
}
|
|
86
127
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { definePlugin } from 'zhin.js';
|
|
3
|
+
import { permissionHostToken } from '@zhin.js/permission';
|
|
4
|
+
import { checkQqPlatformPermit } from "./lib/platform-permit.js";
|
|
5
|
+
import { createQqRuntimeState, qqRuntimeStateToken } from "./lib/qq-runtime-state.js";
|
|
6
|
+
export default definePlugin({
|
|
7
|
+
name: 'qq',
|
|
8
|
+
metadata: {
|
|
9
|
+
displayName: 'QQ Official WebSocket Adapter',
|
|
10
|
+
},
|
|
11
|
+
setup(context) {
|
|
12
|
+
context.resources.provide(qqRuntimeStateToken, createQqRuntimeState());
|
|
13
|
+
if (context.resources.has(permissionHostToken)) {
|
|
14
|
+
const host = context.resources.use(permissionHostToken);
|
|
15
|
+
return host.registerPlatform('qq', checkQqPlatformPermit);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"mode": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"enum": [
|
|
9
|
+
"websocket",
|
|
10
|
+
"webhook",
|
|
11
|
+
"middleware"
|
|
12
|
+
],
|
|
13
|
+
"default": "websocket",
|
|
14
|
+
"description": "WebSocket gateway (default). webhook/middleware use httpHostToken POST."
|
|
15
|
+
},
|
|
16
|
+
"sandbox": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"default": false
|
|
19
|
+
},
|
|
20
|
+
"botKind": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": [
|
|
23
|
+
"public",
|
|
24
|
+
"private"
|
|
25
|
+
],
|
|
26
|
+
"default": "public",
|
|
27
|
+
"description": "公域/私域:均含 GROUP_AND_C2C_EVENT;频道消息分别为 PUBLIC_GUILD_MESSAGES / GUILD_MESSAGES。未显式配置 intents 时按此展开"
|
|
28
|
+
},
|
|
29
|
+
"master": {
|
|
30
|
+
"type": [
|
|
31
|
+
"string",
|
|
32
|
+
"number"
|
|
33
|
+
],
|
|
34
|
+
"description": "框架 master(endpoint 管理命令、AI/工具权限);openid。endpoints[i].master 可逐项覆盖"
|
|
35
|
+
},
|
|
36
|
+
"trusted": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": [
|
|
40
|
+
"string",
|
|
41
|
+
"number"
|
|
42
|
+
],
|
|
43
|
+
"description": "Trusted openid"
|
|
44
|
+
},
|
|
45
|
+
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
|
|
46
|
+
},
|
|
47
|
+
"intents": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
},
|
|
52
|
+
"description": "WebSocket Identify intents;省略时按 botKind 自动生成(public→含 PUBLIC_GUILD_MESSAGES)"
|
|
53
|
+
},
|
|
54
|
+
"accessTokenUrl": {
|
|
55
|
+
"type": "string"
|
|
56
|
+
},
|
|
57
|
+
"gatewayUrl": {
|
|
58
|
+
"type": "string"
|
|
59
|
+
},
|
|
60
|
+
"webhookPath": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"default": "/qq/webhook",
|
|
63
|
+
"description": "POST path on httpHostToken for webhook/middleware modes."
|
|
64
|
+
},
|
|
65
|
+
"port": {
|
|
66
|
+
"type": "number",
|
|
67
|
+
"description": "Legacy standalone webhook port (unused with httpHostToken)."
|
|
68
|
+
},
|
|
69
|
+
"path": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Legacy standalone webhook path (unused with httpHostToken)."
|
|
72
|
+
},
|
|
73
|
+
"endpoints": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": true,
|
|
79
|
+
"properties": {
|
|
80
|
+
"appid": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "QQ official bot app ID"
|
|
83
|
+
},
|
|
84
|
+
"secret": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"description": "QQ official bot secret"
|
|
87
|
+
},
|
|
88
|
+
"master": {
|
|
89
|
+
"type": [
|
|
90
|
+
"string",
|
|
91
|
+
"number"
|
|
92
|
+
],
|
|
93
|
+
"description": "本 endpoint 的框架 master(openid);覆盖顶层 master"
|
|
94
|
+
},
|
|
95
|
+
"trusted": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"items": {
|
|
98
|
+
"type": [
|
|
99
|
+
"string",
|
|
100
|
+
"number"
|
|
101
|
+
],
|
|
102
|
+
"description": "Trusted openid"
|
|
103
|
+
},
|
|
104
|
+
"description": "本 endpoint 的 trusted 列表"
|
|
105
|
+
},
|
|
106
|
+
"botKind": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"enum": [
|
|
109
|
+
"public",
|
|
110
|
+
"private"
|
|
111
|
+
],
|
|
112
|
+
"description": "覆盖顶层 botKind(公域/私域),用于按 endpoint 生成 intents"
|
|
113
|
+
},
|
|
114
|
+
"intents": {
|
|
115
|
+
"type": "array",
|
|
116
|
+
"items": {
|
|
117
|
+
"type": "string"
|
|
118
|
+
},
|
|
119
|
+
"description": "覆盖顶层 intents"
|
|
120
|
+
},
|
|
121
|
+
"id": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "QQ bot name"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"required": [
|
|
127
|
+
"id",
|
|
128
|
+
"appid",
|
|
129
|
+
"secret"
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"commandPrefix": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"default": "",
|
|
136
|
+
"description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"required": [
|
|
140
|
+
"endpoints"
|
|
141
|
+
]
|
|
142
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
2
|
+
import type { QqInboundMessage } from './protocol.js';
|
|
3
|
+
import type { QqBotTransport } from './ws.js';
|
|
4
|
+
|
|
5
|
+
export type QqClientEventMap = Record<string, QqInboundMessage | unknown>;
|
|
6
|
+
|
|
7
|
+
declare module '@zhin.js/feature-kit' {
|
|
8
|
+
interface AdapterClientRegistry {
|
|
9
|
+
readonly qq: {
|
|
10
|
+
readonly client: QqBotTransport;
|
|
11
|
+
readonly events: QqClientEventMap;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const qqClient = defineEndpointClient<QqBotTransport, QqClientEventMap>('qq');
|