@zhin.js/adapter-qq 7.0.2 → 7.0.5
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 +96 -0
- package/README.md +36 -9
- package/adapters/qq.js +9 -7
- package/adapters/qq.ts +9 -7
- package/commands/endpoint/cancel.js +1 -1
- package/commands/endpoint/cancel.ts +1 -1
- package/lib/endpoint.d.ts +3 -9
- package/lib/endpoint.js +79 -75
- package/lib/index.d.ts +3 -2
- package/lib/index.js +3 -2
- package/lib/platform-permit.d.ts +2 -3
- package/lib/platform-permit.js +3 -11
- package/lib/protocol.d.ts +14 -11
- package/lib/protocol.js +26 -19
- package/lib/qq-agent-deps.d.ts +2 -2
- package/lib/qq-agent-deps.js +7 -7
- package/lib/qq-bind-persist.d.ts +2 -2
- package/lib/qq-bind-persist.js +5 -5
- 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 +13 -6
- package/lib/qq-endpoint-commands.js +89 -31
- package/lib/qq-endpoint-config.d.ts +4 -4
- package/lib/qq-endpoint-config.js +4 -4
- package/lib/qq-intents.d.ts +27 -0
- package/lib/qq-intents.js +50 -0
- package/lib/qq-runtime-state.d.ts +17 -4
- package/lib/qq-runtime-state.js +3 -2
- package/lib/webhook.js +2 -0
- package/lib/ws.js +2 -2
- package/middlewares/bot-kind-reply.js +46 -0
- package/middlewares/bot-kind-reply.ts +60 -0
- package/package.json +23 -15
- package/plugin.js +6 -4
- package/schema.json +66 -9
- package/src/endpoint.ts +95 -79
- package/src/index.ts +10 -4
- package/src/platform-permit.ts +4 -9
- package/src/protocol.ts +34 -28
- package/src/qq-agent-deps.ts +8 -8
- package/src/qq-bind-persist.ts +5 -5
- package/src/qq-bot-kind-prompt.ts +47 -0
- package/src/qq-endpoint-commands.ts +125 -34
- package/src/qq-endpoint-config.ts +5 -5
- package/src/qq-intents.ts +76 -0
- package/src/qq-runtime-state.ts +21 -6
- package/src/webhook.ts +2 -0
- package/src/ws.ts +2 -2
- /package/commands/endpoint/add/{[name:string=].js → [[id]].js} +0 -0
- /package/commands/endpoint/add/{[name:string=].ts → [[id]].ts} +0 -0
- /package/commands/endpoint/remove/{[name:string].js → [id].js} +0 -0
- /package/commands/endpoint/remove/{[name:string].ts → [id].ts} +0 -0
package/src/endpoint.ts
CHANGED
|
@@ -5,18 +5,19 @@ import type {
|
|
|
5
5
|
EndpointChannel,
|
|
6
6
|
EndpointInstance,
|
|
7
7
|
EndpointManagement,
|
|
8
|
+
EndpointSendRequest,
|
|
8
9
|
} from '@zhin.js/adapter';
|
|
9
10
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
10
11
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
11
|
-
import { formatCompact,
|
|
12
|
+
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
12
13
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
13
14
|
import { formatOutbound } from './outbound.js';
|
|
14
15
|
import { registerQqAgentEndpoint } from './qq-agent-deps.js';
|
|
15
16
|
import {
|
|
16
17
|
formatInboundContent,
|
|
17
|
-
formatInboundTarget,
|
|
18
18
|
parseCompoundMessageId,
|
|
19
|
-
|
|
19
|
+
qqInboundConversation,
|
|
20
|
+
qqOutboundKind,
|
|
20
21
|
resolveOutboundMessageId,
|
|
21
22
|
senderDisplayName,
|
|
22
23
|
type QqInboundMessage,
|
|
@@ -37,8 +38,6 @@ import {
|
|
|
37
38
|
type QqBotTransport,
|
|
38
39
|
} from './ws.js';
|
|
39
40
|
|
|
40
|
-
const logger = getLogger('qq');
|
|
41
|
-
|
|
42
41
|
export type { CreateQqBot, QqBotTransport } from './ws.js';
|
|
43
42
|
export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
44
43
|
|
|
@@ -50,6 +49,8 @@ export interface QqEndpointOptions {
|
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
export class QqWebsocketEndpoint implements EndpointInstance {
|
|
52
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
53
|
+
|
|
53
54
|
readonly #options: QqEndpointOptions;
|
|
54
55
|
readonly #createBot: CreateQqBot;
|
|
55
56
|
#bot: QqBotTransport | null = null;
|
|
@@ -59,32 +60,30 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
59
60
|
readonly management: EndpointManagement = createQqEndpointManagement(this);
|
|
60
61
|
|
|
61
62
|
constructor(options: QqEndpointOptions) {
|
|
63
|
+
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
62
64
|
this.#options = options;
|
|
63
65
|
this.#createBot = options.createBot ?? defaultCreateBot;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
|
|
67
69
|
get name(): string {
|
|
68
|
-
return this.#options.config.
|
|
70
|
+
return this.#options.config.id;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
async start(): Promise<void> {
|
|
72
74
|
if (this.#started) return;
|
|
73
75
|
this.#started = true;
|
|
74
76
|
try {
|
|
75
|
-
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.
|
|
77
|
+
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.id, this);
|
|
76
78
|
this.#bot = this.#createBot(this.#options.config);
|
|
77
79
|
this.#bindBot(this.#bot);
|
|
78
80
|
await this.#bot.start();
|
|
79
|
-
logger.info(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
mode: 'websocket',
|
|
83
|
-
appid: this.#options.config.appid,
|
|
84
|
-
}));
|
|
81
|
+
this.#logger.info(
|
|
82
|
+
`connected (websocket) | appid: ${this.#options.config.appid}`,
|
|
83
|
+
);
|
|
85
84
|
} catch (error) {
|
|
86
85
|
await this.stop();
|
|
87
|
-
logger.error('Failed to connect QQ websocket:', error);
|
|
86
|
+
this.#logger.error('Failed to connect QQ websocket:', error);
|
|
88
87
|
const raw = error instanceof Error ? error.message : String(error);
|
|
89
88
|
throw new Error(
|
|
90
89
|
`QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`,
|
|
@@ -115,38 +114,35 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
115
114
|
this.#bot = null;
|
|
116
115
|
}
|
|
117
116
|
this.#started = false;
|
|
118
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
117
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
async send({
|
|
120
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
122
121
|
const body = formatOutbound(payload);
|
|
123
|
-
const
|
|
122
|
+
const kind = qqOutboundKind(conversation);
|
|
124
123
|
const bot = this.#requireBot();
|
|
125
124
|
let result: unknown;
|
|
126
|
-
switch (
|
|
125
|
+
switch (kind) {
|
|
127
126
|
case 'private':
|
|
128
|
-
result = await bot.sendPrivateMessage(
|
|
127
|
+
result = await bot.sendPrivateMessage(conversation.id, body);
|
|
129
128
|
break;
|
|
130
129
|
case 'group':
|
|
131
|
-
result = await bot.sendGroupMessage(
|
|
130
|
+
result = await bot.sendGroupMessage(conversation.id, body);
|
|
132
131
|
break;
|
|
133
132
|
case 'channel':
|
|
134
|
-
result = await bot.sendGuildMessage(
|
|
133
|
+
result = await bot.sendGuildMessage(conversation.id, body);
|
|
135
134
|
break;
|
|
136
135
|
case 'direct':
|
|
137
136
|
if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
|
|
138
|
-
result = await bot.sendDirectMessage(
|
|
137
|
+
result = await bot.sendDirectMessage(conversation.id, body);
|
|
139
138
|
break;
|
|
140
|
-
default:
|
|
141
|
-
throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
|
|
142
139
|
}
|
|
143
|
-
const messageId = `${
|
|
144
|
-
logger.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}));
|
|
140
|
+
const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
|
|
141
|
+
this.#logger.info(
|
|
142
|
+
`send ${kind}:${conversation.id}`
|
|
143
|
+
+ ` | id: ${messageId}`
|
|
144
|
+
+ ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`,
|
|
145
|
+
);
|
|
150
146
|
return messageId;
|
|
151
147
|
}
|
|
152
148
|
|
|
@@ -167,9 +163,9 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
167
163
|
await bot.recallDirectMessage?.(channelId, qqMsgId);
|
|
168
164
|
break;
|
|
169
165
|
}
|
|
170
|
-
logger.debug(formatCompact({
|
|
166
|
+
this.#logger.debug(formatCompact({
|
|
171
167
|
op: 'qq_recall',
|
|
172
|
-
endpoint: this.#options.config.
|
|
168
|
+
endpoint: this.#options.config.id,
|
|
173
169
|
kind,
|
|
174
170
|
messageId,
|
|
175
171
|
}));
|
|
@@ -178,27 +174,36 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
178
174
|
/** Test / internal: admit a message when open. */
|
|
179
175
|
admit(msg: QqInboundMessage): void {
|
|
180
176
|
if (!this.#open) return;
|
|
181
|
-
const
|
|
177
|
+
const conversation = qqInboundConversation(String(this.#options.id), msg);
|
|
178
|
+
const content = formatInboundContent(msg);
|
|
179
|
+
this.#logger.info(
|
|
180
|
+
`recv ${conversation.kind}:${conversation.id}`
|
|
181
|
+
+ (msg.authorId ? ` from ${msg.authorId}` : '')
|
|
182
|
+
+ (msg.mentioned ? ' (mentioned)' : '')
|
|
183
|
+
+ ` | ${truncatePreview(content, 80)}`,
|
|
184
|
+
);
|
|
182
185
|
void this.#options.gateway.receive({
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
content
|
|
186
|
+
conversation,
|
|
187
|
+
message: { conversation, id: msg.id },
|
|
188
|
+
content,
|
|
186
189
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
187
|
-
sender:
|
|
188
|
-
|
|
190
|
+
sender: {
|
|
191
|
+
id: msg.authorId,
|
|
192
|
+
name: senderDisplayName(msg) || undefined,
|
|
193
|
+
...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
|
|
194
|
+
},
|
|
195
|
+
endpointId: this.#options.config.id,
|
|
196
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
189
197
|
metadata: Object.freeze({
|
|
190
|
-
endpoint: this.#options.config.name,
|
|
191
198
|
channelKind: msg.channelKind,
|
|
192
199
|
userId: msg.authorId,
|
|
193
200
|
guildId: msg.guildId,
|
|
194
201
|
roles: msg.authorRoles,
|
|
195
|
-
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
196
|
-
...(msg.mentioned ? { mentioned: true } : {}),
|
|
197
202
|
}),
|
|
198
203
|
}).catch((err) => {
|
|
199
|
-
logger.warn(formatCompact({
|
|
204
|
+
this.#logger.warn(formatCompact({
|
|
200
205
|
op: 'qq_gateway_receive_failed',
|
|
201
|
-
target
|
|
206
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
202
207
|
error: err instanceof Error ? err.message : String(err),
|
|
203
208
|
}));
|
|
204
209
|
});
|
|
@@ -249,8 +254,6 @@ export class QqWebsocketEndpoint implements EndpointInstance {
|
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
type ParsedSendTarget = ReturnType<typeof parseSendTarget>;
|
|
253
|
-
|
|
254
257
|
export interface QqHttpEndpointOptions {
|
|
255
258
|
readonly id: CapabilityId;
|
|
256
259
|
readonly gateway: MessageGateway;
|
|
@@ -261,6 +264,8 @@ export interface QqHttpEndpointOptions {
|
|
|
261
264
|
|
|
262
265
|
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
263
266
|
export class QqHttpEndpoint implements EndpointInstance {
|
|
267
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
268
|
+
|
|
264
269
|
readonly #options: QqHttpEndpointOptions;
|
|
265
270
|
readonly #createBot: CreateQqHttpBot;
|
|
266
271
|
#bot: QqHttpBotTransport | null = null;
|
|
@@ -271,13 +276,14 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
271
276
|
readonly management: EndpointManagement = createQqEndpointManagement(this);
|
|
272
277
|
|
|
273
278
|
constructor(options: QqHttpEndpointOptions) {
|
|
279
|
+
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
274
280
|
this.#options = options;
|
|
275
281
|
this.#createBot = options.createBot ?? defaultCreateHttpBot;
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
/** Live endpoint 名(Console/AdapterIndex 展示用)。 */
|
|
279
285
|
get name(): string {
|
|
280
|
-
return this.#options.config.
|
|
286
|
+
return this.#options.config.id;
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
async start(): Promise<void> {
|
|
@@ -285,22 +291,19 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
285
291
|
this.#started = true;
|
|
286
292
|
try {
|
|
287
293
|
this.#unregisterAgent = registerQqAgentEndpoint(
|
|
288
|
-
this.#options.config.
|
|
294
|
+
this.#options.config.id,
|
|
289
295
|
this as unknown as QqWebsocketEndpoint,
|
|
290
296
|
);
|
|
291
297
|
this.#setupRoutes();
|
|
292
298
|
this.#bot = this.#createBot(this.#options.config);
|
|
293
299
|
this.#bindBot(this.#bot);
|
|
294
300
|
await this.#bot.start();
|
|
295
|
-
logger.info(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
mode: this.#options.config.mode,
|
|
299
|
-
path: this.#options.config.webhookPath,
|
|
300
|
-
}));
|
|
301
|
+
this.#logger.info(
|
|
302
|
+
`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`,
|
|
303
|
+
);
|
|
301
304
|
} catch (error) {
|
|
302
305
|
await this.stop();
|
|
303
|
-
logger.error('Failed to connect QQ HTTP receiver:', error);
|
|
306
|
+
this.#logger.error('Failed to connect QQ HTTP receiver:', error);
|
|
304
307
|
throw error;
|
|
305
308
|
}
|
|
306
309
|
}
|
|
@@ -328,32 +331,36 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
328
331
|
this.#bot = null;
|
|
329
332
|
}
|
|
330
333
|
this.#started = false;
|
|
331
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
334
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
332
335
|
}
|
|
333
336
|
|
|
334
|
-
async send({
|
|
337
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
335
338
|
const body = formatOutbound(payload);
|
|
336
|
-
const
|
|
339
|
+
const kind = qqOutboundKind(conversation);
|
|
337
340
|
const bot = this.#requireBot();
|
|
338
341
|
let result: unknown;
|
|
339
|
-
switch (
|
|
342
|
+
switch (kind) {
|
|
340
343
|
case 'private':
|
|
341
|
-
result = await bot.sendPrivateMessage(
|
|
344
|
+
result = await bot.sendPrivateMessage(conversation.id, body);
|
|
342
345
|
break;
|
|
343
346
|
case 'group':
|
|
344
|
-
result = await bot.sendGroupMessage(
|
|
347
|
+
result = await bot.sendGroupMessage(conversation.id, body);
|
|
345
348
|
break;
|
|
346
349
|
case 'channel':
|
|
347
|
-
result = await bot.sendGuildMessage(
|
|
350
|
+
result = await bot.sendGuildMessage(conversation.id, body);
|
|
348
351
|
break;
|
|
349
352
|
case 'direct':
|
|
350
353
|
if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
|
|
351
|
-
result = await bot.sendDirectMessage(
|
|
354
|
+
result = await bot.sendDirectMessage(conversation.id, body);
|
|
352
355
|
break;
|
|
353
|
-
default:
|
|
354
|
-
throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
|
|
355
356
|
}
|
|
356
|
-
|
|
357
|
+
const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
|
|
358
|
+
this.#logger.info(
|
|
359
|
+
`send ${kind}:${conversation.id}`
|
|
360
|
+
+ ` | id: ${messageId}`
|
|
361
|
+
+ ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`,
|
|
362
|
+
);
|
|
363
|
+
return messageId;
|
|
357
364
|
}
|
|
358
365
|
|
|
359
366
|
async recallMessage(messageId: string): Promise<void> {
|
|
@@ -373,9 +380,9 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
373
380
|
await bot.recallDirectMessage?.(channelId, qqMsgId);
|
|
374
381
|
break;
|
|
375
382
|
}
|
|
376
|
-
logger.debug(formatCompact({
|
|
383
|
+
this.#logger.debug(formatCompact({
|
|
377
384
|
op: 'qq_recall',
|
|
378
|
-
endpoint: this.#options.config.
|
|
385
|
+
endpoint: this.#options.config.id,
|
|
379
386
|
kind,
|
|
380
387
|
messageId,
|
|
381
388
|
}));
|
|
@@ -383,27 +390,36 @@ export class QqHttpEndpoint implements EndpointInstance {
|
|
|
383
390
|
|
|
384
391
|
admit(msg: QqInboundMessage): void {
|
|
385
392
|
if (!this.#open) return;
|
|
386
|
-
const
|
|
393
|
+
const conversation = qqInboundConversation(String(this.#options.id), msg);
|
|
394
|
+
const content = formatInboundContent(msg);
|
|
395
|
+
this.#logger.info(
|
|
396
|
+
`recv ${conversation.kind}:${conversation.id}`
|
|
397
|
+
+ (msg.authorId ? ` from ${msg.authorId}` : '')
|
|
398
|
+
+ (msg.mentioned ? ' (mentioned)' : '')
|
|
399
|
+
+ ` | ${truncatePreview(content, 80)}`,
|
|
400
|
+
);
|
|
387
401
|
void this.#options.gateway.receive({
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
content
|
|
402
|
+
conversation,
|
|
403
|
+
message: { conversation, id: msg.id },
|
|
404
|
+
content,
|
|
391
405
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
392
|
-
sender:
|
|
393
|
-
|
|
406
|
+
sender: {
|
|
407
|
+
id: msg.authorId,
|
|
408
|
+
name: senderDisplayName(msg) || undefined,
|
|
409
|
+
...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
|
|
410
|
+
},
|
|
411
|
+
endpointId: this.#options.config.id,
|
|
412
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
394
413
|
metadata: Object.freeze({
|
|
395
|
-
endpoint: this.#options.config.name,
|
|
396
414
|
channelKind: msg.channelKind,
|
|
397
415
|
userId: msg.authorId,
|
|
398
416
|
guildId: msg.guildId,
|
|
399
417
|
roles: msg.authorRoles,
|
|
400
|
-
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
401
|
-
...(msg.mentioned ? { mentioned: true } : {}),
|
|
402
418
|
}),
|
|
403
419
|
}).catch((err) => {
|
|
404
|
-
logger.warn(formatCompact({
|
|
420
|
+
this.#logger.warn(formatCompact({
|
|
405
421
|
op: 'qq_gateway_receive_failed',
|
|
406
|
-
target
|
|
422
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
407
423
|
error: err instanceof Error ? err.message : String(err),
|
|
408
424
|
}));
|
|
409
425
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
export {
|
|
2
2
|
formatInboundContent,
|
|
3
|
-
formatInboundTarget,
|
|
4
3
|
formatOutboundText,
|
|
5
|
-
parseSendTarget,
|
|
6
4
|
resolveOutboundMessageId,
|
|
7
5
|
resolveQqConfig,
|
|
8
6
|
senderDisplayName,
|
|
9
|
-
type ParsedSendTarget,
|
|
10
7
|
type QqAdapterConfig,
|
|
11
8
|
type QqChannelKind,
|
|
12
9
|
type QqInboundMessage,
|
|
@@ -16,6 +13,16 @@ export {
|
|
|
16
13
|
type ResolvedQqWebsocketConfig,
|
|
17
14
|
} from './protocol.js';
|
|
18
15
|
|
|
16
|
+
export {
|
|
17
|
+
DEFAULT_QQ_BOT_KIND,
|
|
18
|
+
QQ_INTENTS_BY_KIND,
|
|
19
|
+
defaultQqEndpointIntentFields,
|
|
20
|
+
parseQqBotKind,
|
|
21
|
+
resolveQqIntents,
|
|
22
|
+
type QqBotKind,
|
|
23
|
+
type QqIntent,
|
|
24
|
+
} from './qq-intents.js';
|
|
25
|
+
|
|
19
26
|
export {
|
|
20
27
|
formatOutbound,
|
|
21
28
|
resolveMediaFile,
|
|
@@ -61,5 +68,4 @@ export {
|
|
|
61
68
|
normalizeQqGuildSenderForPermit,
|
|
62
69
|
platformPermit,
|
|
63
70
|
qqGuildPermitResolver,
|
|
64
|
-
registerQqPlatformPermitChecker,
|
|
65
71
|
} from './platform-permit.js';
|
package/src/platform-permit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import type { PermissionSubject } from '@zhin.js/permission';
|
|
5
5
|
|
|
6
6
|
const ADAPTER = 'qq';
|
|
7
7
|
|
|
@@ -36,10 +36,9 @@ export function normalizeQqGuildSenderForPermit(input: {
|
|
|
36
36
|
return { role: 'member', permissions };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function checkQqPlatformPermit(perm: string,
|
|
40
|
-
const
|
|
41
|
-
const permissions = sender
|
|
42
|
-
const role = sender.role;
|
|
39
|
+
export function checkQqPlatformPermit(perm: string, subject: PermissionSubject): boolean {
|
|
40
|
+
const role = subject.sender?.role?.[0];
|
|
41
|
+
const permissions = subject.sender?.permissions ?? [];
|
|
43
42
|
const has = (t: string) => permissions.includes(t);
|
|
44
43
|
|
|
45
44
|
switch (perm) {
|
|
@@ -55,7 +54,3 @@ export function checkQqPlatformPermit(perm: string, message: Message<any>): bool
|
|
|
55
54
|
return false;
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
|
-
|
|
59
|
-
export function registerQqPlatformPermitChecker(): () => void {
|
|
60
|
-
return registerPlatformPermitChecker(ADAPTER, checkQqPlatformPermit);
|
|
61
|
-
}
|
package/src/protocol.ts
CHANGED
|
@@ -4,19 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { pickCredential } from '@zhin.js/adapter';
|
|
7
|
+
import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
|
|
7
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import { resolveQqIntents } from './qq-intents.js';
|
|
8
10
|
|
|
9
11
|
const logger = getLogger('qq');
|
|
10
12
|
|
|
11
13
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
12
14
|
export interface QqAdapterConfig {
|
|
13
|
-
readonly
|
|
15
|
+
readonly id?: string;
|
|
14
16
|
readonly appid?: string;
|
|
15
17
|
readonly secret?: string;
|
|
16
18
|
/** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
|
|
17
19
|
readonly mode?: 'websocket' | 'webhook' | 'middleware';
|
|
18
20
|
readonly sandbox?: boolean;
|
|
21
|
+
/** Explicit intents; when omitted, derived from `botKind` (default public). */
|
|
19
22
|
readonly intents?: readonly string[];
|
|
23
|
+
/** public | private — drives default intents when `intents` omitted. */
|
|
24
|
+
readonly botKind?: string;
|
|
20
25
|
readonly accessTokenUrl?: string;
|
|
21
26
|
readonly gatewayUrl?: string;
|
|
22
27
|
readonly webhookPath?: string;
|
|
@@ -25,12 +30,13 @@ export interface QqAdapterConfig {
|
|
|
25
30
|
/** Transitional: legacy root `endpoints[]` with `context: qq`. */
|
|
26
31
|
readonly endpoints?: ReadonlyArray<{
|
|
27
32
|
readonly context?: string;
|
|
28
|
-
readonly
|
|
33
|
+
readonly id?: string;
|
|
29
34
|
readonly appid?: string;
|
|
30
35
|
readonly secret?: string;
|
|
31
36
|
readonly mode?: 'websocket' | 'webhook' | 'middleware';
|
|
32
37
|
readonly sandbox?: boolean;
|
|
33
38
|
readonly intents?: readonly string[];
|
|
39
|
+
readonly botKind?: string;
|
|
34
40
|
readonly accessTokenUrl?: string;
|
|
35
41
|
readonly gatewayUrl?: string;
|
|
36
42
|
readonly webhookPath?: string;
|
|
@@ -42,7 +48,7 @@ export interface QqAdapterConfig {
|
|
|
42
48
|
export interface ResolvedQqWebsocketConfig {
|
|
43
49
|
readonly context: 'qq';
|
|
44
50
|
readonly mode: 'websocket';
|
|
45
|
-
readonly
|
|
51
|
+
readonly id: string;
|
|
46
52
|
readonly appid: string;
|
|
47
53
|
readonly secret: string;
|
|
48
54
|
readonly sandbox: boolean;
|
|
@@ -54,7 +60,7 @@ export interface ResolvedQqWebsocketConfig {
|
|
|
54
60
|
export interface ResolvedQqHttpConfig {
|
|
55
61
|
readonly context: 'qq';
|
|
56
62
|
readonly mode: 'webhook' | 'middleware';
|
|
57
|
-
readonly
|
|
63
|
+
readonly id: string;
|
|
58
64
|
readonly appid: string;
|
|
59
65
|
readonly secret: string;
|
|
60
66
|
readonly webhookPath: string;
|
|
@@ -92,11 +98,6 @@ export interface QqWireSegment {
|
|
|
92
98
|
readonly data?: Record<string, unknown>;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
export interface ParsedSendTarget {
|
|
96
|
-
readonly kind: QqChannelKind;
|
|
97
|
-
readonly id: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
101
|
export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig {
|
|
101
102
|
const entry = config.endpoints?.find((item) => item.context === 'qq' || !item.context);
|
|
102
103
|
const appid = pickCredential(config.appid, entry?.appid, process.env.QQ_APPID, process.env.QQ_BOT_APPID);
|
|
@@ -106,8 +107,8 @@ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig
|
|
|
106
107
|
'QQ adapter requires appid + secret (plugins.<key>.appid/secret or endpoints with context: qq)',
|
|
107
108
|
);
|
|
108
109
|
}
|
|
109
|
-
const
|
|
110
|
-
|| (typeof entry?.
|
|
110
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
111
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
111
112
|
|| process.env.QQ_BOT_NAME
|
|
112
113
|
|| 'qq-bot';
|
|
113
114
|
const mode = config.mode ?? entry?.mode ?? 'websocket';
|
|
@@ -118,7 +119,7 @@ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig
|
|
|
118
119
|
return {
|
|
119
120
|
context: 'qq',
|
|
120
121
|
mode,
|
|
121
|
-
|
|
122
|
+
id,
|
|
122
123
|
appid,
|
|
123
124
|
secret,
|
|
124
125
|
webhookPath,
|
|
@@ -129,34 +130,39 @@ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig
|
|
|
129
130
|
return {
|
|
130
131
|
context: 'qq',
|
|
131
132
|
mode: 'websocket',
|
|
132
|
-
|
|
133
|
+
id,
|
|
133
134
|
appid,
|
|
134
135
|
secret,
|
|
135
136
|
sandbox: config.sandbox === true || entry?.sandbox === true,
|
|
136
|
-
intents:
|
|
137
|
+
intents: resolveQqIntents({
|
|
138
|
+
intents: config.intents ?? entry?.intents,
|
|
139
|
+
botKind: config.botKind ?? entry?.botKind,
|
|
140
|
+
}),
|
|
137
141
|
accessTokenUrl: config.accessTokenUrl ?? entry?.accessTokenUrl,
|
|
138
142
|
gatewayUrl: config.gatewayUrl ?? entry?.gatewayUrl,
|
|
139
143
|
};
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
/**
|
|
143
|
-
*
|
|
147
|
+
* 入站归一化 → ConversationRef:`direct`(频道私信)映射为 guild 容器内的
|
|
148
|
+
* private 会话;频道消息的所属 guild 进 `parent`。
|
|
144
149
|
*/
|
|
145
|
-
export function
|
|
146
|
-
|
|
150
|
+
export function qqInboundConversation(endpointKey: string, msg: QqInboundMessage): ConversationRef {
|
|
151
|
+
const kind: ConversationKind = msg.channelKind === 'direct' ? 'private' : msg.channelKind;
|
|
152
|
+
return {
|
|
153
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
154
|
+
kind,
|
|
155
|
+
id: msg.channelId,
|
|
156
|
+
...(msg.guildId && (msg.channelKind === 'channel' || msg.channelKind === 'direct')
|
|
157
|
+
? { parent: { kind: 'channel' as const, id: msg.guildId } }
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
147
160
|
}
|
|
148
161
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const head = target.slice(0, sep);
|
|
155
|
-
const rest = target.slice(sep + 1);
|
|
156
|
-
if (head === 'private' || head === 'group' || head === 'channel' || head === 'direct') {
|
|
157
|
-
return { kind: head, id: rest };
|
|
158
|
-
}
|
|
159
|
-
return { kind: 'private', id: target };
|
|
162
|
+
/** 出站:guild 容器内的 private 会话即 QQ `direct`(频道私信)。 */
|
|
163
|
+
export function qqOutboundKind(conversation: ConversationRef): QqChannelKind {
|
|
164
|
+
if (conversation.kind === 'private' && conversation.parent?.kind === 'channel') return 'direct';
|
|
165
|
+
return conversation.kind;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
export function senderDisplayName(msg: QqInboundMessage): string {
|
package/src/qq-agent-deps.ts
CHANGED
|
@@ -25,20 +25,20 @@ export interface QqAgentEndpoint {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface QqAgentDeps {
|
|
28
|
-
getEndpoint: (
|
|
28
|
+
getEndpoint: (endpointKey: string) => QqAgentEndpoint;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const endpoints = new Map<string, QqAgentEndpoint>();
|
|
32
32
|
let override: QqAgentDeps | null = null;
|
|
33
33
|
|
|
34
34
|
export function registerQqAgentEndpoint(
|
|
35
|
-
|
|
35
|
+
endpointKey: string,
|
|
36
36
|
endpoint: QqAgentEndpoint,
|
|
37
37
|
): () => void {
|
|
38
|
-
endpoints.set(
|
|
38
|
+
endpoints.set(endpointKey, endpoint);
|
|
39
39
|
return () => {
|
|
40
|
-
if (endpoints.get(
|
|
41
|
-
endpoints.delete(
|
|
40
|
+
if (endpoints.get(endpointKey) === endpoint) {
|
|
41
|
+
endpoints.delete(endpointKey);
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
}
|
|
@@ -51,9 +51,9 @@ export function setQqAgentDeps(deps: QqAgentDeps | null): void {
|
|
|
51
51
|
export function getQqAgentDeps(): QqAgentDeps {
|
|
52
52
|
if (override) return override;
|
|
53
53
|
return {
|
|
54
|
-
getEndpoint(
|
|
55
|
-
const registered = endpoints.get(
|
|
56
|
-
if (!registered) throw new Error(`Endpoint ${
|
|
54
|
+
getEndpoint(endpointKey: string): QqAgentEndpoint {
|
|
55
|
+
const registered = endpoints.get(endpointKey);
|
|
56
|
+
if (!registered) throw new Error(`Endpoint ${endpointKey} 不存在`);
|
|
57
57
|
return registered;
|
|
58
58
|
},
|
|
59
59
|
};
|
package/src/qq-bind-persist.ts
CHANGED
|
@@ -14,9 +14,9 @@ export interface QqCredentialEnvKeys {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** 按 endpoint 名称生成唯一 env 键(如 `QQ_MY_BOT_APPID`) */
|
|
17
|
-
export function buildQqCredentialEnvKeys(
|
|
18
|
-
const appidKey = buildEndpointEnvKey('qq',
|
|
19
|
-
const secretKey = buildEndpointEnvKey('qq',
|
|
17
|
+
export function buildQqCredentialEnvKeys(endpointId: string): QqCredentialEnvKeys {
|
|
18
|
+
const appidKey = buildEndpointEnvKey('qq', endpointId, 'appid');
|
|
19
|
+
const secretKey = buildEndpointEnvKey('qq', endpointId, 'secret');
|
|
20
20
|
return {
|
|
21
21
|
appidKey,
|
|
22
22
|
secretKey,
|
|
@@ -27,12 +27,12 @@ export function buildQqCredentialEnvKeys(endpointName: string): QqCredentialEnvK
|
|
|
27
27
|
|
|
28
28
|
/** 写入或更新 `.env` 中的键值,并同步到当前进程 `process.env` */
|
|
29
29
|
export function persistQqCredentialsToEnv(
|
|
30
|
-
|
|
30
|
+
endpointId: string,
|
|
31
31
|
appId: string,
|
|
32
32
|
appSecret: string,
|
|
33
33
|
projectRoot?: string,
|
|
34
34
|
): QqCredentialEnvKeys {
|
|
35
|
-
const keys = buildQqCredentialEnvKeys(
|
|
35
|
+
const keys = buildQqCredentialEnvKeys(endpointId);
|
|
36
36
|
persistEndpointEnvValues(
|
|
37
37
|
{
|
|
38
38
|
[keys.appidKey]: appId,
|