@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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/agent/tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { platformPermit } from '../../src/platform-permit.js';
|
|
4
4
|
import { getQqAgentDeps } from '../../src/qq-agent-deps.js';
|
|
5
5
|
|
|
6
|
-
export default
|
|
6
|
+
export default defineAgentTool<{ endpoint_id: string; guild_id: string; user_id: string }>({
|
|
7
7
|
description: '获取 QQ 频道中指定成员的详细信息',
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
endpoint_id: z.string().describe('Endpoint 名称'),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/agent/tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { platformPermit } from '../../src/platform-permit.js';
|
|
4
4
|
import { getQqAgentDeps } from '../../src/qq-agent-deps.js';
|
|
5
5
|
|
|
6
|
-
export default
|
|
6
|
+
export default defineAgentTool<{ endpoint_id: string; guild_id: string; channel_id: string; user_id: string; role_id: string }>({
|
|
7
7
|
description: '移除成员的 QQ 频道角色',
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
endpoint_id: z.string().describe('Endpoint 名称'),
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
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 type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
8
|
+
import { type QqInboundMessage, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig } from './protocol.js';
|
|
9
|
+
import { type CreateQqHttpBot } from './webhook.js';
|
|
10
|
+
import { type CreateQqBot } from './ws.js';
|
|
11
|
+
export type { CreateQqBot, QqBotTransport } from './ws.js';
|
|
12
|
+
export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
|
|
13
|
+
export interface QqEndpointOptions {
|
|
14
|
+
readonly id: CapabilityId;
|
|
15
|
+
readonly gateway: MessageGateway;
|
|
16
|
+
readonly config: ResolvedQqWebsocketConfig;
|
|
17
|
+
readonly createBot?: CreateQqBot;
|
|
18
|
+
}
|
|
19
|
+
export declare class QqWebsocketEndpoint implements EndpointInstance {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(options: QqEndpointOptions);
|
|
22
|
+
start(): Promise<void>;
|
|
23
|
+
open(): void;
|
|
24
|
+
close(): void;
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
send({ target, payload }: {
|
|
27
|
+
readonly target: string;
|
|
28
|
+
readonly payload: unknown;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
/** Test / internal: admit a message when open. */
|
|
31
|
+
admit(msg: QqInboundMessage): void;
|
|
32
|
+
getGuilds(): Promise<unknown[]>;
|
|
33
|
+
getChannels(guildId: string): Promise<unknown[]>;
|
|
34
|
+
getChannelInfo(channelId: string): Promise<unknown>;
|
|
35
|
+
getGuildMember(guildId: string, userId: string): Promise<unknown>;
|
|
36
|
+
getGuildRoles(guildId: string): Promise<unknown[]>;
|
|
37
|
+
createGuildRole(guildId: string, name: string, color?: number): Promise<unknown>;
|
|
38
|
+
addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
|
|
39
|
+
removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
|
|
40
|
+
}
|
|
41
|
+
export interface QqHttpEndpointOptions {
|
|
42
|
+
readonly id: CapabilityId;
|
|
43
|
+
readonly gateway: MessageGateway;
|
|
44
|
+
readonly http: HttpHost;
|
|
45
|
+
readonly config: ResolvedQqHttpConfig;
|
|
46
|
+
readonly createBot?: CreateQqHttpBot;
|
|
47
|
+
}
|
|
48
|
+
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
49
|
+
export declare class QqHttpEndpoint implements EndpointInstance {
|
|
50
|
+
#private;
|
|
51
|
+
constructor(options: QqHttpEndpointOptions);
|
|
52
|
+
start(): Promise<void>;
|
|
53
|
+
open(): void;
|
|
54
|
+
close(): void;
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
send({ target, payload }: {
|
|
57
|
+
readonly target: string;
|
|
58
|
+
readonly payload: unknown;
|
|
59
|
+
}): Promise<string>;
|
|
60
|
+
admit(msg: QqInboundMessage): void;
|
|
61
|
+
getGuilds(): Promise<unknown[]>;
|
|
62
|
+
getChannels(guildId: string): Promise<unknown[]>;
|
|
63
|
+
getChannelInfo(channelId: string): Promise<unknown>;
|
|
64
|
+
getGuildMember(guildId: string, userId: string): Promise<unknown>;
|
|
65
|
+
getGuildRoles(guildId: string): Promise<unknown[]>;
|
|
66
|
+
createGuildRole(guildId: string, name: string, color?: number): Promise<unknown>;
|
|
67
|
+
addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
|
|
68
|
+
removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
|
|
69
|
+
}
|
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { registerQqAgentEndpoint } from './qq-agent-deps.js';
|
|
3
|
+
import { formatInboundContent, formatInboundTarget, formatOutboundText, parseSendTarget, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
|
|
4
|
+
import { defaultCreateHttpBot, registerQqWebhookRoutes, } from './webhook.js';
|
|
5
|
+
import { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
|
|
6
|
+
const logger = getLogger('qq');
|
|
7
|
+
export class QqWebsocketEndpoint {
|
|
8
|
+
#options;
|
|
9
|
+
#createBot;
|
|
10
|
+
#bot = null;
|
|
11
|
+
#open = false;
|
|
12
|
+
#started = false;
|
|
13
|
+
#unregisterAgent;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.#options = options;
|
|
16
|
+
this.#createBot = options.createBot ?? defaultCreateBot;
|
|
17
|
+
}
|
|
18
|
+
async start() {
|
|
19
|
+
if (this.#started)
|
|
20
|
+
return;
|
|
21
|
+
this.#started = true;
|
|
22
|
+
try {
|
|
23
|
+
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
|
|
24
|
+
this.#bot = this.#createBot(this.#options.config);
|
|
25
|
+
this.#bindBot(this.#bot);
|
|
26
|
+
await this.#bot.start();
|
|
27
|
+
logger.info(formatCompact({
|
|
28
|
+
op: 'connect',
|
|
29
|
+
endpoint: this.#options.config.name,
|
|
30
|
+
mode: 'websocket',
|
|
31
|
+
appid: this.#options.config.appid,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
await this.stop();
|
|
36
|
+
logger.error('Failed to connect QQ websocket:', error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
open() {
|
|
41
|
+
this.#open = true;
|
|
42
|
+
}
|
|
43
|
+
close() {
|
|
44
|
+
this.#open = false;
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
this.#open = false;
|
|
48
|
+
this.#unregisterAgent?.();
|
|
49
|
+
this.#unregisterAgent = undefined;
|
|
50
|
+
if (this.#bot) {
|
|
51
|
+
try {
|
|
52
|
+
this.#bot.removeAllListeners();
|
|
53
|
+
await this.#bot.stop();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* ignore */
|
|
57
|
+
}
|
|
58
|
+
this.#bot = null;
|
|
59
|
+
}
|
|
60
|
+
this.#started = false;
|
|
61
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
62
|
+
}
|
|
63
|
+
async send({ target, payload }) {
|
|
64
|
+
const body = formatOutboundText(payload);
|
|
65
|
+
const parsed = parseSendTarget(target);
|
|
66
|
+
const bot = this.#requireBot();
|
|
67
|
+
let result;
|
|
68
|
+
switch (parsed.kind) {
|
|
69
|
+
case 'private':
|
|
70
|
+
result = await bot.sendPrivateMessage(parsed.id, body);
|
|
71
|
+
break;
|
|
72
|
+
case 'group':
|
|
73
|
+
result = await bot.sendGroupMessage(parsed.id, body);
|
|
74
|
+
break;
|
|
75
|
+
case 'channel':
|
|
76
|
+
result = await bot.sendGuildMessage(parsed.id, body);
|
|
77
|
+
break;
|
|
78
|
+
case 'direct':
|
|
79
|
+
if (!bot.sendDirectMessage)
|
|
80
|
+
throw new Error('QQ direct message not supported by transport');
|
|
81
|
+
result = await bot.sendDirectMessage(parsed.id, body);
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`unsupported QQ target kind: ${String(parsed.kind)}`);
|
|
85
|
+
}
|
|
86
|
+
const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
|
|
87
|
+
logger.debug(formatCompact({
|
|
88
|
+
op: 'qq_send',
|
|
89
|
+
endpoint: this.#options.config.name,
|
|
90
|
+
target,
|
|
91
|
+
messageId,
|
|
92
|
+
}));
|
|
93
|
+
return messageId;
|
|
94
|
+
}
|
|
95
|
+
/** Test / internal: admit a message when open. */
|
|
96
|
+
admit(msg) {
|
|
97
|
+
if (!this.#open)
|
|
98
|
+
return;
|
|
99
|
+
const target = formatInboundTarget(msg);
|
|
100
|
+
void this.#options.gateway.receive({
|
|
101
|
+
adapter: this.#options.id,
|
|
102
|
+
target,
|
|
103
|
+
content: formatInboundContent(msg),
|
|
104
|
+
sender: senderDisplayName(msg),
|
|
105
|
+
id: msg.id,
|
|
106
|
+
metadata: Object.freeze({
|
|
107
|
+
endpoint: this.#options.config.name,
|
|
108
|
+
channelKind: msg.channelKind,
|
|
109
|
+
userId: msg.authorId,
|
|
110
|
+
guildId: msg.guildId,
|
|
111
|
+
roles: msg.authorRoles,
|
|
112
|
+
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
113
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
114
|
+
}),
|
|
115
|
+
}).catch((err) => {
|
|
116
|
+
logger.warn(formatCompact({
|
|
117
|
+
op: 'qq_gateway_receive_failed',
|
|
118
|
+
target,
|
|
119
|
+
error: err instanceof Error ? err.message : String(err),
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
getGuilds() {
|
|
124
|
+
return this.#requireBot().getGuilds();
|
|
125
|
+
}
|
|
126
|
+
getChannels(guildId) {
|
|
127
|
+
return this.#requireBot().getChannels(guildId);
|
|
128
|
+
}
|
|
129
|
+
getChannelInfo(channelId) {
|
|
130
|
+
return this.#requireBot().getChannelInfo(channelId);
|
|
131
|
+
}
|
|
132
|
+
getGuildMember(guildId, userId) {
|
|
133
|
+
return this.#requireBot().getGuildMember(guildId, userId);
|
|
134
|
+
}
|
|
135
|
+
getGuildRoles(guildId) {
|
|
136
|
+
return this.#requireBot().getGuildRoles(guildId);
|
|
137
|
+
}
|
|
138
|
+
createGuildRole(guildId, name, color) {
|
|
139
|
+
return this.#requireBot().createGuildRole(guildId, name, color);
|
|
140
|
+
}
|
|
141
|
+
addMemberRole(guildId, channelId, userId, roleId) {
|
|
142
|
+
return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
|
|
143
|
+
}
|
|
144
|
+
removeMemberRole(guildId, channelId, userId, roleId) {
|
|
145
|
+
return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
|
|
146
|
+
}
|
|
147
|
+
#bindBot(bot) {
|
|
148
|
+
bindQqBotInboundEvents(bot, (raw) => {
|
|
149
|
+
const msg = normalizeQqMessage(raw);
|
|
150
|
+
if (msg)
|
|
151
|
+
this.admit(msg);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
#requireBot() {
|
|
155
|
+
if (!this.#bot)
|
|
156
|
+
throw new Error('QQ bot not connected');
|
|
157
|
+
return this.#bot;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
161
|
+
export class QqHttpEndpoint {
|
|
162
|
+
#options;
|
|
163
|
+
#createBot;
|
|
164
|
+
#bot = null;
|
|
165
|
+
#routeReleases = [];
|
|
166
|
+
#open = false;
|
|
167
|
+
#started = false;
|
|
168
|
+
#unregisterAgent;
|
|
169
|
+
constructor(options) {
|
|
170
|
+
this.#options = options;
|
|
171
|
+
this.#createBot = options.createBot ?? defaultCreateHttpBot;
|
|
172
|
+
}
|
|
173
|
+
async start() {
|
|
174
|
+
if (this.#started)
|
|
175
|
+
return;
|
|
176
|
+
this.#started = true;
|
|
177
|
+
try {
|
|
178
|
+
this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
|
|
179
|
+
this.#setupRoutes();
|
|
180
|
+
this.#bot = this.#createBot(this.#options.config);
|
|
181
|
+
this.#bindBot(this.#bot);
|
|
182
|
+
await this.#bot.start();
|
|
183
|
+
logger.info(formatCompact({
|
|
184
|
+
op: 'connect',
|
|
185
|
+
endpoint: this.#options.config.name,
|
|
186
|
+
mode: this.#options.config.mode,
|
|
187
|
+
path: this.#options.config.webhookPath,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
await this.stop();
|
|
192
|
+
logger.error('Failed to connect QQ HTTP receiver:', error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
open() {
|
|
197
|
+
this.#open = true;
|
|
198
|
+
}
|
|
199
|
+
close() {
|
|
200
|
+
this.#open = false;
|
|
201
|
+
}
|
|
202
|
+
async stop() {
|
|
203
|
+
this.#open = false;
|
|
204
|
+
for (const release of this.#routeReleases.splice(0))
|
|
205
|
+
release();
|
|
206
|
+
this.#unregisterAgent?.();
|
|
207
|
+
this.#unregisterAgent = undefined;
|
|
208
|
+
if (this.#bot) {
|
|
209
|
+
try {
|
|
210
|
+
this.#bot.removeAllListeners();
|
|
211
|
+
await this.#bot.stop();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
/* ignore */
|
|
215
|
+
}
|
|
216
|
+
this.#bot = null;
|
|
217
|
+
}
|
|
218
|
+
this.#started = false;
|
|
219
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
220
|
+
}
|
|
221
|
+
async send({ target, payload }) {
|
|
222
|
+
const body = formatOutboundText(payload);
|
|
223
|
+
const parsed = parseSendTarget(target);
|
|
224
|
+
const bot = this.#requireBot();
|
|
225
|
+
let result;
|
|
226
|
+
switch (parsed.kind) {
|
|
227
|
+
case 'private':
|
|
228
|
+
result = await bot.sendPrivateMessage(parsed.id, body);
|
|
229
|
+
break;
|
|
230
|
+
case 'group':
|
|
231
|
+
result = await bot.sendGroupMessage(parsed.id, body);
|
|
232
|
+
break;
|
|
233
|
+
case 'channel':
|
|
234
|
+
result = await bot.sendGuildMessage(parsed.id, body);
|
|
235
|
+
break;
|
|
236
|
+
case 'direct':
|
|
237
|
+
if (!bot.sendDirectMessage)
|
|
238
|
+
throw new Error('QQ direct message not supported by transport');
|
|
239
|
+
result = await bot.sendDirectMessage(parsed.id, body);
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
throw new Error(`unsupported QQ target kind: ${String(parsed.kind)}`);
|
|
243
|
+
}
|
|
244
|
+
return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
|
|
245
|
+
}
|
|
246
|
+
admit(msg) {
|
|
247
|
+
if (!this.#open)
|
|
248
|
+
return;
|
|
249
|
+
const target = formatInboundTarget(msg);
|
|
250
|
+
void this.#options.gateway.receive({
|
|
251
|
+
adapter: this.#options.id,
|
|
252
|
+
target,
|
|
253
|
+
content: formatInboundContent(msg),
|
|
254
|
+
sender: senderDisplayName(msg),
|
|
255
|
+
id: msg.id,
|
|
256
|
+
metadata: Object.freeze({
|
|
257
|
+
endpoint: this.#options.config.name,
|
|
258
|
+
channelKind: msg.channelKind,
|
|
259
|
+
userId: msg.authorId,
|
|
260
|
+
guildId: msg.guildId,
|
|
261
|
+
roles: msg.authorRoles,
|
|
262
|
+
// AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
|
|
263
|
+
...(msg.mentioned ? { mentioned: true } : {}),
|
|
264
|
+
}),
|
|
265
|
+
}).catch((err) => {
|
|
266
|
+
logger.warn(formatCompact({
|
|
267
|
+
op: 'qq_gateway_receive_failed',
|
|
268
|
+
target,
|
|
269
|
+
error: err instanceof Error ? err.message : String(err),
|
|
270
|
+
}));
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
getGuilds() {
|
|
274
|
+
return this.#requireBot().getGuilds();
|
|
275
|
+
}
|
|
276
|
+
getChannels(guildId) {
|
|
277
|
+
return this.#requireBot().getChannels(guildId);
|
|
278
|
+
}
|
|
279
|
+
getChannelInfo(channelId) {
|
|
280
|
+
return this.#requireBot().getChannelInfo(channelId);
|
|
281
|
+
}
|
|
282
|
+
getGuildMember(guildId, userId) {
|
|
283
|
+
return this.#requireBot().getGuildMember(guildId, userId);
|
|
284
|
+
}
|
|
285
|
+
getGuildRoles(guildId) {
|
|
286
|
+
return this.#requireBot().getGuildRoles(guildId);
|
|
287
|
+
}
|
|
288
|
+
createGuildRole(guildId, name, color) {
|
|
289
|
+
return this.#requireBot().createGuildRole(guildId, name, color);
|
|
290
|
+
}
|
|
291
|
+
addMemberRole(guildId, channelId, userId, roleId) {
|
|
292
|
+
return this.#requireBot().addMemberRole(guildId, channelId, userId, roleId);
|
|
293
|
+
}
|
|
294
|
+
removeMemberRole(guildId, channelId, userId, roleId) {
|
|
295
|
+
return this.#requireBot().removeMemberRole(guildId, channelId, userId, roleId);
|
|
296
|
+
}
|
|
297
|
+
#setupRoutes() {
|
|
298
|
+
this.#routeReleases.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
299
|
+
config: this.#options.config,
|
|
300
|
+
getBot: () => this.#bot,
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
#bindBot(bot) {
|
|
304
|
+
bindQqBotInboundEvents(bot, (raw) => {
|
|
305
|
+
const msg = normalizeQqMessage(raw);
|
|
306
|
+
if (msg)
|
|
307
|
+
this.admit(msg);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
#requireBot() {
|
|
311
|
+
if (!this.#bot)
|
|
312
|
+
throw new Error('QQ bot not connected');
|
|
313
|
+
return this.#bot;
|
|
314
|
+
}
|
|
315
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { formatInboundContent, formatInboundTarget, formatOutboundText, parseSendTarget, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, type ParsedSendTarget, type QqAdapterConfig, type QqChannelKind, type QqInboundMessage, type QqWireSegment, type ResolvedQqConfig, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig, } from './protocol.js';
|
|
2
|
+
export { QqHttpEndpoint, QqWebsocketEndpoint, type CreateQqBot, type CreateQqHttpBot, type QqBotTransport, type QqEndpointOptions, type QqHttpBotTransport, type QqHttpEndpointOptions, } from './endpoint.js';
|
|
3
|
+
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, type QqWebhookHandler, } from './webhook.js';
|
|
4
|
+
export { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
|
|
5
|
+
export { getQqAgentDeps, registerQqAgentEndpoint, setQqAgentDeps, type QqAgentDeps, type QqAgentEndpoint, } from './qq-agent-deps.js';
|
|
6
|
+
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, registerQqPlatformPermitChecker, } from './platform-permit.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { formatInboundContent, formatInboundTarget, formatOutboundText, parseSendTarget, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, } from './protocol.js';
|
|
2
|
+
export { QqHttpEndpoint, QqWebsocketEndpoint, } from './endpoint.js';
|
|
3
|
+
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, } from './webhook.js';
|
|
4
|
+
export { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
|
|
5
|
+
export { getQqAgentDeps, registerQqAgentEndpoint, setQqAgentDeps, } from './qq-agent-deps.js';
|
|
6
|
+
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, registerQqPlatformPermitChecker, } from './platform-permit.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
|
|
3
|
+
*/
|
|
4
|
+
import { type Message } from '@zhin.js/core';
|
|
5
|
+
export declare function platformPermit(perm: string): string;
|
|
6
|
+
export declare function qqGuildPermitResolver(logicalPerm: string): string;
|
|
7
|
+
export declare function normalizeQqGuildSenderForPermit(input: {
|
|
8
|
+
roles?: string[];
|
|
9
|
+
isOwner?: boolean;
|
|
10
|
+
isAdmin?: boolean;
|
|
11
|
+
}): {
|
|
12
|
+
role?: string;
|
|
13
|
+
permissions?: string[];
|
|
14
|
+
};
|
|
15
|
+
export declare function checkQqPlatformPermit(perm: string, message: Message<any>): boolean;
|
|
16
|
+
export declare function registerQqPlatformPermitChecker(): () => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
|
|
3
3
|
*/
|
|
4
|
-
import { registerPlatformPermitChecker } from 'zhin.js';
|
|
4
|
+
import { registerPlatformPermitChecker } from '@zhin.js/core';
|
|
5
5
|
const ADAPTER = 'qq';
|
|
6
6
|
export function platformPermit(perm) {
|
|
7
7
|
return `platform(${ADAPTER},${perm})`;
|
|
@@ -47,4 +47,3 @@ export function checkQqPlatformPermit(perm, message) {
|
|
|
47
47
|
export function registerQqPlatformPermitChecker() {
|
|
48
48
|
return registerPlatformPermitChecker(ADAPTER, checkQqPlatformPermit);
|
|
49
49
|
}
|
|
50
|
-
//# sourceMappingURL=platform-permit.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Official Bot protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
6
|
+
export interface QqAdapterConfig {
|
|
7
|
+
readonly name?: string;
|
|
8
|
+
readonly appid?: string;
|
|
9
|
+
readonly secret?: string;
|
|
10
|
+
/** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
|
|
11
|
+
readonly mode?: 'websocket' | 'webhook' | 'middleware';
|
|
12
|
+
readonly sandbox?: boolean;
|
|
13
|
+
readonly intents?: readonly string[];
|
|
14
|
+
readonly accessTokenUrl?: string;
|
|
15
|
+
readonly gatewayUrl?: string;
|
|
16
|
+
readonly webhookPath?: string;
|
|
17
|
+
readonly port?: number;
|
|
18
|
+
readonly path?: string;
|
|
19
|
+
/** Transitional: legacy root `endpoints[]` with `context: qq`. */
|
|
20
|
+
readonly endpoints?: ReadonlyArray<{
|
|
21
|
+
readonly context?: string;
|
|
22
|
+
readonly name?: string;
|
|
23
|
+
readonly appid?: string;
|
|
24
|
+
readonly secret?: string;
|
|
25
|
+
readonly mode?: 'websocket' | 'webhook' | 'middleware';
|
|
26
|
+
readonly sandbox?: boolean;
|
|
27
|
+
readonly intents?: readonly string[];
|
|
28
|
+
readonly accessTokenUrl?: string;
|
|
29
|
+
readonly gatewayUrl?: string;
|
|
30
|
+
readonly webhookPath?: string;
|
|
31
|
+
readonly port?: number;
|
|
32
|
+
readonly path?: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export interface ResolvedQqWebsocketConfig {
|
|
36
|
+
readonly context: 'qq';
|
|
37
|
+
readonly mode: 'websocket';
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly appid: string;
|
|
40
|
+
readonly secret: string;
|
|
41
|
+
readonly sandbox: boolean;
|
|
42
|
+
readonly intents?: readonly string[];
|
|
43
|
+
readonly accessTokenUrl?: string;
|
|
44
|
+
readonly gatewayUrl?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ResolvedQqHttpConfig {
|
|
47
|
+
readonly context: 'qq';
|
|
48
|
+
readonly mode: 'webhook' | 'middleware';
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly appid: string;
|
|
51
|
+
readonly secret: string;
|
|
52
|
+
readonly webhookPath: string;
|
|
53
|
+
readonly sandbox: boolean;
|
|
54
|
+
}
|
|
55
|
+
export type ResolvedQqConfig = ResolvedQqWebsocketConfig | ResolvedQqHttpConfig;
|
|
56
|
+
export type QqChannelKind = 'private' | 'group' | 'channel' | 'direct';
|
|
57
|
+
export interface QqInboundMessage {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly content: string;
|
|
60
|
+
readonly channelKind: QqChannelKind;
|
|
61
|
+
readonly channelId: string;
|
|
62
|
+
readonly authorId: string;
|
|
63
|
+
readonly authorName: string;
|
|
64
|
+
readonly authorRoles?: string[];
|
|
65
|
+
readonly timestamp: number;
|
|
66
|
+
readonly guildId?: string;
|
|
67
|
+
readonly rawMessage?: string;
|
|
68
|
+
/**
|
|
69
|
+
* 入站即 @ 机器人(AT_MESSAGE_CREATE 的 mentions[].bot,或群 GROUP_AT_MESSAGE_CREATE
|
|
70
|
+
* 事件本身仅 @ 时下发)。新 Runtime 纯文本 content 需经 metadata 传递。
|
|
71
|
+
*/
|
|
72
|
+
readonly mentioned?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface QqWireSegment {
|
|
75
|
+
readonly type: string;
|
|
76
|
+
readonly data?: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
export interface ParsedSendTarget {
|
|
79
|
+
readonly kind: QqChannelKind;
|
|
80
|
+
readonly id: string;
|
|
81
|
+
}
|
|
82
|
+
export declare function resolveQqConfig(config?: QqAdapterConfig): ResolvedQqConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function formatInboundTarget(msg: QqInboundMessage): string;
|
|
87
|
+
export declare function parseSendTarget(target: string): ParsedSendTarget;
|
|
88
|
+
export declare function senderDisplayName(msg: QqInboundMessage): string;
|
|
89
|
+
export declare function formatInboundContent(msg: QqInboundMessage): string;
|
|
90
|
+
/**
|
|
91
|
+
* Wire-encode an already-rendered outbound payload into a plain text / QQ Sendable string.
|
|
92
|
+
* Segment canonicalization is intentionally not done here.
|
|
93
|
+
*/
|
|
94
|
+
export declare function formatOutboundText(payload: unknown): string;
|
|
95
|
+
/** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
|
|
96
|
+
export declare function resolveOutboundMessageId(result: unknown): string;
|
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Official Bot protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
import { pickCredential } from '@zhin.js/adapter';
|
|
6
|
+
export function resolveQqConfig(config = {}) {
|
|
7
|
+
const entry = config.endpoints?.find((item) => item.context === 'qq' || !item.context);
|
|
8
|
+
const appid = pickCredential(config.appid, entry?.appid, process.env.QQ_APPID, process.env.QQ_BOT_APPID);
|
|
9
|
+
const secret = pickCredential(config.secret, entry?.secret, process.env.QQ_SECRET, process.env.QQ_BOT_SECRET);
|
|
10
|
+
if (!appid || !secret) {
|
|
11
|
+
throw new TypeError('QQ adapter requires appid + secret (plugins.<key>.appid/secret or endpoints with context: qq)');
|
|
12
|
+
}
|
|
13
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
14
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
15
|
+
|| process.env.QQ_BOT_NAME
|
|
16
|
+
|| 'qq-bot';
|
|
17
|
+
const mode = config.mode ?? entry?.mode ?? 'websocket';
|
|
18
|
+
if (mode === 'webhook' || mode === 'middleware') {
|
|
19
|
+
const webhookPathRaw = config.webhookPath ?? entry?.webhookPath ?? '/qq/webhook';
|
|
20
|
+
const webhookPath = webhookPathRaw.startsWith('/') ? webhookPathRaw : `/${webhookPathRaw}`;
|
|
21
|
+
return {
|
|
22
|
+
context: 'qq',
|
|
23
|
+
mode,
|
|
24
|
+
name,
|
|
25
|
+
appid,
|
|
26
|
+
secret,
|
|
27
|
+
webhookPath,
|
|
28
|
+
sandbox: config.sandbox === true || entry?.sandbox === true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
context: 'qq',
|
|
33
|
+
mode: 'websocket',
|
|
34
|
+
name,
|
|
35
|
+
appid,
|
|
36
|
+
secret,
|
|
37
|
+
sandbox: config.sandbox === true || entry?.sandbox === true,
|
|
38
|
+
intents: config.intents ?? entry?.intents,
|
|
39
|
+
accessTokenUrl: config.accessTokenUrl ?? entry?.accessTokenUrl,
|
|
40
|
+
gatewayUrl: config.gatewayUrl ?? entry?.gatewayUrl,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
|
|
45
|
+
*/
|
|
46
|
+
export function formatInboundTarget(msg) {
|
|
47
|
+
return `${msg.channelKind}:${msg.channelId}`;
|
|
48
|
+
}
|
|
49
|
+
export function parseSendTarget(target) {
|
|
50
|
+
const sep = target.indexOf(':');
|
|
51
|
+
if (sep <= 0) {
|
|
52
|
+
return { kind: 'private', id: target };
|
|
53
|
+
}
|
|
54
|
+
const head = target.slice(0, sep);
|
|
55
|
+
const rest = target.slice(sep + 1);
|
|
56
|
+
if (head === 'private' || head === 'group' || head === 'channel' || head === 'direct') {
|
|
57
|
+
return { kind: head, id: rest };
|
|
58
|
+
}
|
|
59
|
+
return { kind: 'private', id: target };
|
|
60
|
+
}
|
|
61
|
+
export function senderDisplayName(msg) {
|
|
62
|
+
return msg.authorName || msg.authorId;
|
|
63
|
+
}
|
|
64
|
+
export function formatInboundContent(msg) {
|
|
65
|
+
const text = (msg.content || msg.rawMessage || '').trim();
|
|
66
|
+
return text || '(Empty message)';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Wire-encode an already-rendered outbound payload into a plain text / QQ Sendable string.
|
|
70
|
+
* Segment canonicalization is intentionally not done here.
|
|
71
|
+
*/
|
|
72
|
+
export function formatOutboundText(payload) {
|
|
73
|
+
if (typeof payload === 'string')
|
|
74
|
+
return payload;
|
|
75
|
+
const segments = Array.isArray(payload)
|
|
76
|
+
? payload
|
|
77
|
+
: payload && typeof payload === 'object' && 'type' in payload
|
|
78
|
+
? [payload]
|
|
79
|
+
: [];
|
|
80
|
+
if (segments.length === 0) {
|
|
81
|
+
return payload == null
|
|
82
|
+
? ''
|
|
83
|
+
: typeof payload === 'object'
|
|
84
|
+
? JSON.stringify(payload)
|
|
85
|
+
: String(payload);
|
|
86
|
+
}
|
|
87
|
+
return segments
|
|
88
|
+
.map((item) => {
|
|
89
|
+
if (typeof item === 'string')
|
|
90
|
+
return item;
|
|
91
|
+
const data = item.data ?? {};
|
|
92
|
+
switch (item.type) {
|
|
93
|
+
case 'text':
|
|
94
|
+
return String(data.text ?? data.content ?? '');
|
|
95
|
+
case 'at': {
|
|
96
|
+
const id = String(data.user_id ?? data.qq ?? data.id ?? '');
|
|
97
|
+
return id ? `<@!${id}>` : '';
|
|
98
|
+
}
|
|
99
|
+
case 'image':
|
|
100
|
+
return String(data.url || data.file || '');
|
|
101
|
+
case 'reply':
|
|
102
|
+
return '';
|
|
103
|
+
default:
|
|
104
|
+
return data.text != null ? String(data.text) : '';
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join('');
|
|
109
|
+
}
|
|
110
|
+
/** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
|
|
111
|
+
export function resolveOutboundMessageId(result) {
|
|
112
|
+
if (!result || typeof result !== 'object') {
|
|
113
|
+
throw new Error('QQ 发送消息失败:响应为空');
|
|
114
|
+
}
|
|
115
|
+
const row = result;
|
|
116
|
+
const nested = row.data && typeof row.data === 'object'
|
|
117
|
+
? row.data
|
|
118
|
+
: undefined;
|
|
119
|
+
const audit = (row.message_audit ?? nested?.message_audit);
|
|
120
|
+
const id = row.id ?? row.message_id ?? audit?.audit_id;
|
|
121
|
+
if (id == null || id === '') {
|
|
122
|
+
const code = row.code;
|
|
123
|
+
const msg = row.message;
|
|
124
|
+
throw new Error(code != null
|
|
125
|
+
? `QQ 发送消息失败(${String(code)})${msg ? `: ${String(msg)}` : ''}`
|
|
126
|
+
: 'QQ 发送消息失败:响应缺少消息 ID');
|
|
127
|
+
}
|
|
128
|
+
return String(id);
|
|
129
|
+
}
|