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