@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +75 -409
  3. package/adapters/qq.ts +51 -0
  4. package/agent/tools/add_role.ts +2 -2
  5. package/agent/tools/channel_info.ts +2 -2
  6. package/agent/tools/create_role.ts +2 -2
  7. package/agent/tools/list_channels.ts +2 -2
  8. package/agent/tools/list_guilds.ts +2 -2
  9. package/agent/tools/list_roles.ts +2 -2
  10. package/agent/tools/member_detail.ts +2 -2
  11. package/agent/tools/remove_role.ts +2 -2
  12. package/commands/endpoint/add/[name:string=].ts +21 -0
  13. package/commands/endpoint/cancel.ts +15 -0
  14. package/commands/endpoint/list.ts +10 -0
  15. package/commands/endpoint/remove/[name:string].ts +15 -0
  16. package/lib/endpoint.d.ts +75 -0
  17. package/lib/endpoint.js +358 -0
  18. package/lib/index.d.ts +6 -0
  19. package/lib/index.js +6 -0
  20. package/lib/platform-permit.d.ts +16 -0
  21. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  22. package/lib/protocol.d.ts +96 -0
  23. package/lib/protocol.js +129 -0
  24. package/lib/qq-agent-deps.d.ts +21 -0
  25. package/lib/qq-agent-deps.js +30 -0
  26. package/lib/qq-bind-api.d.ts +36 -0
  27. package/lib/{src/qq-bind-api.js → qq-bind-api.js} +0 -1
  28. package/lib/qq-bind-flow.d.ts +19 -0
  29. package/lib/{src/qq-bind-flow.js → qq-bind-flow.js} +0 -1
  30. package/lib/qq-bind-persist.d.ts +12 -0
  31. package/lib/{src/qq-bind-persist.js → qq-bind-persist.js} +4 -5
  32. package/lib/qq-endpoint-commands.d.ts +25 -0
  33. package/lib/qq-endpoint-commands.js +147 -0
  34. package/lib/qq-endpoint-config.d.ts +17 -0
  35. package/lib/qq-endpoint-config.js +110 -0
  36. package/lib/qq-runtime-state.d.ts +17 -0
  37. package/lib/qq-runtime-state.js +13 -0
  38. package/lib/webhook.d.ts +18 -0
  39. package/lib/webhook.js +100 -0
  40. package/lib/ws.d.ts +24 -0
  41. package/lib/ws.js +118 -0
  42. package/package.json +51 -40
  43. package/plugin.ts +16 -0
  44. package/schema.json +85 -0
  45. package/src/endpoint.ts +362 -596
  46. package/src/index.ts +56 -186
  47. package/src/platform-permit.ts +1 -1
  48. package/src/protocol.ts +229 -0
  49. package/src/qq-agent-deps.ts +49 -12
  50. package/src/qq-bind-persist.ts +4 -4
  51. package/src/qq-endpoint-commands.ts +174 -0
  52. package/src/qq-endpoint-config.ts +135 -0
  53. package/src/qq-runtime-state.ts +36 -0
  54. package/src/webhook.ts +127 -0
  55. package/src/ws.ts +172 -0
  56. package/client/Dashboard.tsx +0 -230
  57. package/client/index.tsx +0 -11
  58. package/client/tsconfig.json +0 -7
  59. package/client/utils/api.ts +0 -30
  60. package/dist/index.js +0 -30
  61. package/lib/agent/tools/add_role.js +0 -23
  62. package/lib/agent/tools/add_role.js.map +0 -1
  63. package/lib/agent/tools/channel_info.js +0 -19
  64. package/lib/agent/tools/channel_info.js.map +0 -1
  65. package/lib/agent/tools/create_role.js +0 -22
  66. package/lib/agent/tools/create_role.js.map +0 -1
  67. package/lib/agent/tools/list_channels.js +0 -20
  68. package/lib/agent/tools/list_channels.js.map +0 -1
  69. package/lib/agent/tools/list_guilds.js +0 -17
  70. package/lib/agent/tools/list_guilds.js.map +0 -1
  71. package/lib/agent/tools/list_roles.js +0 -20
  72. package/lib/agent/tools/list_roles.js.map +0 -1
  73. package/lib/agent/tools/member_detail.js +0 -20
  74. package/lib/agent/tools/member_detail.js.map +0 -1
  75. package/lib/agent/tools/remove_role.js +0 -23
  76. package/lib/agent/tools/remove_role.js.map +0 -1
  77. package/lib/package.json +0 -105
  78. package/lib/src/adapter.js +0 -81
  79. package/lib/src/adapter.js.map +0 -1
  80. package/lib/src/endpoint.js +0 -600
  81. package/lib/src/endpoint.js.map +0 -1
  82. package/lib/src/gateway-config.js +0 -32
  83. package/lib/src/gateway-config.js.map +0 -1
  84. package/lib/src/group-at-normalize.js +0 -68
  85. package/lib/src/group-at-normalize.js.map +0 -1
  86. package/lib/src/inbound-action.js +0 -57
  87. package/lib/src/inbound-action.js.map +0 -1
  88. package/lib/src/inbound-normalize.js +0 -47
  89. package/lib/src/inbound-normalize.js.map +0 -1
  90. package/lib/src/index.js +0 -194
  91. package/lib/src/index.js.map +0 -1
  92. package/lib/src/mention-enrich.js +0 -52
  93. package/lib/src/mention-enrich.js.map +0 -1
  94. package/lib/src/outbound-keyboard.js +0 -100
  95. package/lib/src/outbound-keyboard.js.map +0 -1
  96. package/lib/src/outbound-markdown.js +0 -189
  97. package/lib/src/outbound-markdown.js.map +0 -1
  98. package/lib/src/outbound-media.js +0 -51
  99. package/lib/src/outbound-media.js.map +0 -1
  100. package/lib/src/outbound-mixed-media.js +0 -78
  101. package/lib/src/outbound-mixed-media.js.map +0 -1
  102. package/lib/src/platform-permit.js.map +0 -1
  103. package/lib/src/qq-agent-deps.js +0 -10
  104. package/lib/src/qq-agent-deps.js.map +0 -1
  105. package/lib/src/qq-bind-api.js.map +0 -1
  106. package/lib/src/qq-bind-flow.js.map +0 -1
  107. package/lib/src/qq-bind-persist.js.map +0 -1
  108. package/lib/src/qq-endpoint-manager.js +0 -159
  109. package/lib/src/qq-endpoint-manager.js.map +0 -1
  110. package/lib/src/sdk-version.js +0 -19
  111. package/lib/src/sdk-version.js.map +0 -1
  112. package/lib/src/segment-mapper.js +0 -2
  113. package/lib/src/segment-mapper.js.map +0 -1
  114. package/lib/src/types.js +0 -2
  115. package/lib/src/types.js.map +0 -1
  116. package/plugin.yml +0 -3
  117. package/src/adapter.ts +0 -97
  118. package/src/gateway-config.ts +0 -48
  119. package/src/group-at-normalize.ts +0 -75
  120. package/src/inbound-action.ts +0 -87
  121. package/src/inbound-normalize.ts +0 -57
  122. package/src/mention-enrich.ts +0 -71
  123. package/src/outbound-keyboard.ts +0 -117
  124. package/src/outbound-markdown.ts +0 -212
  125. package/src/outbound-media.ts +0 -58
  126. package/src/outbound-mixed-media.ts +0 -101
  127. package/src/qq-endpoint-manager.ts +0 -199
  128. package/src/sdk-version.ts +0 -21
  129. package/src/segment-mapper.ts +0 -1
  130. package/src/types.ts +0 -52
@@ -0,0 +1,110 @@
1
+ /**
2
+ * zhin.config.yml 中 plugins.qq.endpoints 的读写(Plugin Runtime 版)。
3
+ * 用 yaml Document 操作以保留现有注释;仅支持 .yml/.yaml,其它格式报错提示手动处理。
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { isMap, isSeq, parseDocument } from 'yaml';
8
+ import { resolveProjectRoot } from './qq-bind-persist.js';
9
+ const CONFIG_BASENAME = 'zhin.config';
10
+ const YAML_EXTENSIONS = ['.yml', '.yaml'];
11
+ /** 定位项目配置文件:ZHIN_CONFIG 指定优先,否则发现 zhin.config.yml/.yaml,都没有则默认新建 zhin.config.yml */
12
+ export function findQqConfigFile(projectRoot) {
13
+ const root = projectRoot ?? resolveProjectRoot();
14
+ const envConfig = process.env.ZHIN_CONFIG?.trim();
15
+ if (envConfig)
16
+ return path.resolve(root, envConfig);
17
+ for (const ext of YAML_EXTENSIONS) {
18
+ const candidate = path.join(root, `${CONFIG_BASENAME}${ext}`);
19
+ if (fs.existsSync(candidate))
20
+ return candidate;
21
+ }
22
+ for (const ext of ['.json', '.toml', '.ts']) {
23
+ const candidate = path.join(root, `${CONFIG_BASENAME}${ext}`);
24
+ if (fs.existsSync(candidate)) {
25
+ throw new Error(`暂不支持写入 ${ext} 配置文件,请手动在 ${CONFIG_BASENAME}${ext} 的 plugins.qq.endpoints 中维护`);
26
+ }
27
+ }
28
+ return path.join(root, `${CONFIG_BASENAME}.yml`);
29
+ }
30
+ function readConfigDocument(projectRoot) {
31
+ const filePath = findQqConfigFile(projectRoot);
32
+ const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
33
+ const doc = parseDocument(content || '{}');
34
+ return { filePath, doc };
35
+ }
36
+ function writeConfigDocument({ filePath, doc }) {
37
+ fs.writeFileSync(filePath, doc.toString());
38
+ }
39
+ /** 读取 plugins.qq.endpoints(plain JS);plugins/qq 缺失或形态不符时返回 [] */
40
+ export function listQqEndpointEntries(projectRoot) {
41
+ const { doc } = readConfigDocument(projectRoot);
42
+ const plugins = doc.toJS()?.plugins;
43
+ if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins))
44
+ return [];
45
+ const endpoints = plugins.qq?.endpoints;
46
+ if (!Array.isArray(endpoints))
47
+ return [];
48
+ return endpoints.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.name === 'string');
49
+ }
50
+ function entryName(item) {
51
+ if (!isMap(item))
52
+ return undefined;
53
+ const name = item.get('name');
54
+ return typeof name === 'string' && name ? name : undefined;
55
+ }
56
+ /**
57
+ * 确保 plugins.qq.endpoints 存在并返回其 YAMLSeq(节点级操作,保留既有条目与注释)。
58
+ * `plugins: []`(legacy 空列表,Runtime 忽略)可直接替换为 map;非空数组拒绝写入。
59
+ */
60
+ function ensureEndpointsSeq(doc) {
61
+ const plugins = doc.get('plugins');
62
+ if (isSeq(plugins) && plugins.items.length > 0) {
63
+ throw new Error('配置的 plugins 是数组形态(legacy 插件名列表),请手动迁移为 map 后再试');
64
+ }
65
+ if (plugins !== undefined && !isMap(plugins) && !isSeq(plugins)) {
66
+ throw new Error('配置的 plugins 字段形态异常,请手动检查 zhin.config.yml');
67
+ }
68
+ if (!isMap(doc.get('plugins'))) {
69
+ // 注意:空对象 {} 不会被 doc.set 自动包装为 YAMLMap,必须显式 createNode
70
+ doc.set('plugins', doc.createNode({}));
71
+ }
72
+ const qq = doc.getIn(['plugins', 'qq']);
73
+ if (qq !== undefined && !isMap(qq)) {
74
+ throw new Error('配置的 plugins.qq 字段形态异常,请手动检查 zhin.config.yml');
75
+ }
76
+ if (!isMap(doc.getIn(['plugins', 'qq']))) {
77
+ doc.setIn(['plugins', 'qq'], doc.createNode({}));
78
+ }
79
+ const endpoints = doc.getIn(['plugins', 'qq', 'endpoints']);
80
+ if (endpoints !== undefined && !isSeq(endpoints)) {
81
+ throw new Error('配置的 plugins.qq.endpoints 字段形态异常,请手动检查 zhin.config.yml');
82
+ }
83
+ if (!isSeq(doc.getIn(['plugins', 'qq', 'endpoints']))) {
84
+ doc.setIn(['plugins', 'qq', 'endpoints'], doc.createNode([]));
85
+ }
86
+ return doc.getIn(['plugins', 'qq', 'endpoints']);
87
+ }
88
+ /** 追加 endpoint 到 plugins.qq.endpoints;name 已存在时报错 */
89
+ export function addQqEndpointToConfig(entry, projectRoot) {
90
+ const document = readConfigDocument(projectRoot);
91
+ const seq = ensureEndpointsSeq(document.doc);
92
+ if (seq.items.some((item) => entryName(item) === entry.name)) {
93
+ throw new Error(`配置中已存在 qq endpoint「${entry.name}」,可先 qq endpoint remove ${entry.name} 再重新添加`);
94
+ }
95
+ seq.items.push(document.doc.createNode(entry));
96
+ writeConfigDocument(document);
97
+ return document.filePath;
98
+ }
99
+ /** 按 name 移除 plugins.qq.endpoints 项;不存在返回 false */
100
+ export function removeQqEndpointFromConfig(name, projectRoot) {
101
+ const document = readConfigDocument(projectRoot);
102
+ const seq = ensureEndpointsSeq(document.doc);
103
+ const next = seq.items.filter((item) => entryName(item) !== name);
104
+ if (next.length === seq.items.length) {
105
+ return { removed: false, filePath: document.filePath };
106
+ }
107
+ seq.items = next;
108
+ writeConfigDocument(document);
109
+ return { removed: true, filePath: document.filePath };
110
+ }
@@ -0,0 +1,17 @@
1
+ export interface QqRunningEndpoint {
2
+ name: string;
3
+ mode: string;
4
+ }
5
+ export interface QqBindFlowHandle {
6
+ /** 绑定期望的 endpoint 名(未指定时为 undefined,成功后取 appId) */
7
+ name?: string;
8
+ stop: () => void;
9
+ }
10
+ export interface QqRuntimeState {
11
+ /** 当前 generation 已成功创建的 endpoint(name → 描述) */
12
+ readonly endpoints: Map<string, QqRunningEndpoint>;
13
+ /** 全局单例:同一时间只允许一个扫码绑定流程 */
14
+ bindFlow: QqBindFlowHandle | null;
15
+ }
16
+ export declare function createQqRuntimeState(): QqRuntimeState;
17
+ export declare const qqRuntimeStateToken: import("@zhin.js/plugin-runtime").Token<QqRuntimeState>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * QQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表 +
3
+ * 扫码绑定流程的全局单例。由 plugin.ts setup() provide,
4
+ * adapter create 与 commands 通过 use(qqRuntimeStateToken) 共享(同一 owner generation)。
5
+ */
6
+ import { createToken } from '@zhin.js/plugin-runtime';
7
+ export function createQqRuntimeState() {
8
+ return {
9
+ endpoints: new Map(),
10
+ bindFlow: null,
11
+ };
12
+ }
13
+ export const qqRuntimeStateToken = createToken('zhin.qq.runtime-state', 'QQ adapter runtime state (running endpoints + bind flow singleton)');
@@ -0,0 +1,18 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
3
+ import type { ResolvedQqHttpConfig } from './protocol.js';
4
+ import type { QqBotTransport } from './ws.js';
5
+ export type QqHttpBotTransport = QqBotTransport & {
6
+ middleware: (ctx: unknown, next: () => Promise<void>) => Promise<unknown>;
7
+ };
8
+ export type CreateQqHttpBot = (config: ResolvedQqHttpConfig) => QqHttpBotTransport;
9
+ export interface QqWebhookHandler {
10
+ readonly config: ResolvedQqHttpConfig;
11
+ getBot(): (QqHttpBotTransport & {
12
+ middleware?: QqHttpBotTransport['middleware'];
13
+ }) | null;
14
+ }
15
+ export declare function registerQqWebhookRoutes(http: HttpHost, handler: QqWebhookHandler): HttpRouteRegistration[];
16
+ export declare function handleQqWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: QqWebhookHandler): Promise<void>;
17
+ export declare function readRequestBodyText(request: IncomingMessage): Promise<string>;
18
+ export declare function defaultCreateHttpBot(config: ResolvedQqHttpConfig): QqHttpBotTransport;
package/lib/webhook.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * QQ HTTP webhook / middleware inbound via httpHostToken POST.
3
+ */
4
+ import path from 'node:path';
5
+ import { Bot, ReceiverMode } from 'qq-official-bot';
6
+ import { getLogger } from '@zhin.js/logger';
7
+ const logger = getLogger('qq');
8
+ export function registerQqWebhookRoutes(http, handler) {
9
+ const webhookPath = handler.config.webhookPath;
10
+ return [
11
+ http.route('POST', webhookPath, async (request, response) => {
12
+ await handleQqWebhookRequest(request, response, handler);
13
+ }, { summary: 'QQ webhook callback', tags: ['qq'] }),
14
+ ];
15
+ }
16
+ export async function handleQqWebhookRequest(request, response, handler) {
17
+ const bot = handler.getBot();
18
+ if (!bot?.middleware) {
19
+ response.writeHead(503, { 'Content-Type': 'text/plain' });
20
+ response.end('QQ receiver not ready');
21
+ return;
22
+ }
23
+ try {
24
+ let parsedBody;
25
+ const raw = await readRequestBodyText(request);
26
+ if (raw) {
27
+ try {
28
+ parsedBody = JSON.parse(raw);
29
+ }
30
+ catch {
31
+ parsedBody = undefined;
32
+ }
33
+ }
34
+ await bot.middleware({
35
+ req: request,
36
+ res: response,
37
+ request: { body: parsedBody },
38
+ }, async () => undefined);
39
+ }
40
+ catch (error) {
41
+ logger.error('QQ webhook error:', error);
42
+ if (!response.headersSent) {
43
+ response.writeHead(500, { 'Content-Type': 'text/plain' });
44
+ response.end('Internal Server Error');
45
+ }
46
+ }
47
+ }
48
+ export async function readRequestBodyText(request) {
49
+ const chunks = [];
50
+ let size = 0;
51
+ for await (const chunk of request) {
52
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
53
+ size += buffer.length;
54
+ if (size > 1_048_576) {
55
+ request.destroy();
56
+ throw new Error('Request body exceeds 1MB');
57
+ }
58
+ chunks.push(buffer);
59
+ }
60
+ return Buffer.concat(chunks).toString('utf8');
61
+ }
62
+ export function defaultCreateHttpBot(config) {
63
+ const bot = new Bot({
64
+ appid: config.appid,
65
+ secret: config.secret,
66
+ mode: ReceiverMode.MIDDLEWARE,
67
+ platform: 'koa',
68
+ sandbox: config.sandbox,
69
+ dataDir: path.join(process.cwd(), 'data', 'qq'),
70
+ });
71
+ return {
72
+ start: () => bot.start().then(() => undefined),
73
+ stop: () => bot.stop().then(() => undefined),
74
+ on: (event, listener) => {
75
+ bot.on(event, listener);
76
+ },
77
+ removeAllListeners: () => {
78
+ bot.removeAllListeners(undefined);
79
+ },
80
+ sendPrivateMessage: (userId, message) => bot.sendPrivateMessage(userId, message),
81
+ sendGroupMessage: (groupId, message) => bot.sendGroupMessage(groupId, message),
82
+ sendGuildMessage: (channelId, message) => bot.sendGuildMessage(channelId, message),
83
+ sendDirectMessage: (guildId, message) => bot.sendDirectMessage(guildId, message),
84
+ getGuilds: () => bot.guildService.getList(),
85
+ getChannels: (guildId) => bot.channelService.getList(guildId),
86
+ getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
87
+ getGuildMember: (guildId, userId) => bot.memberService.getGuildMemberInfo(guildId, userId),
88
+ getGuildRoles: (guildId) => bot.guildService.getRoles(guildId),
89
+ createGuildRole: (guildId, name, color) => bot.guildService.createRole(guildId, { name, color: color || 0, hoist: 0 }),
90
+ addMemberRole: async (guildId, channelId, userId, roleId) => {
91
+ await bot.memberService.addMemberRole(guildId, channelId, userId, roleId);
92
+ return true;
93
+ },
94
+ removeMemberRole: async (guildId, channelId, userId, roleId) => {
95
+ await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
96
+ return true;
97
+ },
98
+ middleware: (ctx, next) => bot.middleware(ctx, next),
99
+ };
100
+ }
package/lib/ws.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { QqInboundMessage, ResolvedQqWebsocketConfig } from './protocol.js';
2
+ /** Minimal bot surface used by the endpoint (real qq-official-bot or test mock). */
3
+ export interface QqBotTransport {
4
+ start(): Promise<void>;
5
+ stop(): Promise<void>;
6
+ on(event: string, listener: (...args: unknown[]) => void): void;
7
+ removeAllListeners(): void;
8
+ sendPrivateMessage(userId: string, message: string): Promise<unknown>;
9
+ sendGroupMessage(groupId: string, message: string): Promise<unknown>;
10
+ sendGuildMessage(channelId: string, message: string): Promise<unknown>;
11
+ sendDirectMessage?(guildId: string, message: string): Promise<unknown>;
12
+ getGuilds(): Promise<unknown[]>;
13
+ getChannels(guildId: string): Promise<unknown[]>;
14
+ getChannelInfo(channelId: string): Promise<unknown>;
15
+ getGuildMember(guildId: string, userId: string): Promise<unknown>;
16
+ getGuildRoles(guildId: string): Promise<unknown[]>;
17
+ createGuildRole(guildId: string, name: string, color?: number): Promise<unknown>;
18
+ addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
19
+ removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
20
+ }
21
+ export type CreateQqBot = (config: ResolvedQqWebsocketConfig) => QqBotTransport;
22
+ export declare function bindQqBotInboundEvents(bot: QqBotTransport, admitRaw: (raw: unknown) => void): void;
23
+ export declare function normalizeQqMessage(raw: unknown): QqInboundMessage | null;
24
+ export declare function defaultCreateBot(config: ResolvedQqWebsocketConfig): QqBotTransport;
package/lib/ws.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * QQ WebSocket transport: bot factory and inbound message normalization.
3
+ */
4
+ import path from 'node:path';
5
+ import { Bot, ReceiverMode } from 'qq-official-bot';
6
+ import { getLevel, toLog4jsLevel } from '@zhin.js/logger';
7
+ export function bindQqBotInboundEvents(bot, admitRaw) {
8
+ bot.on('message.group', admitRaw);
9
+ bot.on('message.guild', admitRaw);
10
+ bot.on('message.private', admitRaw);
11
+ }
12
+ export function normalizeQqMessage(raw) {
13
+ if (!raw || typeof raw !== 'object')
14
+ return null;
15
+ const msg = raw;
16
+ if (msg.message_id == null)
17
+ return null;
18
+ let channelKind = 'private';
19
+ let channelId = '';
20
+ if (msg.message_type === 'guild') {
21
+ channelKind = msg.sub_type === 'direct' ? 'direct' : 'channel';
22
+ channelId = channelKind === 'direct'
23
+ ? String(msg.guild_id ?? '')
24
+ : String(msg.channel_id ?? '');
25
+ }
26
+ else if (msg.message_type === 'group') {
27
+ channelKind = 'group';
28
+ channelId = String(msg.group_id ?? msg.group_openid ?? '');
29
+ }
30
+ else {
31
+ channelKind = 'private';
32
+ channelId = String(msg.user_id ?? msg.author?.user_openid ?? msg.author?.id ?? '');
33
+ }
34
+ if (!channelId)
35
+ return null;
36
+ const authorId = String(msg.sender?.user_id
37
+ ?? msg.user_id
38
+ ?? msg.author?.member_openid
39
+ ?? msg.author?.user_openid
40
+ ?? msg.author?.id
41
+ ?? '');
42
+ const content = extractTextContent(msg.message) || msg.raw_message || '';
43
+ const rawRoles = msg.member?.roles ?? msg.sender?.roles;
44
+ const authorRoles = Array.isArray(rawRoles) ? rawRoles.map(String) : undefined;
45
+ // 群 GROUP_AT_MESSAGE_CREATE 仅 @ 机器人时下发;频道 AT 事件看 mentions[].bot
46
+ const mentioned = channelKind === 'group'
47
+ || (Array.isArray(msg.mentions) && msg.mentions.some((m) => m?.bot === true));
48
+ return {
49
+ id: String(msg.message_id),
50
+ content,
51
+ channelKind,
52
+ channelId,
53
+ authorId,
54
+ authorName: msg.sender?.user_name || msg.author?.username || authorId,
55
+ authorRoles,
56
+ timestamp: Date.now(),
57
+ guildId: msg.guild_id != null ? String(msg.guild_id) : undefined,
58
+ rawMessage: msg.raw_message,
59
+ ...(mentioned ? { mentioned: true } : {}),
60
+ };
61
+ }
62
+ function extractTextContent(message) {
63
+ if (typeof message === 'string')
64
+ return message;
65
+ if (!Array.isArray(message))
66
+ return '';
67
+ return message
68
+ .map((seg) => {
69
+ if (!seg || typeof seg !== 'object')
70
+ return '';
71
+ const item = seg;
72
+ if (item.type === 'text')
73
+ return String(item.data?.text ?? item.text ?? '');
74
+ return '';
75
+ })
76
+ .join('');
77
+ }
78
+ export function defaultCreateBot(config) {
79
+ const bot = new Bot({
80
+ appid: config.appid,
81
+ secret: config.secret,
82
+ mode: ReceiverMode.WEBSOCKET,
83
+ sandbox: config.sandbox,
84
+ dataDir: path.join(process.cwd(), 'data', 'qq'),
85
+ logLevel: toLog4jsLevel(getLevel()),
86
+ ...(config.intents ? { intents: config.intents } : {}),
87
+ ...(config.accessTokenUrl ? { accessTokenUrl: config.accessTokenUrl } : {}),
88
+ ...(config.gatewayUrl ? { gatewayUrl: config.gatewayUrl } : {}),
89
+ });
90
+ return {
91
+ start: () => bot.start().then(() => undefined),
92
+ stop: () => bot.stop().then(() => undefined),
93
+ on: (event, listener) => {
94
+ bot.on(event, listener);
95
+ },
96
+ removeAllListeners: () => {
97
+ bot.removeAllListeners(undefined);
98
+ },
99
+ sendPrivateMessage: (userId, message) => bot.sendPrivateMessage(userId, message),
100
+ sendGroupMessage: (groupId, message) => bot.sendGroupMessage(groupId, message),
101
+ sendGuildMessage: (channelId, message) => bot.sendGuildMessage(channelId, message),
102
+ sendDirectMessage: (guildId, message) => bot.sendDirectMessage(guildId, message),
103
+ getGuilds: () => bot.guildService.getList(),
104
+ getChannels: (guildId) => bot.channelService.getList(guildId),
105
+ getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
106
+ getGuildMember: (guildId, userId) => bot.memberService.getGuildMemberInfo(guildId, userId),
107
+ getGuildRoles: (guildId) => bot.guildService.getRoles(guildId),
108
+ createGuildRole: (guildId, name, color) => bot.guildService.createRole(guildId, { name, color: color || 0, hoist: 0 }),
109
+ addMemberRole: async (guildId, channelId, userId, roleId) => {
110
+ await bot.memberService.addMemberRole(guildId, channelId, userId, roleId);
111
+ return true;
112
+ },
113
+ removeMemberRole: async (guildId, channelId, userId, roleId) => {
114
+ await bot.memberService.removeMemberRole(guildId, channelId, userId, roleId);
115
+ return true;
116
+ },
117
+ };
118
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-qq",
3
- "version": "5.0.2",
4
- "description": "Zhin.js adapter for QQ Official Bot",
3
+ "version": "6.0.0",
4
+ "description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -34,59 +34,52 @@
34
34
  "directory": "plugins/adapters/qq"
35
35
  },
36
36
  "dependencies": {
37
- "qq-official-bot": "^1.2.3"
38
- },
39
- "devDependencies": {
40
- "@types/react": "^19.2.17",
41
- "@types/react-dom": "^19.2.3",
42
- "lucide-react": "^1.22.0",
43
- "typescript": "^6.0.3",
44
- "zod": "^4.4.3",
45
- "@zhin.js/cli": "1.0.93",
46
- "@zhin.js/client": "2.0.5",
47
- "@zhin.js/contract": "1.0.3",
48
- "@zhin.js/host-api": "2.0.5",
49
- "@zhin.js/host-router": "2.0.3",
50
- "@zhin.js/logger": "1.0.74",
51
- "zhin.js": "4.1.2",
52
- "@zhin.js/agent": "1.0.3"
37
+ "qq-official-bot": "^1.2.3",
38
+ "yaml": "^2.9.0",
39
+ "@zhin.js/adapter": "1.1.0",
40
+ "@zhin.js/core": "1.4.0",
41
+ "@zhin.js/host-http": "1.0.2",
42
+ "@zhin.js/logger": "1.0.75",
43
+ "@zhin.js/plugin-runtime": "1.1.0",
44
+ "@zhin.js/command": "1.0.2"
53
45
  },
54
46
  "peerDependencies": {
55
47
  "zod": "^4.0.0",
56
- "@zhin.js/client": "2.0.5",
57
- "@zhin.js/host-api": "2.0.5",
58
- "@zhin.js/contract": "1.0.3",
59
- "@zhin.js/host-router": "2.0.3",
60
- "@zhin.js/logger": "1.0.74",
61
- "zhin.js": "4.1.2",
62
- "@zhin.js/agent": "1.0.3"
48
+ "@zhin.js/adapter": "1.1.0",
49
+ "@zhin.js/agent": "1.0.5",
50
+ "@zhin.js/core": "1.4.0",
51
+ "@zhin.js/plugin-runtime": "1.1.0",
52
+ "zhin.js": "5.0.0"
63
53
  },
64
54
  "peerDependenciesMeta": {
65
- "@zhin.js/agent": {
66
- "optional": true
67
- },
68
- "@zhin.js/client": {
55
+ "zhin.js": {
69
56
  "optional": true
70
57
  },
71
- "@zhin.js/host-router": {
72
- "optional": true
73
- },
74
- "@zhin.js/host-api": {
58
+ "@zhin.js/agent": {
75
59
  "optional": true
76
60
  },
77
61
  "zod": {
78
62
  "optional": true
79
63
  }
80
64
  },
65
+ "devDependencies": {
66
+ "@types/node": "^26.1.0",
67
+ "typescript": "^6.0.3",
68
+ "vitest": "^4.1.10",
69
+ "zod": "^4.4.3",
70
+ "@zhin.js/agent": "1.0.5",
71
+ "@zhin.js/host-http": "1.0.2",
72
+ "zhin.js": "5.0.0"
73
+ },
81
74
  "files": [
75
+ "adapters",
76
+ "commands",
77
+ "plugin.ts",
78
+ "schema.json",
82
79
  "src",
83
80
  "lib",
84
- "client",
85
- "dist",
86
81
  "agent",
87
- "plugin.yml",
88
82
  "README.md",
89
- "node",
90
83
  "CHANGELOG.md"
91
84
  ],
92
85
  "publishConfig": {
@@ -96,9 +89,27 @@
96
89
  "engines": {
97
90
  "node": "^20.19.0 || >=22.12.0"
98
91
  },
92
+ "zhin": {
93
+ "protocol": 1,
94
+ "type": "plugin",
95
+ "entry": "./plugin.ts",
96
+ "engine": "^1.0.0",
97
+ "runtime": "trusted",
98
+ "features": [
99
+ {
100
+ "package": "@zhin.js/adapter",
101
+ "api": "^1.0.0"
102
+ },
103
+ {
104
+ "package": "@zhin.js/command",
105
+ "api": "^1.0.0"
106
+ }
107
+ ],
108
+ "plugins": []
109
+ },
99
110
  "scripts": {
100
- "build": "zhin build",
101
- "clean": "rimraf lib dist",
102
- "build:node": "tsc"
111
+ "build": "tsc",
112
+ "clean": "rimraf lib",
113
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/qq/tests"
103
114
  }
104
115
  }
package/plugin.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { definePlugin } from '@zhin.js/plugin-runtime';
2
+ import { registerQqPlatformPermitChecker } from './src/platform-permit.js';
3
+ import { createQqRuntimeState, qqRuntimeStateToken } from './src/qq-runtime-state.js';
4
+
5
+ export default definePlugin({
6
+ name: 'qq',
7
+ metadata: {
8
+ displayName: 'QQ Official WebSocket Adapter',
9
+ },
10
+ setup(context) {
11
+ // 运行中 endpoint 注册表 + 扫码绑定单例,供 adapter create 与 `qq endpoint` 命令共享
12
+ context.resources.provide(qqRuntimeStateToken, createQqRuntimeState());
13
+ // 平台权限门禁:guild_owner / guild_admin / manage_roles / manage_channels(agent 工具 platformPermit)
14
+ return registerQqPlatformPermitChecker();
15
+ },
16
+ });
package/schema.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "mode": {
7
+ "type": "string",
8
+ "enum": [
9
+ "websocket",
10
+ "webhook",
11
+ "middleware"
12
+ ],
13
+ "default": "websocket",
14
+ "description": "WebSocket gateway (default). webhook/middleware use httpHostToken POST."
15
+ },
16
+ "sandbox": {
17
+ "type": "boolean",
18
+ "default": false
19
+ },
20
+ "master": {
21
+ "type": ["string", "number"],
22
+ "description": "Endpoint Owner(qq endpoint 管理命令权限、AI master 角色);endpoints[i] 可逐项覆盖"
23
+ },
24
+ "intents": {
25
+ "type": "array",
26
+ "items": {
27
+ "type": "string"
28
+ }
29
+ },
30
+ "accessTokenUrl": {
31
+ "type": "string"
32
+ },
33
+ "gatewayUrl": {
34
+ "type": "string"
35
+ },
36
+ "webhookPath": {
37
+ "type": "string",
38
+ "default": "/qq/webhook",
39
+ "description": "POST path on httpHostToken for webhook/middleware modes."
40
+ },
41
+ "port": {
42
+ "type": "number",
43
+ "description": "Legacy standalone webhook port (unused with httpHostToken)."
44
+ },
45
+ "path": {
46
+ "type": "string",
47
+ "description": "Legacy standalone webhook path (unused with httpHostToken)."
48
+ },
49
+ "endpoints": {
50
+ "type": "array",
51
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
52
+ "items": {
53
+ "type": "object",
54
+ "additionalProperties": true,
55
+ "properties": {
56
+ "name": {
57
+ "type": "string",
58
+ "description": "QQ bot name"
59
+ },
60
+ "appid": {
61
+ "type": "string",
62
+ "description": "QQ official bot app ID"
63
+ },
64
+ "secret": {
65
+ "type": "string",
66
+ "description": "QQ official bot secret"
67
+ }
68
+ },
69
+ "required": [
70
+ "name",
71
+ "appid",
72
+ "secret"
73
+ ]
74
+ }
75
+ },
76
+ "commandPrefix": {
77
+ "type": "string",
78
+ "default": "",
79
+ "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
80
+ }
81
+ },
82
+ "required": [
83
+ "endpoints"
84
+ ]
85
+ }