@zhin.js/adapter-qq 5.0.2 → 5.0.3
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 +41 -0
- package/README.md +51 -417
- package/adapters/qq.ts +45 -0
- package/agent/tools/add_role.ts +2 -2
- package/agent/tools/channel_info.ts +2 -2
- package/agent/tools/create_role.ts +2 -2
- package/agent/tools/list_channels.ts +2 -2
- package/agent/tools/list_guilds.ts +2 -2
- package/agent/tools/list_roles.ts +2 -2
- package/agent/tools/member_detail.ts +2 -2
- package/agent/tools/remove_role.ts +2 -2
- package/lib/endpoint.d.ts +69 -0
- package/lib/endpoint.js +315 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/platform-permit.d.ts +16 -0
- package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
- package/lib/protocol.d.ts +96 -0
- package/lib/protocol.js +129 -0
- package/lib/qq-agent-deps.d.ts +21 -0
- package/lib/qq-agent-deps.js +30 -0
- package/lib/webhook.d.ts +18 -0
- package/lib/webhook.js +100 -0
- package/lib/ws.d.ts +24 -0
- package/lib/ws.js +118 -0
- package/package.json +44 -40
- package/plugin.ts +13 -0
- package/schema.json +51 -0
- package/src/endpoint.ts +314 -600
- package/src/index.ts +56 -186
- package/src/platform-permit.ts +1 -1
- package/src/protocol.ts +229 -0
- package/src/qq-agent-deps.ts +49 -12
- package/src/webhook.ts +127 -0
- package/src/ws.ts +172 -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 -30
- package/dist/index.js +0 -30
- package/lib/agent/tools/add_role.js +0 -23
- package/lib/agent/tools/add_role.js.map +0 -1
- package/lib/agent/tools/channel_info.js +0 -19
- package/lib/agent/tools/channel_info.js.map +0 -1
- package/lib/agent/tools/create_role.js +0 -22
- package/lib/agent/tools/create_role.js.map +0 -1
- package/lib/agent/tools/list_channels.js +0 -20
- package/lib/agent/tools/list_channels.js.map +0 -1
- package/lib/agent/tools/list_guilds.js +0 -17
- package/lib/agent/tools/list_guilds.js.map +0 -1
- package/lib/agent/tools/list_roles.js +0 -20
- package/lib/agent/tools/list_roles.js.map +0 -1
- package/lib/agent/tools/member_detail.js +0 -20
- package/lib/agent/tools/member_detail.js.map +0 -1
- package/lib/agent/tools/remove_role.js +0 -23
- package/lib/agent/tools/remove_role.js.map +0 -1
- package/lib/package.json +0 -105
- package/lib/src/adapter.js +0 -81
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/endpoint.js +0 -600
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/gateway-config.js +0 -32
- package/lib/src/gateway-config.js.map +0 -1
- package/lib/src/group-at-normalize.js +0 -68
- package/lib/src/group-at-normalize.js.map +0 -1
- package/lib/src/inbound-action.js +0 -57
- package/lib/src/inbound-action.js.map +0 -1
- package/lib/src/inbound-normalize.js +0 -47
- package/lib/src/inbound-normalize.js.map +0 -1
- package/lib/src/index.js +0 -194
- package/lib/src/index.js.map +0 -1
- package/lib/src/mention-enrich.js +0 -52
- package/lib/src/mention-enrich.js.map +0 -1
- package/lib/src/outbound-keyboard.js +0 -100
- package/lib/src/outbound-keyboard.js.map +0 -1
- package/lib/src/outbound-markdown.js +0 -189
- package/lib/src/outbound-markdown.js.map +0 -1
- package/lib/src/outbound-media.js +0 -51
- package/lib/src/outbound-media.js.map +0 -1
- package/lib/src/outbound-mixed-media.js +0 -78
- package/lib/src/outbound-mixed-media.js.map +0 -1
- package/lib/src/platform-permit.js.map +0 -1
- package/lib/src/qq-agent-deps.js +0 -10
- package/lib/src/qq-agent-deps.js.map +0 -1
- package/lib/src/qq-bind-api.js +0 -90
- package/lib/src/qq-bind-api.js.map +0 -1
- package/lib/src/qq-bind-flow.js +0 -86
- package/lib/src/qq-bind-flow.js.map +0 -1
- package/lib/src/qq-bind-persist.js +0 -52
- package/lib/src/qq-bind-persist.js.map +0 -1
- package/lib/src/qq-endpoint-manager.js +0 -159
- package/lib/src/qq-endpoint-manager.js.map +0 -1
- package/lib/src/sdk-version.js +0 -19
- package/lib/src/sdk-version.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js +0 -2
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -97
- package/src/gateway-config.ts +0 -48
- package/src/group-at-normalize.ts +0 -75
- package/src/inbound-action.ts +0 -87
- package/src/inbound-normalize.ts +0 -57
- package/src/mention-enrich.ts +0 -71
- package/src/outbound-keyboard.ts +0 -117
- package/src/outbound-markdown.ts +0 -212
- package/src/outbound-media.ts +0 -58
- package/src/outbound-mixed-media.ts +0 -101
- package/src/qq-bind-api.ts +0 -145
- package/src/qq-bind-flow.ts +0 -127
- package/src/qq-bind-persist.ts +0 -71
- package/src/qq-endpoint-manager.ts +0 -199
- package/src/sdk-version.ts +0 -21
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -52
package/src/endpoint.ts
CHANGED
|
@@ -1,675 +1,389 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QQ
|
|
2
|
+
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
6
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
7
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
9
|
+
import { registerQqAgentEndpoint } from './qq-agent-deps.js';
|
|
4
10
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import type { QQAdapter } from "./adapter.js";
|
|
16
|
-
import { normalizeQqInboundWsPayload, type QqWsPacket } from "./inbound-normalize.js";
|
|
17
|
-
import { normalizeGroupAtPrefix } from "./group-at-normalize.js";
|
|
18
|
-
import { SDK_VERSION, SDK_VERSION_HEADER } from "./sdk-version.js";
|
|
19
|
-
import { applyCustomAuthEndpoints } from "./gateway-config.js";
|
|
20
|
-
import { normalizeOutboundMarkdown, asOutboundSegments, splitReplyPrefix } from "./outbound-markdown.js";
|
|
21
|
-
import { normalizeOutboundMedia } from "./outbound-media.js";
|
|
22
|
-
import { fromCanonicalSegments, toCanonicalSegments } from "./segment-mapper.js";
|
|
23
|
-
import { enrichCanonicalMentionNames } from "./mention-enrich.js";
|
|
11
|
+
formatInboundContent,
|
|
12
|
+
formatInboundTarget,
|
|
13
|
+
formatOutboundText,
|
|
14
|
+
parseSendTarget,
|
|
15
|
+
resolveOutboundMessageId,
|
|
16
|
+
senderDisplayName,
|
|
17
|
+
type QqInboundMessage,
|
|
18
|
+
type ResolvedQqHttpConfig,
|
|
19
|
+
type ResolvedQqWebsocketConfig,
|
|
20
|
+
} from './protocol.js';
|
|
24
21
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw new Error(
|
|
49
|
-
code != null
|
|
50
|
-
? `QQ 发送消息失败(${String(code)})${msg ? `: ${String(msg)}` : ""}`
|
|
51
|
-
: "QQ 发送消息失败:响应缺少消息 ID",
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
return String(id);
|
|
22
|
+
defaultCreateHttpBot,
|
|
23
|
+
registerQqWebhookRoutes,
|
|
24
|
+
type CreateQqHttpBot,
|
|
25
|
+
type QqHttpBotTransport,
|
|
26
|
+
} from './webhook.js';
|
|
27
|
+
import {
|
|
28
|
+
bindQqBotInboundEvents,
|
|
29
|
+
defaultCreateBot,
|
|
30
|
+
normalizeQqMessage,
|
|
31
|
+
type CreateQqBot,
|
|
32
|
+
type QqBotTransport,
|
|
33
|
+
} from './ws.js';
|
|
34
|
+
|
|
35
|
+
const logger = getLogger('qq');
|
|
36
|
+
|
|
37
|
+
export type { CreateQqBot, QqBotTransport } from './ws.js';
|
|
38
|
+
export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
39
|
+
|
|
40
|
+
export interface QqEndpointOptions {
|
|
41
|
+
readonly id: CapabilityId;
|
|
42
|
+
readonly gateway: MessageGateway;
|
|
43
|
+
readonly config: ResolvedQqWebsocketConfig;
|
|
44
|
+
readonly createBot?: CreateQqBot;
|
|
55
45
|
}
|
|
56
46
|
|
|
57
|
-
export class
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
$platformUserId?: string;
|
|
65
|
-
|
|
66
|
-
get pluginLogger() {
|
|
67
|
-
return this.adapter.plugin.logger;
|
|
68
|
-
}
|
|
47
|
+
export class QqWebsocketEndpoint implements EndpointInstance {
|
|
48
|
+
readonly #options: QqEndpointOptions;
|
|
49
|
+
readonly #createBot: CreateQqBot;
|
|
50
|
+
#bot: QqBotTransport | null = null;
|
|
51
|
+
#open = false;
|
|
52
|
+
#started = false;
|
|
53
|
+
#unregisterAgent?: () => void;
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
constructor(options: QqEndpointOptions) {
|
|
56
|
+
this.#options = options;
|
|
57
|
+
this.#createBot = options.createBot ?? defaultCreateBot;
|
|
72
58
|
}
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
60
|
+
async start(): Promise<void> {
|
|
61
|
+
if (this.#started) return;
|
|
62
|
+
this.#started = true;
|
|
63
|
+
try {
|
|
64
|
+
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
|
|
65
|
+
this.#bot = this.#createBot(this.#options.config);
|
|
66
|
+
this.#bindBot(this.#bot);
|
|
67
|
+
await this.#bot.start();
|
|
68
|
+
logger.info(formatCompact({
|
|
69
|
+
op: 'connect',
|
|
70
|
+
endpoint: this.#options.config.name,
|
|
71
|
+
mode: 'websocket',
|
|
72
|
+
appid: this.#options.config.appid,
|
|
73
|
+
}));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await this.stop();
|
|
76
|
+
logger.error('Failed to connect QQ websocket:', error);
|
|
77
|
+
throw error;
|
|
83
78
|
}
|
|
84
|
-
super(config);
|
|
85
|
-
this.$config = config;
|
|
86
|
-
this.attachSdkVersionHeader();
|
|
87
|
-
applyCustomAuthEndpoints(this, config, this.pluginLogger);
|
|
88
79
|
}
|
|
89
80
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.request.interceptors.request.use((reqConfig) => {
|
|
93
|
-
reqConfig.headers[SDK_VERSION_HEADER] = SDK_VERSION;
|
|
94
|
-
return reqConfig;
|
|
95
|
-
});
|
|
81
|
+
open(): void {
|
|
82
|
+
this.#open = true;
|
|
96
83
|
}
|
|
97
84
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.pluginLogger.debug({
|
|
101
|
-
op: 'inbound_ws',
|
|
102
|
-
event,
|
|
103
|
-
group: String(wsRes.d?.group_openid ?? wsRes.d?.group_id ?? '?'),
|
|
104
|
-
});
|
|
105
|
-
if (event === 'GROUP_AT_MESSAGE_CREATE' || event === 'GROUP_MESSAGE_CREATE') {
|
|
106
|
-
this.pluginLogger.info(
|
|
107
|
-
`qq inbound ws: ${event} group=${String(wsRes.d?.group_openid ?? wsRes.d?.group_id ?? '?')}`,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
normalizeQqInboundWsPayload(event, wsRes);
|
|
111
|
-
super.dispatchEvent(event, wsRes);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private guildMemberCache = new Map<string, { at: number; role?: string; permissions: string[] }>();
|
|
115
|
-
private guildOwnerCache = new Map<string, { at: number; ownerId?: string }>();
|
|
116
|
-
|
|
117
|
-
private async enrichGuildSender(
|
|
118
|
-
message: ReturnType<QQEndpoint<ReceiverMode>["$formatMessage"]>,
|
|
119
|
-
guildId: string,
|
|
120
|
-
userId: string,
|
|
121
|
-
): Promise<void> {
|
|
122
|
-
const key = `${guildId}:${userId}`;
|
|
123
|
-
const now = Date.now();
|
|
124
|
-
const cached = this.guildMemberCache.get(key);
|
|
125
|
-
if (cached && now - cached.at < 60_000) {
|
|
126
|
-
message.$sender.role = cached.role;
|
|
127
|
-
message.$sender.permissions = cached.permissions;
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
let ownerId: string | undefined;
|
|
132
|
-
const ownerCached = this.guildOwnerCache.get(guildId);
|
|
133
|
-
if (ownerCached && now - ownerCached.at < 300_000) {
|
|
134
|
-
ownerId = ownerCached.ownerId;
|
|
135
|
-
} else {
|
|
136
|
-
const guild = await this.getGuildInfo(guildId);
|
|
137
|
-
ownerId = guild?.owner_id?.toString?.() ?? guild?.owner?.id?.toString?.();
|
|
138
|
-
this.guildOwnerCache.set(guildId, { at: now, ownerId });
|
|
139
|
-
}
|
|
140
|
-
const member = await this.getGuildMember(guildId, userId);
|
|
141
|
-
const roleIds = Array.isArray(member?.roles)
|
|
142
|
-
? member.roles.map((r: unknown) =>
|
|
143
|
-
typeof r === "string" ? r : (r as { id?: string })?.id ?? String(r),
|
|
144
|
-
)
|
|
145
|
-
: [];
|
|
146
|
-
const isOwner = ownerId === userId;
|
|
147
|
-
const isAdmin = !isOwner && roleIds.length > 0;
|
|
148
|
-
const normalized = normalizeQqGuildSenderForPermit({ roles: roleIds, isOwner, isAdmin });
|
|
149
|
-
const entry = {
|
|
150
|
-
at: now,
|
|
151
|
-
role: normalized.role,
|
|
152
|
-
permissions: normalized.permissions ?? [],
|
|
153
|
-
};
|
|
154
|
-
this.guildMemberCache.set(key, entry);
|
|
155
|
-
message.$sender.role = entry.role;
|
|
156
|
-
message.$sender.permissions = entry.permissions;
|
|
157
|
-
} catch {
|
|
158
|
-
// 保守拒绝
|
|
159
|
-
}
|
|
85
|
+
close(): void {
|
|
86
|
+
this.#open = false;
|
|
160
87
|
}
|
|
161
88
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
89
|
+
async stop(): Promise<void> {
|
|
90
|
+
this.#open = false;
|
|
91
|
+
this.#unregisterAgent?.();
|
|
92
|
+
this.#unregisterAgent = undefined;
|
|
93
|
+
if (this.#bot) {
|
|
94
|
+
try {
|
|
95
|
+
this.#bot.removeAllListeners();
|
|
96
|
+
await this.#bot.stop();
|
|
97
|
+
} catch {
|
|
98
|
+
/* ignore */
|
|
167
99
|
}
|
|
168
|
-
this
|
|
169
|
-
this.pluginLogger.debug(
|
|
170
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
171
|
-
);
|
|
172
|
-
} catch (err) {
|
|
173
|
-
this.pluginLogger.warn(
|
|
174
|
-
`qq format inbound failed (${msg.message_type}): ${err instanceof Error ? err.message : String(err)}`,
|
|
175
|
-
);
|
|
100
|
+
this.#bot = null;
|
|
176
101
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
`qq format action failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
203
|
-
);
|
|
102
|
+
this.#started = false;
|
|
103
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
107
|
+
const body = formatOutboundText(payload);
|
|
108
|
+
const parsed = parseSendTarget(target);
|
|
109
|
+
const bot = this.#requireBot();
|
|
110
|
+
let result: unknown;
|
|
111
|
+
switch (parsed.kind) {
|
|
112
|
+
case 'private':
|
|
113
|
+
result = await bot.sendPrivateMessage(parsed.id, body);
|
|
114
|
+
break;
|
|
115
|
+
case 'group':
|
|
116
|
+
result = await bot.sendGroupMessage(parsed.id, body);
|
|
117
|
+
break;
|
|
118
|
+
case 'channel':
|
|
119
|
+
result = await bot.sendGuildMessage(parsed.id, body);
|
|
120
|
+
break;
|
|
121
|
+
case 'direct':
|
|
122
|
+
if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
|
|
123
|
+
result = await bot.sendDirectMessage(parsed.id, body);
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
|
|
204
127
|
}
|
|
128
|
+
const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
|
|
129
|
+
logger.debug(formatCompact({
|
|
130
|
+
op: 'qq_send',
|
|
131
|
+
endpoint: this.#options.config.name,
|
|
132
|
+
target,
|
|
133
|
+
messageId,
|
|
134
|
+
}));
|
|
135
|
+
return messageId;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Test / internal: admit a message when open. */
|
|
139
|
+
admit(msg: QqInboundMessage): void {
|
|
140
|
+
if (!this.#open) return;
|
|
141
|
+
const target = formatInboundTarget(msg);
|
|
142
|
+
void this.#options.gateway.receive({
|
|
143
|
+
adapter: this.#options.id,
|
|
144
|
+
target,
|
|
145
|
+
content: formatInboundContent(msg),
|
|
146
|
+
sender: senderDisplayName(msg),
|
|
147
|
+
id: msg.id,
|
|
148
|
+
metadata: Object.freeze({
|
|
149
|
+
endpoint: this.#options.config.name,
|
|
150
|
+
channelKind: msg.channelKind,
|
|
151
|
+
userId: msg.authorId,
|
|
152
|
+
guildId: msg.guildId,
|
|
153
|
+
roles: msg.authorRoles,
|
|
154
|
+
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
155
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
156
|
+
}),
|
|
157
|
+
}).catch((err) => {
|
|
158
|
+
logger.warn(formatCompact({
|
|
159
|
+
op: 'qq_gateway_receive_failed',
|
|
160
|
+
target,
|
|
161
|
+
error: err instanceof Error ? err.message : String(err),
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
205
164
|
}
|
|
206
165
|
|
|
207
|
-
|
|
208
|
-
this.
|
|
209
|
-
`qq notice ${event}: group=${payload.group_id ?? '?'} operator=${payload.operator_id ?? '?'}`,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async $connect(): Promise<void> {
|
|
214
|
-
this.on("message.group", this.handleQQMessage.bind(this));
|
|
215
|
-
this.on("message.guild", this.handleQQMessage.bind(this));
|
|
216
|
-
this.on("message.private", this.handleQQMessage.bind(this));
|
|
217
|
-
this.on("notice.friend.action", this.handleQQAction.bind(this));
|
|
218
|
-
this.on("notice.group.action", this.handleQQAction.bind(this));
|
|
219
|
-
this.on("notice.guild.action", this.handleQQAction.bind(this));
|
|
220
|
-
this.on("notice.group.increase", (e) => this.handleGroupNotice('group.add_robot', e));
|
|
221
|
-
this.on("notice.group.decrease", (e) => this.handleGroupNotice('group.del_robot', e));
|
|
222
|
-
this.on("notice.group.receive_open", (e) => this.handleGroupNotice('group.msg_receive_open', e));
|
|
223
|
-
this.on("notice.group.receive_close", (e) => this.handleGroupNotice('group.msg_receive_close', e));
|
|
224
|
-
await this.start();
|
|
225
|
-
this.mountWebhookReceiver();
|
|
226
|
-
this.$connected = true;
|
|
227
|
-
try {
|
|
228
|
-
const self = await this.getSelfInfo();
|
|
229
|
-
const uid = (self as { id?: string; user_id?: string; username?: string })?.id
|
|
230
|
-
?? (self as { user_id?: string })?.user_id;
|
|
231
|
-
if (uid) this.$platformUserId = String(uid);
|
|
232
|
-
} catch (err) {
|
|
233
|
-
this.pluginLogger.debug(
|
|
234
|
-
`${this.$config.name} getSelfInfo failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
166
|
+
getGuilds() {
|
|
167
|
+
return this.#requireBot().getGuilds();
|
|
237
168
|
}
|
|
238
169
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(this as unknown as import('node:events').EventEmitter).removeAllListeners();
|
|
242
|
-
await this.stop();
|
|
243
|
-
this.$connected = false;
|
|
170
|
+
getChannels(guildId: string) {
|
|
171
|
+
return this.#requireBot().getChannels(guildId);
|
|
244
172
|
}
|
|
245
173
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
174
|
+
getChannelInfo(channelId: string) {
|
|
175
|
+
return this.#requireBot().getChannelInfo(channelId);
|
|
249
176
|
}
|
|
250
177
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
*/
|
|
254
|
-
private mountWebhookReceiver(): void {
|
|
255
|
-
const mode = this.$config.mode;
|
|
256
|
-
if (mode === ReceiverMode.MIDDLEWARE) {
|
|
257
|
-
const router = this.adapter.getRouter();
|
|
258
|
-
if (!router) {
|
|
259
|
-
throw new Error("QQ mode=middleware 需要启用 @zhin.js/host-router 插件");
|
|
260
|
-
}
|
|
261
|
-
const webhookPath = this.resolveWebhookPath();
|
|
262
|
-
const mw = this.middleware as (
|
|
263
|
-
ctx: RouterContext,
|
|
264
|
-
next: () => Promise<void>,
|
|
265
|
-
) => Promise<unknown>;
|
|
266
|
-
router.post(webhookPath, async (ctx: RouterContext) => {
|
|
267
|
-
this.pluginLogger.debug(ctx.body);
|
|
268
|
-
await mw(ctx, async () => {});
|
|
269
|
-
});
|
|
270
|
-
this.pluginLogger.info(formatCompact({
|
|
271
|
-
op: "webhook",
|
|
272
|
-
mode: "middleware",
|
|
273
|
-
path: webhookPath,
|
|
274
|
-
url: `POST ${webhookPath}`,
|
|
275
|
-
note: "无 /api 前缀;Host API 才走 /api/*",
|
|
276
|
-
}));
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (mode === ReceiverMode.WEBHOOK) {
|
|
280
|
-
const cfg = this.$config as QQEndpointConfig<ReceiverMode.WEBHOOK>;
|
|
281
|
-
this.pluginLogger.info(formatCompact({
|
|
282
|
-
op: "webhook",
|
|
283
|
-
mode: "standalone",
|
|
284
|
-
port: cfg.port,
|
|
285
|
-
path: cfg.path,
|
|
286
|
-
url: `http://127.0.0.1:${cfg.port}${cfg.path}`,
|
|
287
|
-
}));
|
|
288
|
-
}
|
|
178
|
+
getGuildMember(guildId: string, userId: string) {
|
|
179
|
+
return this.#requireBot().getGuildMember(guildId, userId);
|
|
289
180
|
}
|
|
290
181
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
typeof item === "string" ? { type: "text", data: { text: item } } : item as MessageElement,
|
|
294
|
-
);
|
|
295
|
-
return fromCanonicalSegments(toCanonicalSegments(items as never));
|
|
182
|
+
getGuildRoles(guildId: string) {
|
|
183
|
+
return this.#requireBot().getGuildRoles(guildId);
|
|
296
184
|
}
|
|
297
185
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
author?: { member_openid?: string; user_openid?: string; id?: string; username?: string };
|
|
302
|
-
mentions?: Array<{ id?: string; member_openid?: string; username?: string; nickname?: string }>;
|
|
303
|
-
__zhin_group_at?: boolean;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
if (msg.message_type === "group") {
|
|
307
|
-
if (!raw.group_id && raw.group_openid) {
|
|
308
|
-
raw.group_id = raw.group_openid;
|
|
309
|
-
}
|
|
310
|
-
if (!msg.user_id && raw.author) {
|
|
311
|
-
const uid = raw.author.member_openid ?? raw.author.user_openid ?? raw.author.id;
|
|
312
|
-
if (uid) msg.user_id = String(uid);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
186
|
+
createGuildRole(guildId: string, name: string, color?: number) {
|
|
187
|
+
return this.#requireBot().createGuildRole(guildId, name, color);
|
|
188
|
+
}
|
|
315
189
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (msg.sub_type === "direct") target_id = `direct:${msg.guild_id}`;
|
|
320
|
-
|
|
321
|
-
let content = msg.message;
|
|
322
|
-
if (msg.message_type === "group" && Array.isArray(content)) {
|
|
323
|
-
const endpointAtIds = [this.$platformUserId, this.$config.appid]
|
|
324
|
-
.filter((id): id is string => Boolean(id))
|
|
325
|
-
.map(String);
|
|
326
|
-
content = normalizeGroupAtPrefix(content, endpointAtIds, raw.__zhin_group_at === true);
|
|
327
|
-
}
|
|
190
|
+
addMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
191
|
+
return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
|
|
192
|
+
}
|
|
328
193
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
this.adapter,
|
|
333
|
-
raw.mentions,
|
|
334
|
-
);
|
|
335
|
-
}
|
|
194
|
+
removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
195
|
+
return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
|
|
196
|
+
}
|
|
336
197
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
$sender: {
|
|
342
|
-
id: msg.sender.user_id?.toString(),
|
|
343
|
-
name: msg.sender.user_name?.toString(),
|
|
344
|
-
},
|
|
345
|
-
$channel: {
|
|
346
|
-
id: target_id,
|
|
347
|
-
type: msg.message_type === "guild" ? "channel" : msg.message_type,
|
|
348
|
-
},
|
|
349
|
-
$content: content,
|
|
350
|
-
$raw: msg.raw_message,
|
|
351
|
-
$timestamp: Date.now(),
|
|
352
|
-
$recall: async () => {
|
|
353
|
-
await this.$recallMessage(result.$id);
|
|
354
|
-
},
|
|
355
|
-
$reply: async (content: SendContent, quote: boolean | string = true): Promise<string> => {
|
|
356
|
-
if (!Array.isArray(content)) content = [content];
|
|
357
|
-
if (quote) content.unshift({ type: "reply", data: { id: typeof quote === "boolean" ? result.$id : quote } });
|
|
358
|
-
return await this.adapter.sendMessage({
|
|
359
|
-
...result.$channel,
|
|
360
|
-
context: "qq",
|
|
361
|
-
endpoint: this.$config.name,
|
|
362
|
-
content,
|
|
363
|
-
});
|
|
364
|
-
},
|
|
198
|
+
#bindBot(bot: QqBotTransport): void {
|
|
199
|
+
bindQqBotInboundEvents(bot, (raw) => {
|
|
200
|
+
const msg = normalizeQqMessage(raw);
|
|
201
|
+
if (msg) this.admit(msg);
|
|
365
202
|
});
|
|
366
|
-
return result;
|
|
367
203
|
}
|
|
368
204
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const mediaNorm = normalizeOutboundMedia(canonicalized);
|
|
373
|
-
const markdownMode = this.$config.outboundMarkdown;
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
(options.type === "group" || options.type === "private")
|
|
377
|
-
&& !options.id.startsWith("direct:")
|
|
378
|
-
&& shouldSendTextImageMixedMedia(mediaNorm, markdownMode)
|
|
379
|
-
) {
|
|
380
|
-
return await this.$sendTextImageMixedMedia(options, mediaNorm);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const content = normalizeOutboundMarkdown(mediaNorm, markdownMode);
|
|
384
|
-
switch (options.type) {
|
|
385
|
-
case "private": {
|
|
386
|
-
if (options.id.startsWith("direct:")) {
|
|
387
|
-
const id = options.id.replace("direct:", "");
|
|
388
|
-
const result = await this.sendDirectMessage(id, content);
|
|
389
|
-
this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
|
|
390
|
-
return `direct-${options.id}:${resolveOutboundMessageId(result)}`;
|
|
391
|
-
} else {
|
|
392
|
-
const result = await this.sendPrivateMessage(options.id, content);
|
|
393
|
-
this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
|
|
394
|
-
return `private-${options.id}:${resolveOutboundMessageId(result)}`;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
case "group": {
|
|
398
|
-
const result = await this.sendGroupMessage(options.id, content);
|
|
399
|
-
this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
|
|
400
|
-
return `group-${options.id}:${resolveOutboundMessageId(result)}`;
|
|
401
|
-
}
|
|
402
|
-
case "channel": {
|
|
403
|
-
const result = await this.sendGuildMessage(options.id, content);
|
|
404
|
-
this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(content)}`);
|
|
405
|
-
return `channel-${options.id}:${resolveOutboundMessageId(result)}`;
|
|
406
|
-
}
|
|
407
|
-
default:
|
|
408
|
-
throw new Error(`unsupported channel type ${options.type}`);
|
|
409
|
-
}
|
|
205
|
+
#requireBot(): QqBotTransport {
|
|
206
|
+
if (!this.#bot) throw new Error('QQ bot not connected');
|
|
207
|
+
return this.#bot;
|
|
410
208
|
}
|
|
209
|
+
}
|
|
411
210
|
|
|
412
|
-
|
|
413
|
-
private async $sendTextImageMixedMedia(
|
|
414
|
-
options: SendOptions,
|
|
415
|
-
content: SendContent,
|
|
416
|
-
): Promise<string> {
|
|
417
|
-
const endpointPath = this.resolveV2EndpointPath(options);
|
|
418
|
-
const { body } = splitReplyPrefix(asOutboundSegments(content));
|
|
419
|
-
const image = firstImageSegment(body);
|
|
420
|
-
if (!image) {
|
|
421
|
-
throw new Error("QQ 图文混排:未找到图片段");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const uploadPayload = buildQqImageUploadPayload(image);
|
|
425
|
-
const { data: uploadResult } = await this.request.post(
|
|
426
|
-
`${endpointPath}/files`,
|
|
427
|
-
{ ...uploadPayload, srv_send_msg: false },
|
|
428
|
-
) as { data: { file_info?: string } };
|
|
211
|
+
type ParsedSendTarget = ReturnType<typeof parseSendTarget>;
|
|
429
212
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
213
|
+
export interface QqHttpEndpointOptions {
|
|
214
|
+
readonly id: CapabilityId;
|
|
215
|
+
readonly gateway: MessageGateway;
|
|
216
|
+
readonly http: HttpHost;
|
|
217
|
+
readonly config: ResolvedQqHttpConfig;
|
|
218
|
+
readonly createBot?: CreateQqHttpBot;
|
|
219
|
+
}
|
|
433
220
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return `${idPrefix}-${options.id}:${resolveOutboundMessageId(result)}`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
private resolveV2EndpointPath(options: SendOptions): string {
|
|
447
|
-
switch (options.type) {
|
|
448
|
-
case "group":
|
|
449
|
-
return `/v2/groups/${options.id}`;
|
|
450
|
-
case "private":
|
|
451
|
-
return `/v2/users/${options.id}`;
|
|
452
|
-
default:
|
|
453
|
-
throw new Error(`QQ 图文混排不支持 ${options.type}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
221
|
+
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
222
|
+
export class QqHttpEndpoint implements EndpointInstance {
|
|
223
|
+
readonly #options: QqHttpEndpointOptions;
|
|
224
|
+
readonly #createBot: CreateQqHttpBot;
|
|
225
|
+
#bot: QqHttpBotTransport | null = null;
|
|
226
|
+
#routeReleases: ReturnType<typeof registerQqWebhookRoutes> = [];
|
|
227
|
+
#open = false;
|
|
228
|
+
#started = false;
|
|
229
|
+
#unregisterAgent?: () => void;
|
|
456
230
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (!match) return;
|
|
461
|
-
const [, target_type, target_id, message_id] = match;
|
|
462
|
-
if (target_type === "private") await this.recallPrivateMessage(target_id, message_id);
|
|
463
|
-
if (target_type === "group") await this.recallGroupMessage(target_id, message_id);
|
|
464
|
-
if (target_type === "channel") await this.recallGuildMessage(target_id, message_id);
|
|
465
|
-
if (target_type === "direct") await this.recallDirectMessage(target_id, message_id);
|
|
231
|
+
constructor(options: QqHttpEndpointOptions) {
|
|
232
|
+
this.#options = options;
|
|
233
|
+
this.#createBot = options.createBot ?? defaultCreateHttpBot;
|
|
466
234
|
}
|
|
467
235
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
* 获取频道列表
|
|
472
|
-
*/
|
|
473
|
-
async getGuilds(): Promise<any[]> {
|
|
236
|
+
async start(): Promise<void> {
|
|
237
|
+
if (this.#started) return;
|
|
238
|
+
this.#started = true;
|
|
474
239
|
try {
|
|
475
|
-
|
|
240
|
+
this.#unregisterAgent = registerQqAgentEndpoint(
|
|
241
|
+
this.#options.config.name,
|
|
242
|
+
this as unknown as QqWebsocketEndpoint,
|
|
243
|
+
);
|
|
244
|
+
this.#setupRoutes();
|
|
245
|
+
this.#bot = this.#createBot(this.#options.config);
|
|
246
|
+
this.#bindBot(this.#bot);
|
|
247
|
+
await this.#bot.start();
|
|
248
|
+
logger.info(formatCompact({
|
|
249
|
+
op: 'connect',
|
|
250
|
+
endpoint: this.#options.config.name,
|
|
251
|
+
mode: this.#options.config.mode,
|
|
252
|
+
path: this.#options.config.webhookPath,
|
|
253
|
+
}));
|
|
476
254
|
} catch (error) {
|
|
477
|
-
this.
|
|
255
|
+
await this.stop();
|
|
256
|
+
logger.error('Failed to connect QQ HTTP receiver:', error);
|
|
478
257
|
throw error;
|
|
479
258
|
}
|
|
480
259
|
}
|
|
481
260
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
* @param guildId 频道 ID
|
|
485
|
-
*/
|
|
486
|
-
async getGuildInfo(guildId: string): Promise<any> {
|
|
487
|
-
try {
|
|
488
|
-
return await this.guildService.getInfo(guildId);
|
|
489
|
-
} catch (error) {
|
|
490
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取频道详情失败:`, error);
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
261
|
+
open(): void {
|
|
262
|
+
this.#open = true;
|
|
493
263
|
}
|
|
494
264
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
* @param guildId 频道 ID
|
|
498
|
-
*/
|
|
499
|
-
async getChannels(guildId: string): Promise<any[]> {
|
|
500
|
-
try {
|
|
501
|
-
return await this.channelService.getList(guildId);
|
|
502
|
-
} catch (error) {
|
|
503
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取子频道列表失败:`, error);
|
|
504
|
-
throw error;
|
|
505
|
-
}
|
|
265
|
+
close(): void {
|
|
266
|
+
this.#open = false;
|
|
506
267
|
}
|
|
507
268
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
269
|
+
async stop(): Promise<void> {
|
|
270
|
+
this.#open = false;
|
|
271
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
272
|
+
this.#unregisterAgent?.();
|
|
273
|
+
this.#unregisterAgent = undefined;
|
|
274
|
+
if (this.#bot) {
|
|
275
|
+
try {
|
|
276
|
+
this.#bot.removeAllListeners();
|
|
277
|
+
await this.#bot.stop();
|
|
278
|
+
} catch {
|
|
279
|
+
/* ignore */
|
|
280
|
+
}
|
|
281
|
+
this.#bot = null;
|
|
282
|
+
}
|
|
283
|
+
this.#started = false;
|
|
284
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
288
|
+
const body = formatOutboundText(payload);
|
|
289
|
+
const parsed = parseSendTarget(target);
|
|
290
|
+
const bot = this.#requireBot();
|
|
291
|
+
let result: unknown;
|
|
292
|
+
switch (parsed.kind) {
|
|
293
|
+
case 'private':
|
|
294
|
+
result = await bot.sendPrivateMessage(parsed.id, body);
|
|
295
|
+
break;
|
|
296
|
+
case 'group':
|
|
297
|
+
result = await bot.sendGroupMessage(parsed.id, body);
|
|
298
|
+
break;
|
|
299
|
+
case 'channel':
|
|
300
|
+
result = await bot.sendGuildMessage(parsed.id, body);
|
|
301
|
+
break;
|
|
302
|
+
case 'direct':
|
|
303
|
+
if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
|
|
304
|
+
result = await bot.sendDirectMessage(parsed.id, body);
|
|
305
|
+
break;
|
|
306
|
+
default:
|
|
307
|
+
throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
|
|
518
308
|
}
|
|
309
|
+
return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
admit(msg: QqInboundMessage): void {
|
|
313
|
+
if (!this.#open) return;
|
|
314
|
+
const target = formatInboundTarget(msg);
|
|
315
|
+
void this.#options.gateway.receive({
|
|
316
|
+
adapter: this.#options.id,
|
|
317
|
+
target,
|
|
318
|
+
content: formatInboundContent(msg),
|
|
319
|
+
sender: senderDisplayName(msg),
|
|
320
|
+
id: msg.id,
|
|
321
|
+
metadata: Object.freeze({
|
|
322
|
+
endpoint: this.#options.config.name,
|
|
323
|
+
channelKind: msg.channelKind,
|
|
324
|
+
userId: msg.authorId,
|
|
325
|
+
guildId: msg.guildId,
|
|
326
|
+
roles: msg.authorRoles,
|
|
327
|
+
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
328
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
329
|
+
}),
|
|
330
|
+
}).catch((err) => {
|
|
331
|
+
logger.warn(formatCompact({
|
|
332
|
+
op: 'qq_gateway_receive_failed',
|
|
333
|
+
target,
|
|
334
|
+
error: err instanceof Error ? err.message : String(err),
|
|
335
|
+
}));
|
|
336
|
+
});
|
|
519
337
|
}
|
|
520
338
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
* @param guildId 频道 ID
|
|
524
|
-
*/
|
|
525
|
-
async getGuildMembers(guildId: string): Promise<any[]> {
|
|
526
|
-
try {
|
|
527
|
-
return await this.memberService.getGuildMemberList(guildId);
|
|
528
|
-
} catch (error) {
|
|
529
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取频道成员列表失败:`, error);
|
|
530
|
-
throw error;
|
|
531
|
-
}
|
|
339
|
+
getGuilds() {
|
|
340
|
+
return this.#requireBot().getGuilds();
|
|
532
341
|
}
|
|
533
342
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
* @param guildId 频道 ID
|
|
537
|
-
* @param userId 用户 ID
|
|
538
|
-
*/
|
|
539
|
-
async getGuildMember(guildId: string, userId: string): Promise<any> {
|
|
540
|
-
try {
|
|
541
|
-
return await this.memberService.getGuildMemberInfo(guildId, userId);
|
|
542
|
-
} catch (error) {
|
|
543
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取成员详情失败:`, error);
|
|
544
|
-
throw error;
|
|
545
|
-
}
|
|
343
|
+
getChannels(guildId: string) {
|
|
344
|
+
return this.#requireBot().getChannels(guildId);
|
|
546
345
|
}
|
|
547
346
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
* @param guildId 频道 ID
|
|
551
|
-
* @param userId 用户 ID
|
|
552
|
-
* @param addBlacklist 是否加入黑名单
|
|
553
|
-
* @param deleteHistoryMsg 删除历史消息天数 (-1不删除,0全部删除,3/7/15/30)
|
|
554
|
-
*/
|
|
555
|
-
async removeGuildMember(guildId: string, userId: string, addBlacklist?: boolean, deleteHistoryMsg?: -1 | 0 | 3 | 7 | 15 | 30): Promise<boolean> {
|
|
556
|
-
try {
|
|
557
|
-
await this.memberService.kickMember(guildId, userId, deleteHistoryMsg, addBlacklist);
|
|
558
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 踢出成员 ${userId}(频道 ${guildId})`);
|
|
559
|
-
return true;
|
|
560
|
-
} catch (error) {
|
|
561
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 踢出成员失败:`, error);
|
|
562
|
-
throw error;
|
|
563
|
-
}
|
|
347
|
+
getChannelInfo(channelId: string) {
|
|
348
|
+
return this.#requireBot().getChannelInfo(channelId);
|
|
564
349
|
}
|
|
565
350
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
* @param guildId 频道 ID
|
|
569
|
-
*/
|
|
570
|
-
async getGuildRoles(guildId: string): Promise<any[]> {
|
|
571
|
-
try {
|
|
572
|
-
return await this.guildService.getRoles(guildId);
|
|
573
|
-
} catch (error) {
|
|
574
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 获取角色列表失败:`, error);
|
|
575
|
-
throw error;
|
|
576
|
-
}
|
|
351
|
+
getGuildMember(guildId: string, userId: string) {
|
|
352
|
+
return this.#requireBot().getGuildMember(guildId, userId);
|
|
577
353
|
}
|
|
578
354
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
* @param guildId 频道 ID
|
|
582
|
-
* @param name 角色名
|
|
583
|
-
* @param color 颜色
|
|
584
|
-
* @param hoist 是否在成员列表中单独展示 (0 或 1)
|
|
585
|
-
*/
|
|
586
|
-
async createGuildRole(guildId: string, name: string, color?: number, hoist?: 0 | 1): Promise<any> {
|
|
587
|
-
try {
|
|
588
|
-
const result = await this.guildService.createRole(guildId, { name, color: color || 0, hoist: hoist ?? 0 });
|
|
589
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 创建角色 "${name}"(频道 ${guildId})`);
|
|
590
|
-
return result;
|
|
591
|
-
} catch (error) {
|
|
592
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 创建角色失败:`, error);
|
|
593
|
-
throw error;
|
|
594
|
-
}
|
|
355
|
+
getGuildRoles(guildId: string) {
|
|
356
|
+
return this.#requireBot().getGuildRoles(guildId);
|
|
595
357
|
}
|
|
596
358
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
* @param guildId 频道 ID
|
|
600
|
-
* @param channelId 子频道 ID
|
|
601
|
-
* @param userId 用户 ID
|
|
602
|
-
* @param roleId 角色 ID
|
|
603
|
-
*/
|
|
604
|
-
async addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean> {
|
|
605
|
-
try {
|
|
606
|
-
await this.memberService.addMemberRole(guildId, channelId, userId, roleId);
|
|
607
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 给成员 ${userId} 添加角色 ${roleId}(频道 ${guildId})`);
|
|
608
|
-
return true;
|
|
609
|
-
} catch (error) {
|
|
610
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 添加角色失败:`, error);
|
|
611
|
-
throw error;
|
|
612
|
-
}
|
|
359
|
+
createGuildRole(guildId: string, name: string, color?: number) {
|
|
360
|
+
return this.#requireBot().createGuildRole(guildId, name, color);
|
|
613
361
|
}
|
|
614
362
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
* @param guildId 频道 ID
|
|
618
|
-
* @param channelId 子频道 ID
|
|
619
|
-
* @param userId 用户 ID
|
|
620
|
-
* @param roleId 角色 ID
|
|
621
|
-
*/
|
|
622
|
-
async removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean> {
|
|
623
|
-
try {
|
|
624
|
-
await this.memberService.removeMemberRole(guildId, channelId, userId, roleId);
|
|
625
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 移除成员 ${userId} 的角色 ${roleId}(频道 ${guildId})`);
|
|
626
|
-
return true;
|
|
627
|
-
} catch (error) {
|
|
628
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 移除角色失败:`, error);
|
|
629
|
-
throw error;
|
|
630
|
-
}
|
|
363
|
+
addMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
364
|
+
return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
|
|
631
365
|
}
|
|
632
366
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
* @param guildId 频道 ID
|
|
636
|
-
* @param userIds 用户 ID 列表
|
|
637
|
-
* @param muteSeconds 禁言时长(秒),0 表示解除禁言
|
|
638
|
-
*/
|
|
639
|
-
async muteMembers(guildId: string, userIds: string[], muteSeconds: number): Promise<boolean> {
|
|
640
|
-
try {
|
|
641
|
-
if (muteSeconds > 0) {
|
|
642
|
-
await this.memberService.muteMembers(guildId, userIds, muteSeconds);
|
|
643
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 禁言成员 ${userIds.join(',')} ${muteSeconds}秒(频道 ${guildId})`);
|
|
644
|
-
} else {
|
|
645
|
-
await this.memberService.unmuteMembers(guildId, userIds);
|
|
646
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 解除成员 ${userIds.join(',')} 禁言(频道 ${guildId})`);
|
|
647
|
-
}
|
|
648
|
-
return true;
|
|
649
|
-
} catch (error) {
|
|
650
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 禁言操作失败:`, error);
|
|
651
|
-
throw error;
|
|
652
|
-
}
|
|
367
|
+
removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
368
|
+
return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
|
|
653
369
|
}
|
|
654
370
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
throw error;
|
|
673
|
-
}
|
|
371
|
+
#setupRoutes(): void {
|
|
372
|
+
this.#routeReleases.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
373
|
+
config: this.#options.config,
|
|
374
|
+
getBot: () => this.#bot,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#bindBot(bot: QqBotTransport): void {
|
|
379
|
+
bindQqBotInboundEvents(bot, (raw) => {
|
|
380
|
+
const msg = normalizeQqMessage(raw);
|
|
381
|
+
if (msg) this.admit(msg);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#requireBot(): QqBotTransport {
|
|
386
|
+
if (!this.#bot) throw new Error('QQ bot not connected');
|
|
387
|
+
return this.#bot;
|
|
674
388
|
}
|
|
675
389
|
}
|