@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.
Files changed (134) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +52 -408
  3. package/adapters/qq.ts +45 -0
  4. package/agent/tools/add_role.ts +23 -0
  5. package/agent/tools/channel_info.ts +19 -0
  6. package/agent/tools/create_role.ts +22 -0
  7. package/agent/tools/list_channels.ts +20 -0
  8. package/agent/tools/list_guilds.ts +17 -0
  9. package/agent/tools/list_roles.ts +20 -0
  10. package/agent/tools/member_detail.ts +20 -0
  11. package/agent/tools/remove_role.ts +23 -0
  12. package/lib/endpoint.d.ts +64 -120
  13. package/lib/endpoint.js +279 -564
  14. package/lib/index.d.ts +6 -23
  15. package/lib/index.js +6 -375
  16. package/lib/platform-permit.d.ts +1 -2
  17. package/lib/platform-permit.js +4 -2
  18. package/lib/protocol.d.ts +96 -0
  19. package/lib/protocol.js +129 -0
  20. package/lib/qq-agent-deps.d.ts +21 -0
  21. package/lib/qq-agent-deps.js +30 -0
  22. package/lib/webhook.d.ts +18 -0
  23. package/lib/webhook.js +100 -0
  24. package/lib/ws.d.ts +24 -0
  25. package/lib/ws.js +118 -0
  26. package/package.json +47 -33
  27. package/plugin.ts +13 -0
  28. package/schema.json +51 -0
  29. package/src/endpoint.ts +313 -605
  30. package/src/index.ts +55 -368
  31. package/src/platform-permit.ts +1 -2
  32. package/src/protocol.ts +229 -0
  33. package/src/qq-agent-deps.ts +60 -0
  34. package/src/webhook.ts +127 -0
  35. package/src/ws.ts +172 -0
  36. package/client/Dashboard.tsx +0 -230
  37. package/client/index.tsx +0 -11
  38. package/client/tsconfig.json +0 -7
  39. package/client/utils/api.ts +0 -30
  40. package/dist/index.js +0 -30
  41. package/lib/adapter.d.ts +0 -32
  42. package/lib/adapter.d.ts.map +0 -1
  43. package/lib/adapter.js +0 -82
  44. package/lib/adapter.js.map +0 -1
  45. package/lib/endpoint.d.ts.map +0 -1
  46. package/lib/endpoint.js.map +0 -1
  47. package/lib/gateway-config.d.ts +0 -14
  48. package/lib/gateway-config.d.ts.map +0 -1
  49. package/lib/gateway-config.js +0 -32
  50. package/lib/gateway-config.js.map +0 -1
  51. package/lib/group-at-normalize.d.ts +0 -8
  52. package/lib/group-at-normalize.d.ts.map +0 -1
  53. package/lib/group-at-normalize.js +0 -68
  54. package/lib/group-at-normalize.js.map +0 -1
  55. package/lib/inbound-action.d.ts +0 -15
  56. package/lib/inbound-action.d.ts.map +0 -1
  57. package/lib/inbound-action.js +0 -57
  58. package/lib/inbound-action.js.map +0 -1
  59. package/lib/inbound-normalize.d.ts +0 -11
  60. package/lib/inbound-normalize.d.ts.map +0 -1
  61. package/lib/inbound-normalize.js +0 -47
  62. package/lib/inbound-normalize.js.map +0 -1
  63. package/lib/index.d.ts.map +0 -1
  64. package/lib/index.js.map +0 -1
  65. package/lib/mention-enrich.d.ts +0 -12
  66. package/lib/mention-enrich.d.ts.map +0 -1
  67. package/lib/mention-enrich.js +0 -52
  68. package/lib/mention-enrich.js.map +0 -1
  69. package/lib/outbound-keyboard.d.ts +0 -4
  70. package/lib/outbound-keyboard.d.ts.map +0 -1
  71. package/lib/outbound-keyboard.js +0 -96
  72. package/lib/outbound-keyboard.js.map +0 -1
  73. package/lib/outbound-markdown.d.ts +0 -24
  74. package/lib/outbound-markdown.d.ts.map +0 -1
  75. package/lib/outbound-markdown.js +0 -177
  76. package/lib/outbound-markdown.js.map +0 -1
  77. package/lib/outbound-media.d.ts +0 -11
  78. package/lib/outbound-media.d.ts.map +0 -1
  79. package/lib/outbound-media.js +0 -51
  80. package/lib/outbound-media.js.map +0 -1
  81. package/lib/outbound-mixed-media.d.ts +0 -21
  82. package/lib/outbound-mixed-media.d.ts.map +0 -1
  83. package/lib/outbound-mixed-media.js +0 -78
  84. package/lib/outbound-mixed-media.js.map +0 -1
  85. package/lib/platform-permit.d.ts.map +0 -1
  86. package/lib/platform-permit.js.map +0 -1
  87. package/lib/qq-bind-api.d.ts +0 -37
  88. package/lib/qq-bind-api.d.ts.map +0 -1
  89. package/lib/qq-bind-api.js +0 -90
  90. package/lib/qq-bind-api.js.map +0 -1
  91. package/lib/qq-bind-flow.d.ts +0 -20
  92. package/lib/qq-bind-flow.d.ts.map +0 -1
  93. package/lib/qq-bind-flow.js +0 -86
  94. package/lib/qq-bind-flow.js.map +0 -1
  95. package/lib/qq-bind-persist.d.ts +0 -11
  96. package/lib/qq-bind-persist.d.ts.map +0 -1
  97. package/lib/qq-bind-persist.js +0 -52
  98. package/lib/qq-bind-persist.js.map +0 -1
  99. package/lib/qq-endpoint-manager.d.ts +0 -16
  100. package/lib/qq-endpoint-manager.d.ts.map +0 -1
  101. package/lib/qq-endpoint-manager.js +0 -159
  102. package/lib/qq-endpoint-manager.js.map +0 -1
  103. package/lib/sdk-version.d.ts +0 -8
  104. package/lib/sdk-version.d.ts.map +0 -1
  105. package/lib/sdk-version.js +0 -19
  106. package/lib/sdk-version.js.map +0 -1
  107. package/lib/segment-mapper.d.ts +0 -2
  108. package/lib/segment-mapper.d.ts.map +0 -1
  109. package/lib/segment-mapper.js +0 -2
  110. package/lib/segment-mapper.js.map +0 -1
  111. package/lib/types.d.ts +0 -45
  112. package/lib/types.d.ts.map +0 -1
  113. package/lib/types.js +0 -2
  114. package/lib/types.js.map +0 -1
  115. package/plugin.yml +0 -3
  116. package/src/adapter.ts +0 -101
  117. package/src/gateway-config.ts +0 -48
  118. package/src/group-at-normalize.ts +0 -76
  119. package/src/inbound-action.ts +0 -87
  120. package/src/inbound-normalize.ts +0 -57
  121. package/src/mention-enrich.ts +0 -71
  122. package/src/outbound-keyboard.ts +0 -118
  123. package/src/outbound-markdown.ts +0 -203
  124. package/src/outbound-media.ts +0 -58
  125. package/src/outbound-mixed-media.ts +0 -101
  126. package/src/qq-bind-api.ts +0 -145
  127. package/src/qq-bind-flow.ts +0 -127
  128. package/src/qq-bind-persist.ts +0 -71
  129. package/src/qq-endpoint-manager.ts +0 -199
  130. package/src/sdk-version.ts +0 -21
  131. package/src/segment-mapper.ts +0 -1
  132. package/src/types.ts +0 -53
  133. /package/{skills/qq → agent}/PERMITS.md +0 -0
  134. /package/{skills/qq/SKILL.md → agent/skills/qq.md} +0 -0
package/src/index.ts CHANGED
@@ -1,371 +1,58 @@
1
- /**
2
- * QQ 官方适配器入口:类型扩展、导出、注册
3
- */
4
- import path from "node:path";
5
- import { usePlugin, type Plugin, type ISceneManagement, createSceneManagementTools, type ToolFeature } from "zhin.js";
6
- import type { Router } from "@zhin.js/host-router";
7
- import { QQAdapter } from "./adapter.js";
8
- import { PageManager } from "@zhin.js/host-api";
9
- import {
1
+ export {
2
+ formatInboundContent,
3
+ formatInboundTarget,
4
+ formatOutboundText,
5
+ parseSendTarget,
6
+ resolveOutboundMessageId,
7
+ resolveQqConfig,
8
+ senderDisplayName,
9
+ type ParsedSendTarget,
10
+ type QqAdapterConfig,
11
+ type QqChannelKind,
12
+ type QqInboundMessage,
13
+ type QqWireSegment,
14
+ type ResolvedQqConfig,
15
+ type ResolvedQqHttpConfig,
16
+ type ResolvedQqWebsocketConfig,
17
+ } from './protocol.js';
18
+
19
+ export {
20
+ QqHttpEndpoint,
21
+ QqWebsocketEndpoint,
22
+ type CreateQqBot,
23
+ type CreateQqHttpBot,
24
+ type QqBotTransport,
25
+ type QqEndpointOptions,
26
+ type QqHttpBotTransport,
27
+ type QqHttpEndpointOptions,
28
+ } from './endpoint.js';
29
+
30
+ export {
31
+ registerQqWebhookRoutes,
32
+ handleQqWebhookRequest,
33
+ readRequestBodyText,
34
+ defaultCreateHttpBot,
35
+ type QqWebhookHandler,
36
+ } from './webhook.js';
37
+
38
+ export {
39
+ bindQqBotInboundEvents,
40
+ defaultCreateBot,
41
+ normalizeQqMessage,
42
+ } from './ws.js';
43
+
44
+ export {
45
+ getQqAgentDeps,
46
+ registerQqAgentEndpoint,
47
+ setQqAgentDeps,
48
+ type QqAgentDeps,
49
+ type QqAgentEndpoint,
50
+ } from './qq-agent-deps.js';
51
+
52
+ export {
53
+ checkQqPlatformPermit,
54
+ normalizeQqGuildSenderForPermit,
10
55
  platformPermit,
11
56
  qqGuildPermitResolver,
12
57
  registerQqPlatformPermitChecker,
13
- } from "./platform-permit.js";
14
- import { disposeQqEndpointProvision } from "./qq-endpoint-manager.js";
15
-
16
- declare module "zhin.js" {
17
- namespace Plugin {
18
- interface Contexts {
19
- web: PageManager;
20
- router: Router;
21
- }
22
- }
23
- }
24
-
25
- declare module "zhin.js" {
26
- interface Adapters {
27
- qq: QQAdapter;
28
- }
29
- }
30
-
31
- export * from "./types.js";
32
- export { QQEndpoint } from "./endpoint.js";
33
- export { QQAdapter } from "./adapter.js";
34
- export { startQqBindFlow } from "./qq-bind-flow.js";
35
- export * from "./qq-bind-api.js";
36
- export { QqEndpointManager, disposeQqEndpointProvision } from "./qq-endpoint-manager.js";
37
-
38
- const plugin = usePlugin();
39
- const { provide, useContext, onDispose } = plugin;
40
-
41
- useContext("router", (router: Router) => {
42
- provide({
43
- name: "qq",
44
- description: "QQ Official Endpoint Adapter",
45
- mounted: async (p: Plugin) => {
46
- const adapter = new QQAdapter(p, router);
47
- await adapter.start();
48
- return adapter;
49
- },
50
- dispose: async (adapter: QQAdapter) => {
51
- await adapter.stop();
52
- },
53
- });
54
- });
55
-
56
- useContext('tool', 'qq', (toolService: ToolFeature, qq: QQAdapter) => {
57
- const disposers: (() => void)[] = [];
58
- disposers.push(registerQqPlatformPermitChecker());
59
- const sceneTools = createSceneManagementTools(
60
- qq as unknown as ISceneManagement,
61
- 'qq',
62
- { permitResolver: qqGuildPermitResolver, registerChecker: false },
63
- );
64
- disposers.push(...sceneTools.map(t => toolService.addTool(t, plugin.name)));
65
-
66
- // 获取频道列表
67
- disposers.push(toolService.addTool({
68
- name: 'qq_list_guilds',
69
- description: '获取 QQ 频道列表',
70
- parameters: {
71
- type: 'object',
72
- properties: {
73
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
74
- },
75
- required: ['endpoint_id'],
76
- },
77
- platforms: ['qq'],
78
- tags: ['qq'],
79
- execute: async (args: Record<string, any>) => {
80
- const endpoint = qq.endpoints.get(args.endpoint_id);
81
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
82
- const guilds = await endpoint.getGuilds();
83
- return { guilds, count: guilds.length };
84
- },
85
- }, plugin.name));
86
-
87
- // 获取子频道列表
88
- disposers.push(toolService.addTool({
89
- name: 'qq_list_channels',
90
- description: '获取 QQ 频道下的子频道列表',
91
- parameters: {
92
- type: 'object',
93
- properties: {
94
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
95
- guild_id: { type: 'string', description: '频道 ID' },
96
- },
97
- required: ['endpoint_id', 'guild_id'],
98
- },
99
- platforms: ['qq'],
100
- tags: ['qq'],
101
- permissions: [platformPermit('manage_channels')],
102
- execute: async (args: Record<string, any>) => {
103
- const endpoint = qq.endpoints.get(args.endpoint_id);
104
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
105
- const channels = await endpoint.getChannels(args.guild_id);
106
- return { channels, count: channels.length };
107
- },
108
- }, plugin.name));
109
-
110
- // 获取角色列表
111
- disposers.push(toolService.addTool({
112
- name: 'qq_list_roles',
113
- description: '获取 QQ 频道角色列表',
114
- parameters: {
115
- type: 'object',
116
- properties: {
117
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
118
- guild_id: { type: 'string', description: '频道 ID' },
119
- },
120
- required: ['endpoint_id', 'guild_id'],
121
- },
122
- platforms: ['qq'],
123
- tags: ['qq'],
124
- permissions: [platformPermit('manage_roles')],
125
- execute: async (args: Record<string, any>) => {
126
- const endpoint = qq.endpoints.get(args.endpoint_id);
127
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
128
- const roles = await endpoint.getGuildRoles(args.guild_id);
129
- return { roles, count: roles.length };
130
- },
131
- }, plugin.name));
132
-
133
- // 创建角色
134
- disposers.push(toolService.addTool({
135
- name: 'qq_create_role',
136
- description: '创建 QQ 频道角色',
137
- parameters: {
138
- type: 'object',
139
- properties: {
140
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
141
- guild_id: { type: 'string', description: '频道 ID' },
142
- name: { type: 'string', description: '角色名称' },
143
- color: { type: 'number', description: '颜色(RGB 十进制数值)' },
144
- },
145
- required: ['endpoint_id', 'guild_id', 'name'],
146
- },
147
- platforms: ['qq'],
148
- tags: ['qq'],
149
- permissions: [platformPermit('guild_owner')],
150
- execute: async (args: Record<string, any>) => {
151
- const endpoint = qq.endpoints.get(args.endpoint_id);
152
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
153
- const role = await endpoint.createGuildRole(args.guild_id, args.name, args.color);
154
- return { success: !!role, role, message: role ? `角色 "${args.name}" 创建成功` : '创建失败' };
155
- },
156
- }, plugin.name));
157
-
158
- // 添加角色
159
- disposers.push(toolService.addTool({
160
- name: 'qq_add_role',
161
- description: '给成员添加 QQ 频道角色',
162
- parameters: {
163
- type: 'object',
164
- properties: {
165
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
166
- guild_id: { type: 'string', description: '频道 ID' },
167
- channel_id: { type: 'string', description: '子频道 ID' },
168
- user_id: { type: 'string', description: '用户 ID' },
169
- role_id: { type: 'string', description: '角色 ID' },
170
- },
171
- required: ['endpoint_id', 'guild_id', 'channel_id', 'user_id', 'role_id'],
172
- },
173
- platforms: ['qq'],
174
- tags: ['qq'],
175
- permissions: [platformPermit('manage_roles')],
176
- execute: async (args: Record<string, any>) => {
177
- const endpoint = qq.endpoints.get(args.endpoint_id);
178
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
179
- const success = await endpoint.addMemberRole(args.guild_id, args.channel_id, args.user_id, args.role_id);
180
- return { success, message: success ? '已给成员添加角色' : '操作失败' };
181
- },
182
- }, plugin.name));
183
-
184
- // 移除角色
185
- disposers.push(toolService.addTool({
186
- name: 'qq_remove_role',
187
- description: '移除成员的 QQ 频道角色',
188
- parameters: {
189
- type: 'object',
190
- properties: {
191
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
192
- guild_id: { type: 'string', description: '频道 ID' },
193
- channel_id: { type: 'string', description: '子频道 ID' },
194
- user_id: { type: 'string', description: '用户 ID' },
195
- role_id: { type: 'string', description: '角色 ID' },
196
- },
197
- required: ['endpoint_id', 'guild_id', 'channel_id', 'user_id', 'role_id'],
198
- },
199
- platforms: ['qq'],
200
- tags: ['qq'],
201
- permissions: [platformPermit('manage_roles')],
202
- execute: async (args: Record<string, any>) => {
203
- const endpoint = qq.endpoints.get(args.endpoint_id);
204
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
205
- const success = await endpoint.removeMemberRole(args.guild_id, args.channel_id, args.user_id, args.role_id);
206
- return { success, message: success ? '已移除成员的角色' : '操作失败' };
207
- },
208
- }, plugin.name));
209
-
210
- // 子频道详情
211
- disposers.push(toolService.addTool({
212
- name: 'qq_channel_info',
213
- description: '获取 QQ 频道中指定子频道的详细信息',
214
- parameters: {
215
- type: 'object',
216
- properties: {
217
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
218
- channel_id: { type: 'string', description: '子频道 ID' },
219
- },
220
- required: ['endpoint_id', 'channel_id'],
221
- },
222
- platforms: ['qq'],
223
- tags: ['qq'],
224
- permissions: [platformPermit('guild_admin')],
225
- execute: async (args: Record<string, any>) => {
226
- const endpoint = qq.endpoints.get(args.endpoint_id);
227
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
228
- const info = await endpoint.getChannelInfo(args.channel_id);
229
- return info;
230
- },
231
- }, plugin.name));
232
-
233
- // 单成员详情
234
- disposers.push(toolService.addTool({
235
- name: 'qq_member_detail',
236
- description: '获取 QQ 频道中指定成员的详细信息',
237
- parameters: {
238
- type: 'object',
239
- properties: {
240
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
241
- guild_id: { type: 'string', description: '频道 ID' },
242
- user_id: { type: 'string', description: '用户 ID' },
243
- },
244
- required: ['endpoint_id', 'guild_id', 'user_id'],
245
- },
246
- platforms: ['qq'],
247
- tags: ['qq'],
248
- permissions: [platformPermit('guild_admin')],
249
- execute: async (args: Record<string, any>) => {
250
- const endpoint = qq.endpoints.get(args.endpoint_id);
251
- if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
252
- const member = await endpoint.getGuildMember(args.guild_id, args.user_id);
253
- return member;
254
- },
255
- }, plugin.name));
256
-
257
- return () => disposers.forEach(d => d());
258
- });
259
-
260
- // ── Web 控制台 ─────────────────────────────────────────────────────────
261
- useContext("web", (pageManager) => {
262
- pageManager.addEntry({
263
- id: "qq",
264
- development: path.resolve(import.meta.dirname, "../client/index.tsx"),
265
- production: path.resolve(import.meta.dirname, "../dist/index.js"),
266
- meta: { name: "QQ" },
267
- });
268
- });
269
-
270
- useContext("router", "qq", (router: any, qq: QQAdapter) => {
271
- router.get("/api/qq/endpoints", async (ctx: any) => {
272
- try {
273
- const endpoints = Array.from(qq.endpoints.values());
274
- const result = await Promise.all(endpoints.map(async (endpoint) => {
275
- try {
276
- let guildCount = 0;
277
- try { const guilds = await endpoint.getGuilds(); guildCount = guilds?.length || 0; } catch {}
278
- return {
279
- name: endpoint.$config.name,
280
- connected: endpoint.$connected || false,
281
- guildCount,
282
- status: endpoint.$connected ? "online" : "offline",
283
- };
284
- } catch {
285
- return { name: endpoint.$config.name, connected: false, guildCount: 0, status: "error" };
286
- }
287
- }));
288
- ctx.body = { success: true, data: result };
289
- } catch {
290
- ctx.status = 500;
291
- ctx.body = { success: false, error: "获取 Endpoint 数据失败" };
292
- }
293
- });
294
-
295
- // Endpoint 连接/断开
296
- router.post("/api/qq/endpoints/:name/connect", async (ctx: any) => {
297
- try {
298
- const endpoint = qq.endpoints.get(ctx.params.name);
299
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
300
- if (endpoint.$connected) { ctx.body = { success: true, message: "已经在线" }; return; }
301
- await endpoint.$connect();
302
- ctx.body = { success: true, message: "连接成功" };
303
- } catch (e: unknown) {
304
- ctx.status = 500;
305
- ctx.body = { success: false, error: e instanceof Error ? e.message : "连接失败" };
306
- }
307
- });
308
-
309
- router.post("/api/qq/endpoints/:name/disconnect", async (ctx: any) => {
310
- try {
311
- const endpoint = qq.endpoints.get(ctx.params.name);
312
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
313
- if (!endpoint.$connected) { ctx.body = { success: true, message: "已经离线" }; return; }
314
- await endpoint.$disconnect();
315
- ctx.body = { success: true, message: "已断开" };
316
- } catch (e: unknown) {
317
- ctx.status = 500;
318
- ctx.body = { success: false, error: e instanceof Error ? e.message : "断开失败" };
319
- }
320
- });
321
-
322
- // 频道列表(QQ 官方 API 返回 guild_id/guild_name,归一化为控制台用的 id/name)
323
- router.get("/api/qq/endpoints/:name/guilds", async (ctx: any) => {
324
- try {
325
- const endpoint = qq.endpoints.get(ctx.params.name);
326
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
327
- if (!endpoint.$connected) { ctx.status = 400; ctx.body = { success: false, error: "Endpoint 未连接" }; return; }
328
- const raw = await endpoint.getGuilds();
329
- const guilds = (raw || []).map((g: any) => ({
330
- id: g.guild_id ?? g.id,
331
- name: g.guild_name ?? g.name,
332
- icon: g.icon,
333
- description: g.description,
334
- memberCount: g.member_count,
335
- ownerId: g.owner_id,
336
- owner: g.owner,
337
- joinTime: g.join_time,
338
- }));
339
- ctx.body = { success: true, data: guilds };
340
- } catch (e: unknown) {
341
- ctx.status = 500;
342
- ctx.body = { success: false, error: e instanceof Error ? e.message : "获取频道失败" };
343
- }
344
- });
345
-
346
- // 子频道列表(QQ 官方 API 返回 channel_id/channel_name,归一化为 id/name)
347
- router.get("/api/qq/endpoints/:name/guilds/:guildId/channels", async (ctx: any) => {
348
- try {
349
- const endpoint = qq.endpoints.get(ctx.params.name);
350
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
351
- if (!endpoint.$connected) { ctx.status = 400; ctx.body = { success: false, error: "Endpoint 未连接" }; return; }
352
- const raw = await endpoint.getChannels(ctx.params.guildId);
353
- const channels = (raw || []).map((ch: any) => ({
354
- id: ch.channel_id ?? ch.id,
355
- name: ch.channel_name ?? ch.name,
356
- type: ch.type,
357
- subType: ch.sub_type,
358
- position: ch.position,
359
- parentId: ch.parent_id,
360
- }));
361
- ctx.body = { success: true, data: channels };
362
- } catch (e: unknown) {
363
- ctx.status = 500;
364
- ctx.body = { success: false, error: e instanceof Error ? e.message : "获取子频道失败" };
365
- }
366
- });
367
- });
368
-
369
- onDispose(() => {
370
- disposeQqEndpointProvision();
371
- });
58
+ } from './platform-permit.js';
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
3
3
  */
4
- import type { Message } from 'zhin.js';
5
- import { registerPlatformPermitChecker } from 'zhin.js';
4
+ import { registerPlatformPermitChecker, type Message } from '@zhin.js/core';
6
5
 
7
6
  const ADAPTER = 'qq';
8
7
 
@@ -0,0 +1,229 @@
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
+
6
+ import { pickCredential } from '@zhin.js/adapter';
7
+
8
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
9
+ export interface QqAdapterConfig {
10
+ readonly name?: string;
11
+ readonly appid?: string;
12
+ readonly secret?: string;
13
+ /** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
14
+ readonly mode?: 'websocket' | 'webhook' | 'middleware';
15
+ readonly sandbox?: boolean;
16
+ readonly intents?: readonly string[];
17
+ readonly accessTokenUrl?: string;
18
+ readonly gatewayUrl?: string;
19
+ readonly webhookPath?: string;
20
+ readonly port?: number;
21
+ readonly path?: string;
22
+ /** Transitional: legacy root `endpoints[]` with `context: qq`. */
23
+ readonly endpoints?: ReadonlyArray<{
24
+ readonly context?: string;
25
+ readonly name?: string;
26
+ readonly appid?: string;
27
+ readonly secret?: string;
28
+ readonly mode?: 'websocket' | 'webhook' | 'middleware';
29
+ readonly sandbox?: boolean;
30
+ readonly intents?: readonly string[];
31
+ readonly accessTokenUrl?: string;
32
+ readonly gatewayUrl?: string;
33
+ readonly webhookPath?: string;
34
+ readonly port?: number;
35
+ readonly path?: string;
36
+ }>;
37
+ }
38
+
39
+ export interface ResolvedQqWebsocketConfig {
40
+ readonly context: 'qq';
41
+ readonly mode: 'websocket';
42
+ readonly name: string;
43
+ readonly appid: string;
44
+ readonly secret: string;
45
+ readonly sandbox: boolean;
46
+ readonly intents?: readonly string[];
47
+ readonly accessTokenUrl?: string;
48
+ readonly gatewayUrl?: string;
49
+ }
50
+
51
+ export interface ResolvedQqHttpConfig {
52
+ readonly context: 'qq';
53
+ readonly mode: 'webhook' | 'middleware';
54
+ readonly name: string;
55
+ readonly appid: string;
56
+ readonly secret: string;
57
+ readonly webhookPath: string;
58
+ readonly sandbox: boolean;
59
+ }
60
+
61
+ export type ResolvedQqConfig = ResolvedQqWebsocketConfig | ResolvedQqHttpConfig;
62
+
63
+ export type QqChannelKind = 'private' | 'group' | 'channel' | 'direct';
64
+
65
+ export interface QqInboundMessage {
66
+ readonly id: string;
67
+ readonly content: string;
68
+ readonly channelKind: QqChannelKind;
69
+ readonly channelId: string;
70
+ readonly authorId: string;
71
+ readonly authorName: string;
72
+ readonly authorRoles?: string[];
73
+ readonly timestamp: number;
74
+ readonly guildId?: string;
75
+ readonly rawMessage?: string;
76
+ /**
77
+ * 入站即 @ 机器人(AT_MESSAGE_CREATE 的 mentions[].bot,或群 GROUP_AT_MESSAGE_CREATE
78
+ * 事件本身仅 @ 时下发)。新 Runtime 纯文本 content 需经 metadata 传递。
79
+ */
80
+ readonly mentioned?: boolean;
81
+ }
82
+
83
+ export interface QqWireSegment {
84
+ readonly type: string;
85
+ readonly data?: Record<string, unknown>;
86
+ }
87
+
88
+ export interface ParsedSendTarget {
89
+ readonly kind: QqChannelKind;
90
+ readonly id: string;
91
+ }
92
+
93
+ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig {
94
+ const entry = config.endpoints?.find((item) => item.context === 'qq' || !item.context);
95
+ const appid = pickCredential(config.appid, entry?.appid, process.env.QQ_APPID, process.env.QQ_BOT_APPID);
96
+ const secret = pickCredential(config.secret, entry?.secret, process.env.QQ_SECRET, process.env.QQ_BOT_SECRET);
97
+ if (!appid || !secret) {
98
+ throw new TypeError(
99
+ 'QQ adapter requires appid + secret (plugins.<key>.appid/secret or endpoints with context: qq)',
100
+ );
101
+ }
102
+ const name = (typeof config.name === 'string' && config.name)
103
+ || (typeof entry?.name === 'string' && entry.name)
104
+ || process.env.QQ_BOT_NAME
105
+ || 'qq-bot';
106
+ const mode = config.mode ?? entry?.mode ?? 'websocket';
107
+
108
+ if (mode === 'webhook' || mode === 'middleware') {
109
+ const webhookPathRaw = config.webhookPath ?? entry?.webhookPath ?? '/qq/webhook';
110
+ const webhookPath = webhookPathRaw.startsWith('/') ? webhookPathRaw : `/${webhookPathRaw}`;
111
+ return {
112
+ context: 'qq',
113
+ mode,
114
+ name,
115
+ appid,
116
+ secret,
117
+ webhookPath,
118
+ sandbox: config.sandbox === true || entry?.sandbox === true,
119
+ };
120
+ }
121
+
122
+ return {
123
+ context: 'qq',
124
+ mode: 'websocket',
125
+ name,
126
+ appid,
127
+ secret,
128
+ sandbox: config.sandbox === true || entry?.sandbox === true,
129
+ intents: config.intents ?? entry?.intents,
130
+ accessTokenUrl: config.accessTokenUrl ?? entry?.accessTokenUrl,
131
+ gatewayUrl: config.gatewayUrl ?? entry?.gatewayUrl,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
137
+ */
138
+ export function formatInboundTarget(msg: QqInboundMessage): string {
139
+ return `${msg.channelKind}:${msg.channelId}`;
140
+ }
141
+
142
+ export function parseSendTarget(target: string): ParsedSendTarget {
143
+ const sep = target.indexOf(':');
144
+ if (sep <= 0) {
145
+ return { kind: 'private', id: target };
146
+ }
147
+ const head = target.slice(0, sep);
148
+ const rest = target.slice(sep + 1);
149
+ if (head === 'private' || head === 'group' || head === 'channel' || head === 'direct') {
150
+ return { kind: head, id: rest };
151
+ }
152
+ return { kind: 'private', id: target };
153
+ }
154
+
155
+ export function senderDisplayName(msg: QqInboundMessage): string {
156
+ return msg.authorName || msg.authorId;
157
+ }
158
+
159
+ export function formatInboundContent(msg: QqInboundMessage): string {
160
+ const text = (msg.content || msg.rawMessage || '').trim();
161
+ return text || '(Empty message)';
162
+ }
163
+
164
+ /**
165
+ * Wire-encode an already-rendered outbound payload into a plain text / QQ Sendable string.
166
+ * Segment canonicalization is intentionally not done here.
167
+ */
168
+ export function formatOutboundText(payload: unknown): string {
169
+ if (typeof payload === 'string') return payload;
170
+
171
+ const segments: Array<string | QqWireSegment> = Array.isArray(payload)
172
+ ? payload as Array<string | QqWireSegment>
173
+ : payload && typeof payload === 'object' && 'type' in (payload as object)
174
+ ? [payload as QqWireSegment]
175
+ : [];
176
+
177
+ if (segments.length === 0) {
178
+ return payload == null
179
+ ? ''
180
+ : typeof payload === 'object'
181
+ ? JSON.stringify(payload)
182
+ : String(payload);
183
+ }
184
+
185
+ return segments
186
+ .map((item) => {
187
+ if (typeof item === 'string') return item;
188
+ const data = item.data ?? {};
189
+ switch (item.type) {
190
+ case 'text':
191
+ return String(data.text ?? data.content ?? '');
192
+ case 'at': {
193
+ const id = String(data.user_id ?? data.qq ?? data.id ?? '');
194
+ return id ? `<@!${id}>` : '';
195
+ }
196
+ case 'image':
197
+ return String(data.url || data.file || '');
198
+ case 'reply':
199
+ return '';
200
+ default:
201
+ return data.text != null ? String(data.text) : '';
202
+ }
203
+ })
204
+ .filter(Boolean)
205
+ .join('');
206
+ }
207
+
208
+ /** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
209
+ export function resolveOutboundMessageId(result: unknown): string {
210
+ if (!result || typeof result !== 'object') {
211
+ throw new Error('QQ 发送消息失败:响应为空');
212
+ }
213
+ const row = result as Record<string, unknown>;
214
+ const nested = row.data && typeof row.data === 'object'
215
+ ? row.data as Record<string, unknown>
216
+ : undefined;
217
+ const audit = (row.message_audit ?? nested?.message_audit) as Record<string, unknown> | undefined;
218
+ const id = row.id ?? row.message_id ?? audit?.audit_id;
219
+ if (id == null || id === '') {
220
+ const code = row.code;
221
+ const msg = row.message;
222
+ throw new Error(
223
+ code != null
224
+ ? `QQ 发送消息失败(${String(code)})${msg ? `: ${String(msg)}` : ''}`
225
+ : 'QQ 发送消息失败:响应缺少消息 ID',
226
+ );
227
+ }
228
+ return String(id);
229
+ }