@zhin.js/adapter-qq 5.0.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +75 -409
- package/adapters/qq.ts +51 -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/commands/endpoint/add/[name:string=].ts +21 -0
- package/commands/endpoint/cancel.ts +15 -0
- package/commands/endpoint/list.ts +10 -0
- package/commands/endpoint/remove/[name:string].ts +15 -0
- package/lib/endpoint.d.ts +75 -0
- package/lib/endpoint.js +358 -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/qq-bind-api.d.ts +36 -0
- package/lib/{src/qq-bind-api.js → qq-bind-api.js} +0 -1
- package/lib/qq-bind-flow.d.ts +19 -0
- package/lib/{src/qq-bind-flow.js → qq-bind-flow.js} +0 -1
- package/lib/qq-bind-persist.d.ts +12 -0
- package/lib/{src/qq-bind-persist.js → qq-bind-persist.js} +4 -5
- package/lib/qq-endpoint-commands.d.ts +25 -0
- package/lib/qq-endpoint-commands.js +147 -0
- package/lib/qq-endpoint-config.d.ts +17 -0
- package/lib/qq-endpoint-config.js +110 -0
- package/lib/qq-runtime-state.d.ts +17 -0
- package/lib/qq-runtime-state.js +13 -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 +51 -40
- package/plugin.ts +16 -0
- package/schema.json +85 -0
- package/src/endpoint.ts +362 -596
- 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/qq-bind-persist.ts +4 -4
- package/src/qq-endpoint-commands.ts +174 -0
- package/src/qq-endpoint-config.ts +135 -0
- package/src/qq-runtime-state.ts +36 -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/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.map +0 -1
- package/lib/src/qq-bind-flow.js.map +0 -1
- 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-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,441 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QQ
|
|
2
|
+
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
+
import type {
|
|
5
|
+
EndpointChannel,
|
|
6
|
+
EndpointInstance,
|
|
7
|
+
EndpointManagement,
|
|
8
|
+
} from '@zhin.js/adapter';
|
|
9
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
10
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
11
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
12
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
13
|
+
import { registerQqAgentEndpoint } from './qq-agent-deps.js';
|
|
4
14
|
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";
|
|
15
|
+
formatInboundContent,
|
|
16
|
+
formatInboundTarget,
|
|
17
|
+
formatOutboundText,
|
|
18
|
+
parseSendTarget,
|
|
19
|
+
resolveOutboundMessageId,
|
|
20
|
+
senderDisplayName,
|
|
21
|
+
type QqInboundMessage,
|
|
22
|
+
type ResolvedQqHttpConfig,
|
|
23
|
+
type ResolvedQqWebsocketConfig,
|
|
24
|
+
} from './protocol.js';
|
|
24
25
|
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);
|
|
26
|
+
defaultCreateHttpBot,
|
|
27
|
+
registerQqWebhookRoutes,
|
|
28
|
+
type CreateQqHttpBot,
|
|
29
|
+
type QqHttpBotTransport,
|
|
30
|
+
} from './webhook.js';
|
|
31
|
+
import {
|
|
32
|
+
bindQqBotInboundEvents,
|
|
33
|
+
defaultCreateBot,
|
|
34
|
+
normalizeQqMessage,
|
|
35
|
+
type CreateQqBot,
|
|
36
|
+
type QqBotTransport,
|
|
37
|
+
} from './ws.js';
|
|
38
|
+
|
|
39
|
+
const logger = getLogger('qq');
|
|
40
|
+
|
|
41
|
+
export type { CreateQqBot, QqBotTransport } from './ws.js';
|
|
42
|
+
export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
43
|
+
|
|
44
|
+
export interface QqEndpointOptions {
|
|
45
|
+
readonly id: CapabilityId;
|
|
46
|
+
readonly gateway: MessageGateway;
|
|
47
|
+
readonly config: ResolvedQqWebsocketConfig;
|
|
48
|
+
readonly createBot?: CreateQqBot;
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
export class
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
export class QqWebsocketEndpoint implements EndpointInstance {
|
|
52
|
+
readonly #options: QqEndpointOptions;
|
|
53
|
+
readonly #createBot: CreateQqBot;
|
|
54
|
+
#bot: QqBotTransport | null = null;
|
|
55
|
+
#open = false;
|
|
56
|
+
#started = false;
|
|
57
|
+
#unregisterAgent?: () => void;
|
|
58
|
+
readonly management: EndpointManagement = createQqEndpointManagement(this);
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
constructor(options: QqEndpointOptions) {
|
|
61
|
+
this.#options = options;
|
|
62
|
+
this.#createBot = options.createBot ?? defaultCreateBot;
|
|
68
63
|
}
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
/** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
|
|
66
|
+
get name(): string {
|
|
67
|
+
return this.#options.config.name;
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
70
|
+
async start(): Promise<void> {
|
|
71
|
+
if (this.#started) return;
|
|
72
|
+
this.#started = true;
|
|
73
|
+
try {
|
|
74
|
+
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
|
|
75
|
+
this.#bot = this.#createBot(this.#options.config);
|
|
76
|
+
this.#bindBot(this.#bot);
|
|
77
|
+
await this.#bot.start();
|
|
78
|
+
logger.info(formatCompact({
|
|
79
|
+
op: 'connect',
|
|
80
|
+
endpoint: this.#options.config.name,
|
|
81
|
+
mode: 'websocket',
|
|
82
|
+
appid: this.#options.config.appid,
|
|
83
|
+
}));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
await this.stop();
|
|
86
|
+
logger.error('Failed to connect QQ websocket:', error);
|
|
87
|
+
throw error;
|
|
83
88
|
}
|
|
84
|
-
super(config);
|
|
85
|
-
this.$config = config;
|
|
86
|
-
this.attachSdkVersionHeader();
|
|
87
|
-
applyCustomAuthEndpoints(this, config, this.pluginLogger);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.request.interceptors.request.use((reqConfig) => {
|
|
93
|
-
reqConfig.headers[SDK_VERSION_HEADER] = SDK_VERSION;
|
|
94
|
-
return reqConfig;
|
|
95
|
-
});
|
|
91
|
+
open(): void {
|
|
92
|
+
this.#open = true;
|
|
96
93
|
}
|
|
97
94
|
|
|
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
|
-
}
|
|
95
|
+
close(): void {
|
|
96
|
+
this.#open = false;
|
|
160
97
|
}
|
|
161
98
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
99
|
+
async stop(): Promise<void> {
|
|
100
|
+
this.#open = false;
|
|
101
|
+
this.#unregisterAgent?.();
|
|
102
|
+
this.#unregisterAgent = undefined;
|
|
103
|
+
if (this.#bot) {
|
|
104
|
+
try {
|
|
105
|
+
this.#bot.removeAllListeners();
|
|
106
|
+
await this.#bot.stop();
|
|
107
|
+
} catch {
|
|
108
|
+
/* ignore */
|
|
167
109
|
}
|
|
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
|
-
);
|
|
110
|
+
this.#bot = null;
|
|
176
111
|
}
|
|
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
|
-
);
|
|
112
|
+
this.#started = false;
|
|
113
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
117
|
+
const body = formatOutboundText(payload);
|
|
118
|
+
const parsed = parseSendTarget(target);
|
|
119
|
+
const bot = this.#requireBot();
|
|
120
|
+
let result: unknown;
|
|
121
|
+
switch (parsed.kind) {
|
|
122
|
+
case 'private':
|
|
123
|
+
result = await bot.sendPrivateMessage(parsed.id, body);
|
|
124
|
+
break;
|
|
125
|
+
case 'group':
|
|
126
|
+
result = await bot.sendGroupMessage(parsed.id, body);
|
|
127
|
+
break;
|
|
128
|
+
case 'channel':
|
|
129
|
+
result = await bot.sendGuildMessage(parsed.id, body);
|
|
130
|
+
break;
|
|
131
|
+
case 'direct':
|
|
132
|
+
if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
|
|
133
|
+
result = await bot.sendDirectMessage(parsed.id, body);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
|
|
204
137
|
}
|
|
138
|
+
const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
|
|
139
|
+
logger.debug(formatCompact({
|
|
140
|
+
op: 'qq_send',
|
|
141
|
+
endpoint: this.#options.config.name,
|
|
142
|
+
target,
|
|
143
|
+
messageId,
|
|
144
|
+
}));
|
|
145
|
+
return messageId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Test / internal: admit a message when open. */
|
|
149
|
+
admit(msg: QqInboundMessage): void {
|
|
150
|
+
if (!this.#open) return;
|
|
151
|
+
const target = formatInboundTarget(msg);
|
|
152
|
+
void this.#options.gateway.receive({
|
|
153
|
+
adapter: this.#options.id,
|
|
154
|
+
target,
|
|
155
|
+
content: formatInboundContent(msg),
|
|
156
|
+
sender: senderDisplayName(msg),
|
|
157
|
+
id: msg.id,
|
|
158
|
+
metadata: Object.freeze({
|
|
159
|
+
endpoint: this.#options.config.name,
|
|
160
|
+
channelKind: msg.channelKind,
|
|
161
|
+
userId: msg.authorId,
|
|
162
|
+
guildId: msg.guildId,
|
|
163
|
+
roles: msg.authorRoles,
|
|
164
|
+
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
165
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
166
|
+
}),
|
|
167
|
+
}).catch((err) => {
|
|
168
|
+
logger.warn(formatCompact({
|
|
169
|
+
op: 'qq_gateway_receive_failed',
|
|
170
|
+
target,
|
|
171
|
+
error: err instanceof Error ? err.message : String(err),
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
205
174
|
}
|
|
206
175
|
|
|
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
|
-
}
|
|
176
|
+
getGuilds() {
|
|
177
|
+
return this.#requireBot().getGuilds();
|
|
237
178
|
}
|
|
238
179
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(this as unknown as import('node:events').EventEmitter).removeAllListeners();
|
|
242
|
-
await this.stop();
|
|
243
|
-
this.$connected = false;
|
|
180
|
+
getChannels(guildId: string) {
|
|
181
|
+
return this.#requireBot().getChannels(guildId);
|
|
244
182
|
}
|
|
245
183
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
184
|
+
getChannelInfo(channelId: string) {
|
|
185
|
+
return this.#requireBot().getChannelInfo(channelId);
|
|
249
186
|
}
|
|
250
187
|
|
|
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
|
-
}
|
|
188
|
+
getGuildMember(guildId: string, userId: string) {
|
|
189
|
+
return this.#requireBot().getGuildMember(guildId, userId);
|
|
289
190
|
}
|
|
290
191
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
typeof item === "string" ? { type: "text", data: { text: item } } : item as MessageElement,
|
|
294
|
-
);
|
|
295
|
-
return fromCanonicalSegments(toCanonicalSegments(items as never));
|
|
192
|
+
getGuildRoles(guildId: string) {
|
|
193
|
+
return this.#requireBot().getGuildRoles(guildId);
|
|
296
194
|
}
|
|
297
195
|
|
|
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
|
-
}
|
|
196
|
+
createGuildRole(guildId: string, name: string, color?: number) {
|
|
197
|
+
return this.#requireBot().createGuildRole(guildId, name, color);
|
|
198
|
+
}
|
|
315
199
|
|
|
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
|
-
}
|
|
200
|
+
addMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
201
|
+
return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
|
|
202
|
+
}
|
|
328
203
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
this.adapter,
|
|
333
|
-
raw.mentions,
|
|
334
|
-
);
|
|
335
|
-
}
|
|
204
|
+
removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
205
|
+
return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
|
|
206
|
+
}
|
|
336
207
|
|
|
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
|
-
},
|
|
208
|
+
#bindBot(bot: QqBotTransport): void {
|
|
209
|
+
bindQqBotInboundEvents(bot, (raw) => {
|
|
210
|
+
const msg = normalizeQqMessage(raw);
|
|
211
|
+
if (msg) this.admit(msg);
|
|
365
212
|
});
|
|
366
|
-
return result;
|
|
367
213
|
}
|
|
368
214
|
|
|
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
|
-
}
|
|
215
|
+
#requireBot(): QqBotTransport {
|
|
216
|
+
if (!this.#bot) throw new Error('QQ bot not connected');
|
|
217
|
+
return this.#bot;
|
|
410
218
|
}
|
|
219
|
+
}
|
|
411
220
|
|
|
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
|
-
}
|
|
221
|
+
type ParsedSendTarget = ReturnType<typeof parseSendTarget>;
|
|
423
222
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
223
|
+
export interface QqHttpEndpointOptions {
|
|
224
|
+
readonly id: CapabilityId;
|
|
225
|
+
readonly gateway: MessageGateway;
|
|
226
|
+
readonly http: HttpHost;
|
|
227
|
+
readonly config: ResolvedQqHttpConfig;
|
|
228
|
+
readonly createBot?: CreateQqHttpBot;
|
|
229
|
+
}
|
|
429
230
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
231
|
+
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
232
|
+
export class QqHttpEndpoint implements EndpointInstance {
|
|
233
|
+
readonly #options: QqHttpEndpointOptions;
|
|
234
|
+
readonly #createBot: CreateQqHttpBot;
|
|
235
|
+
#bot: QqHttpBotTransport | null = null;
|
|
236
|
+
#routeReleases: ReturnType<typeof registerQqWebhookRoutes> = [];
|
|
237
|
+
#open = false;
|
|
238
|
+
#started = false;
|
|
239
|
+
#unregisterAgent?: () => void;
|
|
240
|
+
readonly management: EndpointManagement = createQqEndpointManagement(this);
|
|
433
241
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
messagePayload,
|
|
438
|
-
);
|
|
439
|
-
this.pluginLogger.debug(
|
|
440
|
-
`${this.$config.name} send mixed-media ${options.type}(${options.id}):${segment.raw(content)}`,
|
|
441
|
-
);
|
|
442
|
-
const idPrefix = options.type === "group" ? "group" : "private";
|
|
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
|
-
}
|
|
242
|
+
constructor(options: QqHttpEndpointOptions) {
|
|
243
|
+
this.#options = options;
|
|
244
|
+
this.#createBot = options.createBot ?? defaultCreateHttpBot;
|
|
455
245
|
}
|
|
456
246
|
|
|
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);
|
|
247
|
+
/** Live endpoint 名(Console/AdapterIndex 展示用)。 */
|
|
248
|
+
get name(): string {
|
|
249
|
+
return this.#options.config.name;
|
|
466
250
|
}
|
|
467
251
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
* 获取频道列表
|
|
472
|
-
*/
|
|
473
|
-
async getGuilds(): Promise<any[]> {
|
|
252
|
+
async start(): Promise<void> {
|
|
253
|
+
if (this.#started) return;
|
|
254
|
+
this.#started = true;
|
|
474
255
|
try {
|
|
475
|
-
|
|
256
|
+
this.#unregisterAgent = registerQqAgentEndpoint(
|
|
257
|
+
this.#options.config.name,
|
|
258
|
+
this as unknown as QqWebsocketEndpoint,
|
|
259
|
+
);
|
|
260
|
+
this.#setupRoutes();
|
|
261
|
+
this.#bot = this.#createBot(this.#options.config);
|
|
262
|
+
this.#bindBot(this.#bot);
|
|
263
|
+
await this.#bot.start();
|
|
264
|
+
logger.info(formatCompact({
|
|
265
|
+
op: 'connect',
|
|
266
|
+
endpoint: this.#options.config.name,
|
|
267
|
+
mode: this.#options.config.mode,
|
|
268
|
+
path: this.#options.config.webhookPath,
|
|
269
|
+
}));
|
|
476
270
|
} catch (error) {
|
|
477
|
-
this.
|
|
271
|
+
await this.stop();
|
|
272
|
+
logger.error('Failed to connect QQ HTTP receiver:', error);
|
|
478
273
|
throw error;
|
|
479
274
|
}
|
|
480
275
|
}
|
|
481
276
|
|
|
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
|
-
}
|
|
277
|
+
open(): void {
|
|
278
|
+
this.#open = true;
|
|
493
279
|
}
|
|
494
280
|
|
|
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
|
-
}
|
|
281
|
+
close(): void {
|
|
282
|
+
this.#open = false;
|
|
506
283
|
}
|
|
507
284
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
285
|
+
async stop(): Promise<void> {
|
|
286
|
+
this.#open = false;
|
|
287
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
288
|
+
this.#unregisterAgent?.();
|
|
289
|
+
this.#unregisterAgent = undefined;
|
|
290
|
+
if (this.#bot) {
|
|
291
|
+
try {
|
|
292
|
+
this.#bot.removeAllListeners();
|
|
293
|
+
await this.#bot.stop();
|
|
294
|
+
} catch {
|
|
295
|
+
/* ignore */
|
|
296
|
+
}
|
|
297
|
+
this.#bot = null;
|
|
298
|
+
}
|
|
299
|
+
this.#started = false;
|
|
300
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
304
|
+
const body = formatOutboundText(payload);
|
|
305
|
+
const parsed = parseSendTarget(target);
|
|
306
|
+
const bot = this.#requireBot();
|
|
307
|
+
let result: unknown;
|
|
308
|
+
switch (parsed.kind) {
|
|
309
|
+
case 'private':
|
|
310
|
+
result = await bot.sendPrivateMessage(parsed.id, body);
|
|
311
|
+
break;
|
|
312
|
+
case 'group':
|
|
313
|
+
result = await bot.sendGroupMessage(parsed.id, body);
|
|
314
|
+
break;
|
|
315
|
+
case 'channel':
|
|
316
|
+
result = await bot.sendGuildMessage(parsed.id, body);
|
|
317
|
+
break;
|
|
318
|
+
case 'direct':
|
|
319
|
+
if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
|
|
320
|
+
result = await bot.sendDirectMessage(parsed.id, body);
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
|
|
518
324
|
}
|
|
325
|
+
return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
admit(msg: QqInboundMessage): void {
|
|
329
|
+
if (!this.#open) return;
|
|
330
|
+
const target = formatInboundTarget(msg);
|
|
331
|
+
void this.#options.gateway.receive({
|
|
332
|
+
adapter: this.#options.id,
|
|
333
|
+
target,
|
|
334
|
+
content: formatInboundContent(msg),
|
|
335
|
+
sender: senderDisplayName(msg),
|
|
336
|
+
id: msg.id,
|
|
337
|
+
metadata: Object.freeze({
|
|
338
|
+
endpoint: this.#options.config.name,
|
|
339
|
+
channelKind: msg.channelKind,
|
|
340
|
+
userId: msg.authorId,
|
|
341
|
+
guildId: msg.guildId,
|
|
342
|
+
roles: msg.authorRoles,
|
|
343
|
+
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
344
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
345
|
+
}),
|
|
346
|
+
}).catch((err) => {
|
|
347
|
+
logger.warn(formatCompact({
|
|
348
|
+
op: 'qq_gateway_receive_failed',
|
|
349
|
+
target,
|
|
350
|
+
error: err instanceof Error ? err.message : String(err),
|
|
351
|
+
}));
|
|
352
|
+
});
|
|
519
353
|
}
|
|
520
354
|
|
|
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
|
-
}
|
|
355
|
+
getGuilds() {
|
|
356
|
+
return this.#requireBot().getGuilds();
|
|
532
357
|
}
|
|
533
358
|
|
|
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
|
-
}
|
|
359
|
+
getChannels(guildId: string) {
|
|
360
|
+
return this.#requireBot().getChannels(guildId);
|
|
546
361
|
}
|
|
547
362
|
|
|
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
|
-
}
|
|
363
|
+
getChannelInfo(channelId: string) {
|
|
364
|
+
return this.#requireBot().getChannelInfo(channelId);
|
|
564
365
|
}
|
|
565
366
|
|
|
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
|
-
}
|
|
367
|
+
getGuildMember(guildId: string, userId: string) {
|
|
368
|
+
return this.#requireBot().getGuildMember(guildId, userId);
|
|
577
369
|
}
|
|
578
370
|
|
|
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
|
-
}
|
|
371
|
+
getGuildRoles(guildId: string) {
|
|
372
|
+
return this.#requireBot().getGuildRoles(guildId);
|
|
595
373
|
}
|
|
596
374
|
|
|
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
|
-
}
|
|
375
|
+
createGuildRole(guildId: string, name: string, color?: number) {
|
|
376
|
+
return this.#requireBot().createGuildRole(guildId, name, color);
|
|
613
377
|
}
|
|
614
378
|
|
|
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
|
-
}
|
|
379
|
+
addMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
380
|
+
return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
|
|
631
381
|
}
|
|
632
382
|
|
|
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
|
-
}
|
|
383
|
+
removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string) {
|
|
384
|
+
return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
|
|
653
385
|
}
|
|
654
386
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
async muteAll(guildId: string, muteSeconds: number): Promise<boolean> {
|
|
661
|
-
try {
|
|
662
|
-
if (muteSeconds > 0) {
|
|
663
|
-
await this.guildService.mute(guildId, muteSeconds);
|
|
664
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 开启全员禁言(频道 ${guildId})`);
|
|
665
|
-
} else {
|
|
666
|
-
await this.guildService.unmute(guildId);
|
|
667
|
-
this.pluginLogger.info(`QQ Endpoint ${this.$id} 关闭全员禁言(频道 ${guildId})`);
|
|
668
|
-
}
|
|
669
|
-
return true;
|
|
670
|
-
} catch (error) {
|
|
671
|
-
this.pluginLogger.error(`QQ Endpoint ${this.$id} 全员禁言操作失败:`, error);
|
|
672
|
-
throw error;
|
|
673
|
-
}
|
|
387
|
+
#setupRoutes(): void {
|
|
388
|
+
this.#routeReleases.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
389
|
+
config: this.#options.config,
|
|
390
|
+
getBot: () => this.#bot,
|
|
391
|
+
}));
|
|
674
392
|
}
|
|
393
|
+
|
|
394
|
+
#bindBot(bot: QqBotTransport): void {
|
|
395
|
+
bindQqBotInboundEvents(bot, (raw) => {
|
|
396
|
+
const msg = normalizeQqMessage(raw);
|
|
397
|
+
if (msg) this.admit(msg);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#requireBot(): QqBotTransport {
|
|
402
|
+
if (!this.#bot) throw new Error('QQ bot not connected');
|
|
403
|
+
return this.#bot;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function createQqEndpointManagement(endpoint: {
|
|
408
|
+
getGuilds(): Promise<unknown[]>;
|
|
409
|
+
getChannels(guildId: string): Promise<unknown[]>;
|
|
410
|
+
}): EndpointManagement {
|
|
411
|
+
return Object.freeze<EndpointManagement>({
|
|
412
|
+
async listChannels(): Promise<readonly EndpointChannel[]> {
|
|
413
|
+
const channels: EndpointChannel[] = [];
|
|
414
|
+
const guilds = await endpoint.getGuilds();
|
|
415
|
+
for (const guildValue of Array.isArray(guilds) ? guilds : []) {
|
|
416
|
+
const guild = asRecord(guildValue);
|
|
417
|
+
const guildId = String(guild.id ?? guild.guild_id ?? guildValue ?? '');
|
|
418
|
+
if (!guildId) continue;
|
|
419
|
+
const guildName = String(guild.name ?? guild.guild_name ?? guildId);
|
|
420
|
+
const rows = await endpoint.getChannels(guildId);
|
|
421
|
+
for (const channelValue of Array.isArray(rows) ? rows : []) {
|
|
422
|
+
const channel = asRecord(channelValue);
|
|
423
|
+
const id = String(channel.id ?? channel.channel_id ?? channelValue ?? '');
|
|
424
|
+
if (!id) continue;
|
|
425
|
+
channels.push({
|
|
426
|
+
id,
|
|
427
|
+
name: String(channel.name ?? channel.channel_name ?? id),
|
|
428
|
+
parent: { type: 'guild', id: guildId, name: guildName },
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return channels;
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
438
|
+
return value !== null && typeof value === 'object'
|
|
439
|
+
? value as Record<string, unknown>
|
|
440
|
+
: {};
|
|
675
441
|
}
|