@zhin.js/adapter-qq 1.1.17 → 1.1.18
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 +43 -0
- package/README.md +56 -5
- package/adapters/qq/index.js +7 -2
- package/adapters/qq/index.ts +7 -2
- package/agents/qq/boundaries.md +2 -0
- package/agents/qq/conversation.js +92 -0
- package/agents/qq/conversation.ts +99 -0
- package/agents/qq/messages.js +60 -0
- package/agents/qq/messages.ts +60 -0
- package/agents/qq/skills/qq/SKILL.md +8 -2
- package/agents/qq/skills/qq/tools/guild_kick_member/index.js +30 -0
- package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +29 -0
- package/agents/qq/skills/qq/tools/guild_mute_member/index.js +31 -0
- package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +30 -0
- package/agents/qq/skills/qq-conversation/SKILL.md +24 -0
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +78 -0
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +76 -0
- package/agents/qq/skills/qq-group-admin/SKILL.md +34 -0
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +39 -0
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +38 -0
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +33 -0
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +32 -0
- package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.js +25 -0
- package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.ts +25 -0
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +37 -0
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +36 -0
- package/agents/qq/skills/qq-group-directory/SKILL.md +24 -0
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +16 -0
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +16 -0
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +19 -0
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +19 -0
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +19 -0
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +19 -0
- package/agents/qq/skills/qq-group-state/SKILL.md +21 -0
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +19 -0
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +19 -0
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +19 -0
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +19 -0
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +21 -0
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +21 -0
- package/agents/qq/skills/qq-messages/SKILL.md +27 -0
- package/agents/qq/skills/qq-messages/tools/quote_reply/index.js +21 -0
- package/agents/qq/skills/qq-messages/tools/quote_reply/index.ts +21 -0
- package/agents/qq/skills/qq-messages/tools/recall_own_message/index.js +18 -0
- package/agents/qq/skills/qq-messages/tools/recall_own_message/index.ts +17 -0
- package/commands/qq/endpoint/definition.js +35 -22
- package/commands/qq/endpoint/definition.ts +37 -22
- package/lib/console.d.ts +8 -0
- package/lib/console.js +84 -0
- package/lib/endpoint.d.ts +10 -1
- package/lib/endpoint.js +134 -8
- package/lib/group-admin.d.ts +36 -0
- package/lib/group-admin.js +113 -0
- package/lib/protocol.d.ts +5 -0
- package/lib/protocol.js +4 -0
- package/lib/qq-console-spec.d.ts +17 -0
- package/lib/qq-console-spec.js +10 -0
- package/lib/qq-console.d.ts +11 -0
- package/lib/qq-console.js +116 -0
- package/lib/qq-endpoint-write.d.ts +22 -0
- package/lib/qq-endpoint-write.js +34 -0
- package/lib/qq-runtime-state.d.ts +19 -0
- package/lib/qq-runtime-state.js +1 -0
- package/package.json +17 -9
- package/pages/index/index.js +15 -0
- package/pages/index/index.ts +16 -0
- package/plugin.js +5 -2
- package/schema.json +174 -0
- package/src/console.ts +96 -0
- package/src/endpoint.ts +137 -8
- package/src/group-admin.ts +153 -0
- package/src/protocol.ts +8 -0
- package/src/qq-console-spec.ts +18 -0
- package/src/qq-console.ts +124 -0
- package/src/qq-endpoint-write.ts +52 -0
- package/src/qq-runtime-state.ts +21 -0
package/src/endpoint.ts
CHANGED
|
@@ -3,18 +3,27 @@ import { projectQqGuildSenderRoles } from './platform-permit.js';
|
|
|
3
3
|
import { qqDeliveryFailure } from './delivery-error.js';
|
|
4
4
|
import { normalizeQqInteraction } from './interaction.js';
|
|
5
5
|
import { awaitQqStartup } from './start-deadline.js';
|
|
6
|
-
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
6
|
+
import { EndpointDeliveryError, type MessageRef } from '@zhin.js/im-contract';
|
|
7
|
+
import {
|
|
8
|
+
inspectQqGroupMember,
|
|
9
|
+
inspectQqGroupMemberProfile,
|
|
10
|
+
inspectQqGroupScene,
|
|
11
|
+
kickQqGroupMember,
|
|
12
|
+
moderateQqGroupMember,
|
|
13
|
+
muteQqGroupMember,
|
|
14
|
+
qqGroupService,
|
|
15
|
+
} from './group-admin.js';
|
|
7
16
|
/**
|
|
8
17
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
9
18
|
*/
|
|
10
19
|
import {
|
|
11
20
|
Endpoint, createEndpointLifecycle, type EndpointLifecycle,
|
|
12
|
-
createRecallEndpointControl,
|
|
13
21
|
type EndpointControl,
|
|
14
22
|
type EndpointChannel,
|
|
15
23
|
type EndpointManagement,
|
|
16
24
|
type EndpointSendRequest,
|
|
17
25
|
type EndpointTransportState,
|
|
26
|
+
type AdapterOutageNotifier,
|
|
18
27
|
} from 'zhin.js/adapter';
|
|
19
28
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
20
29
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
@@ -59,6 +68,8 @@ export interface QqEndpointOptions {
|
|
|
59
68
|
readonly id: CapabilityId;
|
|
60
69
|
readonly config: ResolvedQqWebsocketConfig;
|
|
61
70
|
readonly createBot?: CreateQqBot;
|
|
71
|
+
/** Out-of-band outage notifier; absent in tests/hosts without outbound. */
|
|
72
|
+
readonly notifier?: AdapterOutageNotifier;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
@@ -68,14 +79,15 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
68
79
|
readonly #createBot: CreateQqBot;
|
|
69
80
|
#bot: QqBotTransport | null = null;
|
|
70
81
|
#open = false;
|
|
82
|
+
#offlineNoticeAt = 0;
|
|
71
83
|
readonly #seenInteractions = new Map<string, number>();
|
|
72
84
|
/** Recent msg_id / event_id per conversation, so replies go out as passive messages. */
|
|
73
85
|
readonly #passiveReplies = new QqPassiveReplyLedger();
|
|
74
86
|
readonly #activeGate = new QqActiveMessageGate();
|
|
75
87
|
readonly #lifecycle: EndpointLifecycle;
|
|
76
88
|
#cleanup?: () => Promise<void>;
|
|
77
|
-
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot());
|
|
78
|
-
readonly control: EndpointControl =
|
|
89
|
+
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#bot !== null);
|
|
90
|
+
readonly control: EndpointControl = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
79
91
|
|
|
80
92
|
constructor(options: QqEndpointOptions) {
|
|
81
93
|
super();
|
|
@@ -121,7 +133,12 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
121
133
|
this.#cleanup = cleanup;
|
|
122
134
|
handle.onForceClose(() => { void cleanup(); });
|
|
123
135
|
try {
|
|
124
|
-
bot.on('transport.close', () => {
|
|
136
|
+
bot.on('transport.close', () => {
|
|
137
|
+
if (cancelled || this.#bot !== bot) return;
|
|
138
|
+
this.#notifyOffline('transport_close');
|
|
139
|
+
void cleanup();
|
|
140
|
+
handle.notifyClosed();
|
|
141
|
+
});
|
|
125
142
|
this.#bindBot(bot);
|
|
126
143
|
await awaitQqStartup(bot.start().then(async () => {
|
|
127
144
|
if (cancelled) await stopBot();
|
|
@@ -134,11 +151,13 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
134
151
|
await stopping;
|
|
135
152
|
return;
|
|
136
153
|
}
|
|
154
|
+
this.#offlineNoticeAt = 0;
|
|
137
155
|
this.#logger.info(`connected (websocket) | appid: ${this.#options.config.appid}`);
|
|
138
156
|
} catch (error) {
|
|
139
157
|
const wasCancelled = cancelled;
|
|
140
158
|
await cleanup();
|
|
141
159
|
if (wasCancelled) return;
|
|
160
|
+
this.#notifyOffline('start_failed');
|
|
142
161
|
const raw = error instanceof Error ? error.message : String(error);
|
|
143
162
|
throw new Error(
|
|
144
163
|
`QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`,
|
|
@@ -273,6 +292,24 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
273
292
|
return true;
|
|
274
293
|
}
|
|
275
294
|
|
|
295
|
+
#notifyOffline(reason: string): void {
|
|
296
|
+
const notifier = this.#options.notifier;
|
|
297
|
+
if (!notifier?.configured) return;
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
if (now - this.#offlineNoticeAt < 60_000) return;
|
|
300
|
+
this.#offlineNoticeAt = now;
|
|
301
|
+
const text = `[Zhin] QQ 机器人 ${this.#options.config.id} 掉线(${reason})。正在自动重连。\n`
|
|
302
|
+
+ '恢复:在 QQ 回复 `qq endpoint add` 重新扫码绑定,或打开 Console 的 QQ 页面。';
|
|
303
|
+
void notifier.send(text).then((sent) => {
|
|
304
|
+
if (sent) return;
|
|
305
|
+
this.#logger.warn(formatCompact({
|
|
306
|
+
op: 'qq_notify_failed',
|
|
307
|
+
endpoint: this.#options.config.id,
|
|
308
|
+
reason,
|
|
309
|
+
}));
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
276
313
|
#bindBot(bot: QqBotTransport): void {
|
|
277
314
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
278
315
|
if (this.#bot !== bot) return;
|
|
@@ -316,6 +353,8 @@ export interface QqHttpEndpointOptions {
|
|
|
316
353
|
readonly http: HttpHost;
|
|
317
354
|
readonly config: ResolvedQqHttpConfig;
|
|
318
355
|
readonly createBot?: CreateQqHttpBot;
|
|
356
|
+
/** Out-of-band outage notifier; absent in tests/hosts without outbound. */
|
|
357
|
+
readonly notifier?: AdapterOutageNotifier;
|
|
319
358
|
}
|
|
320
359
|
|
|
321
360
|
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
@@ -326,14 +365,15 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
326
365
|
readonly #createBot: CreateQqHttpBot;
|
|
327
366
|
#bot: QqHttpBotTransport | null = null;
|
|
328
367
|
#open = false;
|
|
368
|
+
#offlineNoticeAt = 0;
|
|
329
369
|
readonly #seenInteractions = new Map<string, number>();
|
|
330
370
|
/** Recent msg_id / event_id per conversation, so replies go out as passive messages. */
|
|
331
371
|
readonly #passiveReplies = new QqPassiveReplyLedger();
|
|
332
372
|
readonly #activeGate = new QqActiveMessageGate();
|
|
333
373
|
readonly #lifecycle: EndpointLifecycle;
|
|
334
374
|
#cleanup?: () => Promise<void>;
|
|
335
|
-
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot());
|
|
336
|
-
readonly control: EndpointControl =
|
|
375
|
+
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#bot !== null);
|
|
376
|
+
readonly control: EndpointControl = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
337
377
|
|
|
338
378
|
constructor(options: QqHttpEndpointOptions) {
|
|
339
379
|
super();
|
|
@@ -396,11 +436,13 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
396
436
|
await stopping;
|
|
397
437
|
return;
|
|
398
438
|
}
|
|
439
|
+
this.#offlineNoticeAt = 0;
|
|
399
440
|
this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
|
|
400
441
|
} catch (error) {
|
|
401
442
|
const wasCancelled = cancelled;
|
|
402
443
|
await cleanup();
|
|
403
444
|
if (wasCancelled) return;
|
|
445
|
+
this.#notifyOffline('start_failed');
|
|
404
446
|
throw error;
|
|
405
447
|
}
|
|
406
448
|
});
|
|
@@ -530,6 +572,24 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
530
572
|
return true;
|
|
531
573
|
}
|
|
532
574
|
|
|
575
|
+
#notifyOffline(reason: string): void {
|
|
576
|
+
const notifier = this.#options.notifier;
|
|
577
|
+
if (!notifier?.configured) return;
|
|
578
|
+
const now = Date.now();
|
|
579
|
+
if (now - this.#offlineNoticeAt < 60_000) return;
|
|
580
|
+
this.#offlineNoticeAt = now;
|
|
581
|
+
const text = `[Zhin] QQ 机器人 ${this.#options.config.id} 掉线(${reason})。正在自动重连。\n`
|
|
582
|
+
+ '恢复:在 QQ 回复 `qq endpoint add` 重新扫码绑定,或打开 Console 的 QQ 页面。';
|
|
583
|
+
void notifier.send(text).then((sent) => {
|
|
584
|
+
if (sent) return;
|
|
585
|
+
this.#logger.warn(formatCompact({
|
|
586
|
+
op: 'qq_notify_failed',
|
|
587
|
+
endpoint: this.#options.config.id,
|
|
588
|
+
reason,
|
|
589
|
+
}));
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
533
593
|
#bindBot(bot: QqBotTransport): void {
|
|
534
594
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
535
595
|
if (this.#bot !== bot) return;
|
|
@@ -569,9 +629,25 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
569
629
|
}
|
|
570
630
|
|
|
571
631
|
function createQqEndpointManagement(
|
|
572
|
-
requireClient: () => Pick<QqBotTransport, 'getGuilds' | 'getChannels'>,
|
|
632
|
+
requireClient: () => Pick<QqBotTransport, 'getGuilds' | 'getChannels' | 'api'>,
|
|
633
|
+
isActive: () => boolean,
|
|
573
634
|
): EndpointManagement {
|
|
635
|
+
const api = () => requireClient().api;
|
|
574
636
|
return Object.freeze<EndpointManagement>({
|
|
637
|
+
// QQ 群(group_openid / member_openid):官方 v2 群管理接口,部分为白名单能力(11253)。
|
|
638
|
+
inspectGroupMember: (group, user, signal) => inspectQqGroupMember(api(), group, user, signal),
|
|
639
|
+
inspectGroupScene: (group, signal) => inspectQqGroupScene(api(), group, signal),
|
|
640
|
+
inspectGroupMemberProfile: (group, user, signal) => inspectQqGroupMemberProfile(api(), group, user, signal),
|
|
641
|
+
moderateGroupMember: (group, user, action, signal) => moderateQqGroupMember(api(), group, user, action, signal, isActive),
|
|
642
|
+
async listGroupMembers(groupId: string): Promise<readonly unknown[]> {
|
|
643
|
+
return qqGroupService(api()).getMembers(groupId);
|
|
644
|
+
},
|
|
645
|
+
async kickGroupMember(groupId: string, userId: string): Promise<void> {
|
|
646
|
+
await kickQqGroupMember(api(), groupId, userId);
|
|
647
|
+
},
|
|
648
|
+
async muteGroupMember(groupId: string, userId: string, durationSeconds: number): Promise<void> {
|
|
649
|
+
await muteQqGroupMember(api(), groupId, userId, durationSeconds);
|
|
650
|
+
},
|
|
575
651
|
async listChannels(): Promise<readonly EndpointChannel[]> {
|
|
576
652
|
const channels: EndpointChannel[] = [];
|
|
577
653
|
const client = requireClient();
|
|
@@ -604,6 +680,59 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
604
680
|
: {};
|
|
605
681
|
}
|
|
606
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Recall + 频道表情回应。recall 同时接受 send() 的复合 ID 与入站原生消息 ID(目标取自 MessageRef.conversation),
|
|
685
|
+
* 因此群管理员机器人可撤回普通成员 2 分钟内的消息(官方限制由平台执行)。
|
|
686
|
+
* 表情回应只有频道子频道消息支持(官方无群 / 单聊表情回应接口)。
|
|
687
|
+
*/
|
|
688
|
+
function createQqEndpointControl(
|
|
689
|
+
requireBot: () => QqBotTransport,
|
|
690
|
+
recallById: (messageId: string) => Promise<void>,
|
|
691
|
+
): EndpointControl {
|
|
692
|
+
return Object.freeze<EndpointControl>({
|
|
693
|
+
recall: async (message: MessageRef) => {
|
|
694
|
+
if (isCompoundMessageId(message.id)) return recallById(message.id);
|
|
695
|
+
const kind = qqOutboundKind(message.conversation);
|
|
696
|
+
await recallQqMessage(requireBot(), kind, message.conversation.id, message.id);
|
|
697
|
+
},
|
|
698
|
+
addReaction: async (message: MessageRef, emoji: string) => {
|
|
699
|
+
const target = qqGuildReactionTarget(message, emoji);
|
|
700
|
+
if (!target) return null;
|
|
701
|
+
await requireBot().api.reactionService.addGuildMessageReaction(target.channelId, target.messageId, target.type, target.id);
|
|
702
|
+
return `${target.type}:${target.id}`;
|
|
703
|
+
},
|
|
704
|
+
removeReaction: async (message: MessageRef, reactionId: string) => {
|
|
705
|
+
const target = qqGuildReactionTarget(message, reactionId);
|
|
706
|
+
if (!target) return;
|
|
707
|
+
await requireBot().api.reactionService.deleteGuildMessageReaction(target.channelId, target.messageId, target.type, target.id);
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function isCompoundMessageId(id: string): boolean {
|
|
713
|
+
try { parseCompoundMessageId(id); return true; } catch { return false; }
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** `type:id`(1 系统表情 / 2 emoji)或单个 Unicode emoji(→ type 2 + 码点)。 */
|
|
717
|
+
export function parseQqEmoji(value: string): { type: 1 | 2; id: `${number}` } | null {
|
|
718
|
+
const explicit = /^([12]):(\d+)$/.exec(value.trim());
|
|
719
|
+
if (explicit) return { type: Number(explicit[1]) as 1 | 2, id: explicit[2] as `${number}` };
|
|
720
|
+
const chars = [...value.trim().replace(/\uFE0F/g, '')];
|
|
721
|
+
if (chars.length !== 1) return null;
|
|
722
|
+
const code = chars[0]!.codePointAt(0)!;
|
|
723
|
+
return code > 0x7f ? { type: 2, id: String(code) as `${number}` } : null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function qqGuildReactionTarget(message: MessageRef, emoji: string) {
|
|
727
|
+
let kind = qqOutboundKind(message.conversation);
|
|
728
|
+
let channelId = message.conversation.id;
|
|
729
|
+
let messageId = message.id;
|
|
730
|
+
if (isCompoundMessageId(message.id)) ({ kind, channelId, qqMsgId: messageId } = parseCompoundMessageId(message.id));
|
|
731
|
+
if (kind !== 'channel' || !channelId || !messageId) return null;
|
|
732
|
+
const parsed = parseQqEmoji(emoji);
|
|
733
|
+
return parsed ? { channelId, messageId, ...parsed } : null;
|
|
734
|
+
}
|
|
735
|
+
|
|
607
736
|
async function recallQqMessage(bot: QqBotTransport, kind: ReturnType<typeof qqOutboundKind>, channelId: string, messageId: string): Promise<void> {
|
|
608
737
|
const recall = { private: bot.recallPrivateMessage, group: bot.recallGroupMessage,
|
|
609
738
|
channel: bot.recallGuildMessage, direct: bot.recallDirectMessage }[kind];
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ 群(group_openid / member_openid)管理:只走官方 v2 群管理接口。
|
|
3
|
+
*
|
|
4
|
+
* 官方限制(https://bot.q.qq.com/wiki/develop/api-v2/autogen/):
|
|
5
|
+
* - 成员列表 / 成员信息 / 批量移除 / 黑名单 / 入群申请多为白名单能力,无权限返回 11253;
|
|
6
|
+
* - 禁言与入群审批要求机器人是群管理员;禁言最长 30 天,只能禁言普通成员;
|
|
7
|
+
* - 没有「机器人所在群列表」「好友列表」「退群」「设管理员」「群公告」等接口。
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
GroupBotState,
|
|
11
|
+
GroupMemberInfo,
|
|
12
|
+
GroupMemberRole,
|
|
13
|
+
} from 'qq-official-bot';
|
|
14
|
+
import type {
|
|
15
|
+
GroupMemberProfile,
|
|
16
|
+
GroupModerationAction,
|
|
17
|
+
GroupModerationMember,
|
|
18
|
+
GroupSceneFacts,
|
|
19
|
+
} from 'zhin.js/adapter';
|
|
20
|
+
import type { QqOfficialApi } from './ws.js';
|
|
21
|
+
|
|
22
|
+
/** 官方 restrict_chat_setting 上限:30 天。 */
|
|
23
|
+
export const QQ_GROUP_MUTE_MAX_SECONDS = 30 * 24 * 60 * 60;
|
|
24
|
+
|
|
25
|
+
export type QqGroupService = QqOfficialApi['groupService'];
|
|
26
|
+
|
|
27
|
+
export function qqGroupService(api: Partial<QqOfficialApi> | undefined): QqGroupService {
|
|
28
|
+
const service = api?.groupService;
|
|
29
|
+
if (!service) throw new Error('QQ group management API is unavailable on this transport');
|
|
30
|
+
return service;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isQqGroupManagerRole(role: unknown): role is 'owner' | 'admin' {
|
|
34
|
+
return role === 'owner' || role === 'admin';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function platformRole(role: unknown): GroupMemberRole | undefined {
|
|
38
|
+
return role === 'owner' || role === 'admin' || role === 'member' ? role : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requireId(value: string, what: string): string {
|
|
42
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
43
|
+
if (!id || /[\s/?#]/.test(id)) throw new Error(`Invalid QQ ${what}`);
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** RFC3339 → epoch ms;非法或未来时间返回 undefined(从不猜测)。 */
|
|
48
|
+
export function parseQqTime(value: unknown, { allowFuture = false } = {}): number | undefined {
|
|
49
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
50
|
+
const at = Date.parse(value);
|
|
51
|
+
if (!Number.isFinite(at) || at <= 0) return undefined;
|
|
52
|
+
if (!allowFuture && at > Date.now()) return undefined;
|
|
53
|
+
return at;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 只读、实时:机器人在群里的状态(角色、是否允许主动消息、接收设置)。 */
|
|
57
|
+
export async function qqGroupBotState(api: Partial<QqOfficialApi> | undefined, groupId: string): Promise<GroupBotState> {
|
|
58
|
+
return qqGroupService(api).getBotState(requireId(groupId, 'group id'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function qqGroupMember(api: Partial<QqOfficialApi> | undefined, groupId: string, userId: string): Promise<GroupMemberInfo> {
|
|
62
|
+
return qqGroupService(api).getMemberInfo(requireId(groupId, 'group id'), requireId(userId, 'member id'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 禁言 / 解除禁言一个普通成员;0 = 解除。 */
|
|
66
|
+
export async function muteQqGroupMember(
|
|
67
|
+
api: Partial<QqOfficialApi> | undefined, groupId: string, userId: string, durationSeconds: number,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 0 || durationSeconds > QQ_GROUP_MUTE_MAX_SECONDS) {
|
|
70
|
+
throw new RangeError(`QQ group mute duration must be 0..${QQ_GROUP_MUTE_MAX_SECONDS} seconds`);
|
|
71
|
+
}
|
|
72
|
+
const member = requireId(userId, 'member id');
|
|
73
|
+
await qqGroupService(api).setMemberMute(requireId(groupId, 'group id'), [durationSeconds === 0
|
|
74
|
+
? { op: 'del', member_openid: member, mute_expire_at: '' }
|
|
75
|
+
: { op: 'add', member_openid: member, mute_expire_at: new Date(Date.now() + durationSeconds * 1000).toISOString() }]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 移出一个成员;可选同时加入群黑名单。平台未确认 success 时抛错,不把未确认当成功。 */
|
|
79
|
+
export async function kickQqGroupMember(
|
|
80
|
+
api: Partial<QqOfficialApi> | undefined, groupId: string, userId: string, addToBlacklist = false,
|
|
81
|
+
): Promise<{ blacklistFailed: boolean }> {
|
|
82
|
+
const member = requireId(userId, 'member id');
|
|
83
|
+
const result = await qqGroupService(api).removeMembers(requireId(groupId, 'group id'), {
|
|
84
|
+
member_openids: [member],
|
|
85
|
+
...(addToBlacklist ? { add_to_member_blacklist: true } : {}),
|
|
86
|
+
});
|
|
87
|
+
if (result?.remove_members_result !== 'success') throw new Error('QQ did not confirm the member removal');
|
|
88
|
+
return { blacklistFailed: addToBlacklist && (result.add_to_member_blacklist_fail_openids ?? []).includes(member) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 自动审核用:机器人与目标成员的实时角色;平台只允许管理员处置普通成员(且非机器人)。 */
|
|
92
|
+
export async function inspectQqGroupMember(
|
|
93
|
+
api: Partial<QqOfficialApi> | undefined, groupId: string, userId: string, signal: AbortSignal,
|
|
94
|
+
): Promise<GroupModerationMember> {
|
|
95
|
+
signal.throwIfAborted();
|
|
96
|
+
const [self, member] = await Promise.all([qqGroupBotState(api, groupId), qqGroupMember(api, groupId, userId)]);
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
const role = member.member_openid === userId ? platformRole(member.member_role) : undefined;
|
|
99
|
+
const joinedAt = parseQqTime(member.joined_at);
|
|
100
|
+
return {
|
|
101
|
+
canModerate: isQqGroupManagerRole(self.member_role) && self.member_openid !== userId
|
|
102
|
+
&& member.member_openid === userId && role === 'member' && member.bot !== true,
|
|
103
|
+
...(role ? { platformRole: role } : {}),
|
|
104
|
+
...(joinedAt ? { joinedAt } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function moderateQqGroupMember(
|
|
109
|
+
api: Partial<QqOfficialApi> | undefined, groupId: string, userId: string,
|
|
110
|
+
action: GroupModerationAction, signal: AbortSignal, isActive: () => boolean,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const member = await inspectQqGroupMember(api, groupId, userId, signal);
|
|
113
|
+
signal.throwIfAborted();
|
|
114
|
+
if (!isActive() || !member.canModerate) throw new Error('Group moderation permission is unavailable');
|
|
115
|
+
if (action.type === 'kick') {
|
|
116
|
+
if (!Number.isSafeInteger(action.maxMembershipAgeMs) || action.maxMembershipAgeMs <= 0
|
|
117
|
+
|| !member.joinedAt || member.joinedAt !== action.joinedAt
|
|
118
|
+
|| Date.now() - member.joinedAt > action.maxMembershipAgeMs) {
|
|
119
|
+
throw new Error('Group membership no longer matches the approved newcomer window');
|
|
120
|
+
}
|
|
121
|
+
await kickQqGroupMember(api, groupId, userId);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!Number.isSafeInteger(action.durationSeconds) || action.durationSeconds <= 0) {
|
|
125
|
+
throw new Error('Invalid group mute duration');
|
|
126
|
+
}
|
|
127
|
+
await muteQqGroupMember(api, groupId, userId, Math.min(action.durationSeconds, QQ_GROUP_MUTE_MAX_SECONDS));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** ADR 0041 场景事实:一次群信息读取;官方群信息不含群主/管理员/全员禁言,留作 unknown。 */
|
|
131
|
+
export async function inspectQqGroupScene(
|
|
132
|
+
api: Partial<QqOfficialApi> | undefined, groupId: string, signal: AbortSignal,
|
|
133
|
+
): Promise<GroupSceneFacts> {
|
|
134
|
+
signal.throwIfAborted();
|
|
135
|
+
const info = await qqGroupService(api).getInfo(requireId(groupId, 'group id'));
|
|
136
|
+
signal.throwIfAborted();
|
|
137
|
+
if (info?.group_openid && info.group_openid !== groupId) return {};
|
|
138
|
+
const name = typeof info?.group_name === 'string' && info.group_name.trim() ? info.group_name.trim() : undefined;
|
|
139
|
+
const memberCount = Number.isSafeInteger(info?.group_member_num) && info.group_member_num > 0 ? info.group_member_num : undefined;
|
|
140
|
+
return { ...(name ? { name } : {}), ...(memberCount ? { memberCount } : {}) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** ADR 0041 成员事实:一次成员读取(群名片/头衔/禁言到期官方不提供)。 */
|
|
144
|
+
export async function inspectQqGroupMemberProfile(
|
|
145
|
+
api: Partial<QqOfficialApi> | undefined, groupId: string, userId: string, signal: AbortSignal,
|
|
146
|
+
): Promise<GroupMemberProfile> {
|
|
147
|
+
signal.throwIfAborted();
|
|
148
|
+
const member = await qqGroupMember(api, groupId, userId);
|
|
149
|
+
signal.throwIfAborted();
|
|
150
|
+
if (member.member_openid !== userId) return {};
|
|
151
|
+
const role = platformRole(member.member_role);
|
|
152
|
+
return role ? { platformRole: role } : {};
|
|
153
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { EndpointDeliveryError, type ConversationKind, type ConversationRef } from '@zhin.js/im-contract';
|
|
4
4
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
5
5
|
import { resolveQqIntents } from './qq-intents.js';
|
|
6
|
+
import { resolveAdapterNotifyTarget, type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
|
|
6
7
|
|
|
7
8
|
const logger = getLogger('qq');
|
|
8
9
|
|
|
@@ -22,6 +23,8 @@ export interface QqEndpointConfig {
|
|
|
22
23
|
readonly gatewayUrl?: string;
|
|
23
24
|
readonly streamProxy?: import('./stream-proxy.js').QqStreamProxy;
|
|
24
25
|
readonly webhookPath?: string;
|
|
26
|
+
/** Optional out-of-band outage notification target (usually an ICQQ instance). */
|
|
27
|
+
readonly notify?: AdapterNotifyTarget;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface ResolvedQqWebsocketConfig {
|
|
@@ -35,6 +38,7 @@ export interface ResolvedQqWebsocketConfig {
|
|
|
35
38
|
readonly accessTokenUrl?: string;
|
|
36
39
|
readonly gatewayUrl?: string;
|
|
37
40
|
readonly streamProxy?: import('./stream-proxy.js').QqStreamProxy;
|
|
41
|
+
readonly notify?: ResolvedAdapterNotifyTarget;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export interface ResolvedQqHttpConfig {
|
|
@@ -45,6 +49,7 @@ export interface ResolvedQqHttpConfig {
|
|
|
45
49
|
readonly secret: string;
|
|
46
50
|
readonly webhookPath: string;
|
|
47
51
|
readonly sandbox: boolean;
|
|
52
|
+
readonly notify?: ResolvedAdapterNotifyTarget;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export type ResolvedQqConfig = ResolvedQqWebsocketConfig | ResolvedQqHttpConfig;
|
|
@@ -83,6 +88,7 @@ export function resolveQqConfig(config: QqEndpointConfig): ResolvedQqConfig {
|
|
|
83
88
|
const appid = requiredEndpointField(config.appid, 'appid');
|
|
84
89
|
const secret = requiredEndpointField(config.secret, 'secret');
|
|
85
90
|
const mode = config.mode ?? 'websocket';
|
|
91
|
+
const notify = resolveAdapterNotifyTarget(config.notify);
|
|
86
92
|
|
|
87
93
|
if (mode === 'webhook' || mode === 'middleware') {
|
|
88
94
|
const webhookPathRaw = config.webhookPath ?? '/qq/webhook';
|
|
@@ -95,6 +101,7 @@ export function resolveQqConfig(config: QqEndpointConfig): ResolvedQqConfig {
|
|
|
95
101
|
secret,
|
|
96
102
|
webhookPath,
|
|
97
103
|
sandbox: config.sandbox === true,
|
|
104
|
+
...(notify ? { notify } : {}),
|
|
98
105
|
};
|
|
99
106
|
}
|
|
100
107
|
|
|
@@ -114,6 +121,7 @@ export function resolveQqConfig(config: QqEndpointConfig): ResolvedQqConfig {
|
|
|
114
121
|
accessTokenUrl: config.accessTokenUrl,
|
|
115
122
|
gatewayUrl: config.gatewayUrl,
|
|
116
123
|
...(config.streamProxy ? { streamProxy: config.streamProxy } : {}),
|
|
124
|
+
...(notify ? { notify } : {}),
|
|
117
125
|
};
|
|
118
126
|
}
|
|
119
127
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Console 页面元数据(浏览器与服务端共享;不要在这里 import 服务端代码)。
|
|
3
|
+
*
|
|
4
|
+
* QQ 的 endpoint 不走通用「字段表单」:`qq endpoint add` 是扫码绑定流程,
|
|
5
|
+
* 因此 `canAdd: false`,改由 `qq-console.ts` 的 `qq-bind-*` 动作完成。
|
|
6
|
+
*/
|
|
7
|
+
import type { AdapterConsoleField } from 'zhin.js/adapter';
|
|
8
|
+
|
|
9
|
+
export const qqConsoleSpec = {
|
|
10
|
+
adapterKey: 'qq',
|
|
11
|
+
adapterDisplayName: 'QQ',
|
|
12
|
+
fields: [] as readonly AdapterConsoleField[],
|
|
13
|
+
canAdd: false,
|
|
14
|
+
canRemove: true,
|
|
15
|
+
docsUrl: 'https://zhin.js.org/adapters/qq',
|
|
16
|
+
readPermission: 'qq:read',
|
|
17
|
+
writePermission: 'qq:write',
|
|
18
|
+
} as const;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Console 扫码绑定:把原本只能在聊天里执行的 `qq endpoint add` 流程
|
|
3
|
+
* 搬到 Console 页面。
|
|
4
|
+
*
|
|
5
|
+
* - `start` 返回扫码链接;后续状态写入 `state.consoleBind`,页面轮询 status。
|
|
6
|
+
* - 扫码成功后凭据只留在内存,等页面选择公域 / 私域再一次性写 `.env` + 配置。
|
|
7
|
+
* - 与聊天命令共享 `state.bindFlow` 单例,同一时间只允许一个绑定流程。
|
|
8
|
+
*/
|
|
9
|
+
import { PluginConsoleRpcError } from 'zhin.js';
|
|
10
|
+
import { startQqBindFlow } from './qq-bind-flow.js';
|
|
11
|
+
import { writeQqEndpointWithBotKind, type QqEndpointWriteResult } from './qq-endpoint-write.js';
|
|
12
|
+
import { parseQqBotKind } from './qq-intents.js';
|
|
13
|
+
import type { QqRuntimeState } from './qq-runtime-state.js';
|
|
14
|
+
import type { EndpointConfigurationStore } from 'zhin.js/adapter';
|
|
15
|
+
|
|
16
|
+
/** 启动 Console 扫码;resolve 时二维码链接已就绪。 */
|
|
17
|
+
export function startQqConsoleBind(
|
|
18
|
+
state: QqRuntimeState,
|
|
19
|
+
id?: string,
|
|
20
|
+
): Promise<{ readonly url: string }> {
|
|
21
|
+
if (state.bindFlow || state.pendingBotKind) {
|
|
22
|
+
throw new PluginConsoleRpcError('conflict', '已有扫码绑定流程在进行,请先完成或取消');
|
|
23
|
+
}
|
|
24
|
+
if (state.consoleBind?.pending) {
|
|
25
|
+
throw new PluginConsoleRpcError('conflict', '已经扫码成功,请先选择公域或私域');
|
|
26
|
+
}
|
|
27
|
+
const endpointId = id?.trim() || undefined;
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let settled = false;
|
|
30
|
+
const stop = startQqBindFlow({
|
|
31
|
+
onQrDisplayed: (url) => {
|
|
32
|
+
state.consoleBind = {
|
|
33
|
+
state: 'waiting',
|
|
34
|
+
url,
|
|
35
|
+
message: '请用手机 QQ 扫码',
|
|
36
|
+
startedAt: state.consoleBind?.startedAt ?? Date.now(),
|
|
37
|
+
};
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
resolve({ url });
|
|
41
|
+
},
|
|
42
|
+
onQrExpired: () => {
|
|
43
|
+
state.consoleBind = {
|
|
44
|
+
state: 'waiting',
|
|
45
|
+
...(state.consoleBind?.url ? { url: state.consoleBind.url } : {}),
|
|
46
|
+
message: '二维码已过期,正在刷新…',
|
|
47
|
+
startedAt: state.consoleBind?.startedAt ?? Date.now(),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
onSuccess: (credentials) => {
|
|
51
|
+
state.bindFlow = null;
|
|
52
|
+
const [first] = credentials;
|
|
53
|
+
if (!first) {
|
|
54
|
+
state.consoleBind = {
|
|
55
|
+
state: 'failed',
|
|
56
|
+
error: '扫码结果为空,请重新扫码',
|
|
57
|
+
startedAt: state.consoleBind?.startedAt ?? Date.now(),
|
|
58
|
+
};
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
state.consoleBind = {
|
|
62
|
+
state: 'scanned',
|
|
63
|
+
...(state.consoleBind?.url ? { url: state.consoleBind.url } : {}),
|
|
64
|
+
message: '已扫码,请选择公域或私域',
|
|
65
|
+
startedAt: state.consoleBind?.startedAt ?? Date.now(),
|
|
66
|
+
pending: {
|
|
67
|
+
endpointId: endpointId ?? first.appId,
|
|
68
|
+
appId: first.appId,
|
|
69
|
+
appSecret: first.appSecret,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
onFailure: (error) => {
|
|
74
|
+
state.bindFlow = null;
|
|
75
|
+
state.consoleBind = {
|
|
76
|
+
state: 'failed',
|
|
77
|
+
error: error.message,
|
|
78
|
+
startedAt: state.consoleBind?.startedAt ?? Date.now(),
|
|
79
|
+
};
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
reject(new PluginConsoleRpcError('unavailable', error.message));
|
|
83
|
+
},
|
|
84
|
+
}, { source: 'zhin' });
|
|
85
|
+
state.bindFlow = { id: endpointId, stop, locale: 'zh-CN' };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 扫码成功后选择公域 / 私域并写回配置。 */
|
|
90
|
+
export async function completeQqConsoleBind(
|
|
91
|
+
state: QqRuntimeState,
|
|
92
|
+
store: EndpointConfigurationStore,
|
|
93
|
+
kindValue: unknown,
|
|
94
|
+
): Promise<QqEndpointWriteResult> {
|
|
95
|
+
const pending = state.consoleBind?.pending;
|
|
96
|
+
if (!pending) {
|
|
97
|
+
throw new PluginConsoleRpcError('conflict', '还没有扫码成功,请先扫码绑定');
|
|
98
|
+
}
|
|
99
|
+
const botKind = parseQqBotKind(kindValue);
|
|
100
|
+
if (!botKind) {
|
|
101
|
+
throw new PluginConsoleRpcError('invalid_input', '请选择公域(public)或私域(private)');
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const result = await writeQqEndpointWithBotKind({ ...pending, botKind }, store);
|
|
105
|
+
state.consoleBind = null;
|
|
106
|
+
return result;
|
|
107
|
+
} catch {
|
|
108
|
+
throw new PluginConsoleRpcError('unavailable', '写入配置失败,请检查配置文件和 .env 的权限');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 取消 Console 绑定(也可用于收拾聊天命令发起的流程)。 */
|
|
113
|
+
export function cancelQqConsoleBind(state: QqRuntimeState): boolean {
|
|
114
|
+
const active = Boolean(state.bindFlow || state.pendingBotKind || state.consoleBind);
|
|
115
|
+
try {
|
|
116
|
+
state.bindFlow?.stop();
|
|
117
|
+
} catch {
|
|
118
|
+
// best effort
|
|
119
|
+
}
|
|
120
|
+
state.bindFlow = null;
|
|
121
|
+
state.pendingBotKind = null;
|
|
122
|
+
state.consoleBind = null;
|
|
123
|
+
return active;
|
|
124
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ endpoint 配置写回:扫码成功后把 appId / appSecret 写进 `.env`,
|
|
3
|
+
* 配置里只保留 `${ENV}` 引用,并按公域 / 私域展开 intents。
|
|
4
|
+
*
|
|
5
|
+
* 命令(`qq endpoint add`)与 Console 扫码绑定共用这一处实现。
|
|
6
|
+
*/
|
|
7
|
+
import { buildEndpointEnvKey, type EndpointConfigurationStore } from 'zhin.js/adapter';
|
|
8
|
+
import { defaultQqEndpointIntentFields, type QqBotKind } from './qq-intents.js';
|
|
9
|
+
|
|
10
|
+
export interface QqEndpointWriteInput {
|
|
11
|
+
readonly endpointId: string;
|
|
12
|
+
readonly appId: string;
|
|
13
|
+
readonly appSecret: string;
|
|
14
|
+
readonly botKind: QqBotKind;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface QqEndpointWriteResult {
|
|
18
|
+
readonly filePath: string;
|
|
19
|
+
readonly endpointId: string;
|
|
20
|
+
readonly botKind: QqBotKind;
|
|
21
|
+
readonly intents: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 写回 `.env` + `zhin.config.yml`;失败时抛原错误,由调用方决定文案。 */
|
|
25
|
+
export async function writeQqEndpointWithBotKind(
|
|
26
|
+
input: QqEndpointWriteInput,
|
|
27
|
+
store: EndpointConfigurationStore,
|
|
28
|
+
): Promise<QqEndpointWriteResult> {
|
|
29
|
+
const appidKey = buildEndpointEnvKey('qq', input.endpointId, 'appid');
|
|
30
|
+
const secretKey = buildEndpointEnvKey('qq', input.endpointId, 'secret');
|
|
31
|
+
const intentFields = defaultQqEndpointIntentFields(input.botKind);
|
|
32
|
+
const { filePath } = await store.add({
|
|
33
|
+
adapterKey: 'qq',
|
|
34
|
+
entry: {
|
|
35
|
+
id: input.endpointId,
|
|
36
|
+
appid: `\${${appidKey}}`,
|
|
37
|
+
secret: `\${${secretKey}}`,
|
|
38
|
+
botKind: intentFields.botKind,
|
|
39
|
+
intents: [...intentFields.intents],
|
|
40
|
+
},
|
|
41
|
+
environment: {
|
|
42
|
+
[appidKey]: input.appId,
|
|
43
|
+
[secretKey]: input.appSecret,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
filePath,
|
|
48
|
+
endpointId: input.endpointId,
|
|
49
|
+
botKind: intentFields.botKind,
|
|
50
|
+
intents: intentFields.intents,
|
|
51
|
+
};
|
|
52
|
+
}
|