@zhin.js/adapter-qq 1.1.17 → 1.1.18

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 (76) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -5
  3. package/adapters/qq/index.js +7 -2
  4. package/adapters/qq/index.ts +7 -2
  5. package/agents/qq/boundaries.md +2 -0
  6. package/agents/qq/conversation.js +92 -0
  7. package/agents/qq/conversation.ts +99 -0
  8. package/agents/qq/messages.js +60 -0
  9. package/agents/qq/messages.ts +60 -0
  10. package/agents/qq/skills/qq/SKILL.md +8 -2
  11. package/agents/qq/skills/qq/tools/guild_kick_member/index.js +30 -0
  12. package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +29 -0
  13. package/agents/qq/skills/qq/tools/guild_mute_member/index.js +31 -0
  14. package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +30 -0
  15. package/agents/qq/skills/qq-conversation/SKILL.md +24 -0
  16. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +78 -0
  17. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +76 -0
  18. package/agents/qq/skills/qq-group-admin/SKILL.md +34 -0
  19. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +39 -0
  20. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +38 -0
  21. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +33 -0
  22. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +32 -0
  23. package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.js +25 -0
  24. package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.ts +25 -0
  25. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +37 -0
  26. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +36 -0
  27. package/agents/qq/skills/qq-group-directory/SKILL.md +24 -0
  28. package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +16 -0
  29. package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +16 -0
  30. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +19 -0
  31. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +19 -0
  32. package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +19 -0
  33. package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +19 -0
  34. package/agents/qq/skills/qq-group-state/SKILL.md +21 -0
  35. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +19 -0
  36. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +19 -0
  37. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +19 -0
  38. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +19 -0
  39. package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +21 -0
  40. package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +21 -0
  41. package/agents/qq/skills/qq-messages/SKILL.md +27 -0
  42. package/agents/qq/skills/qq-messages/tools/quote_reply/index.js +21 -0
  43. package/agents/qq/skills/qq-messages/tools/quote_reply/index.ts +21 -0
  44. package/agents/qq/skills/qq-messages/tools/recall_own_message/index.js +18 -0
  45. package/agents/qq/skills/qq-messages/tools/recall_own_message/index.ts +17 -0
  46. package/commands/qq/endpoint/definition.js +35 -22
  47. package/commands/qq/endpoint/definition.ts +37 -22
  48. package/lib/console.d.ts +8 -0
  49. package/lib/console.js +84 -0
  50. package/lib/endpoint.d.ts +10 -1
  51. package/lib/endpoint.js +134 -8
  52. package/lib/group-admin.d.ts +36 -0
  53. package/lib/group-admin.js +113 -0
  54. package/lib/protocol.d.ts +5 -0
  55. package/lib/protocol.js +4 -0
  56. package/lib/qq-console-spec.d.ts +17 -0
  57. package/lib/qq-console-spec.js +10 -0
  58. package/lib/qq-console.d.ts +11 -0
  59. package/lib/qq-console.js +116 -0
  60. package/lib/qq-endpoint-write.d.ts +22 -0
  61. package/lib/qq-endpoint-write.js +34 -0
  62. package/lib/qq-runtime-state.d.ts +19 -0
  63. package/lib/qq-runtime-state.js +1 -0
  64. package/package.json +17 -9
  65. package/pages/index/index.js +15 -0
  66. package/pages/index/index.ts +16 -0
  67. package/plugin.js +5 -2
  68. package/schema.json +174 -0
  69. package/src/console.ts +96 -0
  70. package/src/endpoint.ts +137 -8
  71. package/src/group-admin.ts +153 -0
  72. package/src/protocol.ts +8 -0
  73. package/src/qq-console-spec.ts +18 -0
  74. package/src/qq-console.ts +124 -0
  75. package/src/qq-endpoint-write.ts +52 -0
  76. package/src/qq-runtime-state.ts +21 -0
@@ -0,0 +1,19 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { currentGroup } from '../../../../conversation.js';
4
+
5
+ export default defineAgentTool({
6
+ description: '获取当前 QQ 群中指定成员的信息(昵称、角色、是否机器人、入群时间);官方白名单能力',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ permissions: ['role(master,admin,trusted,owner)'],
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ inputSchema: z.object({
13
+ user_id: z.string().min(1).describe('目标成员的 member_openid(来自消息发送者或成员列表,不要猜测)'),
14
+ }),
15
+ async execute({ user_id }, context) {
16
+ const member = await context.$client.api.groupService.getMemberInfo(currentGroup(context), user_id);
17
+ return { success: true, member };
18
+ },
19
+ });
@@ -0,0 +1,19 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { currentGroup } from '../../../../conversation.js';
5
+ export default defineAgentTool({
6
+ description: '分页列出当前 QQ 群成员(每页最多 30 人,含 member_openid、昵称、角色);官方白名单能力',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ permissions: ['role(master,admin,trusted,owner)'],
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ inputSchema: z.object({
13
+ cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
+ }),
15
+ async execute({ cursor }, context) {
16
+ const page = await context.$client.api.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
17
+ return { success: true, members: page.members ?? [], count: page.members?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
+ },
19
+ });
@@ -0,0 +1,19 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { currentGroup } from '../../../../conversation.js';
4
+
5
+ export default defineAgentTool({
6
+ description: '分页列出当前 QQ 群成员(每页最多 30 人,含 member_openid、昵称、角色);官方白名单能力',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ permissions: ['role(master,admin,trusted,owner)'],
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ inputSchema: z.object({
13
+ cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
+ }),
15
+ async execute({ cursor }, context) {
16
+ const page = await context.$client.api.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
17
+ return { success: true, members: page.members ?? [], count: page.members?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
+ },
19
+ });
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: qq-group-state
3
+ platforms:
4
+ - qq
5
+ description: 当前 QQ 群的禁言状态、待处理入群申请与黑名单查询。
6
+ keywords:
7
+ - 禁言列表
8
+ - 入群申请
9
+ - 黑名单
10
+ tools:
11
+ - list_muted
12
+ - list_join_requests
13
+ - group_blacklist
14
+ scopes:
15
+ - group
16
+ ---
17
+ # qq-group-state
18
+
19
+ Read-only views of the current group. `list_muted` returns the global mute rule and members whose mute has not expired. `list_join_requests` and `group_blacklist` are paged invite-only APIs; error 11253 means the bot has no access.
20
+
21
+ Act on a join request with `qq-group-admin` → `handle_join_request`, after confirming the applicant. Group files are not exposed by the official API.
@@ -0,0 +1,19 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { currentGroup } from '../../../../conversation.js';
5
+ export default defineAgentTool({
6
+ description: '分页查询当前 QQ 群的成员黑名单;官方白名单能力',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ permissions: ['role(master,admin,trusted,owner)'],
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ inputSchema: z.object({
13
+ cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
+ }),
15
+ async execute({ cursor }, context) {
16
+ const page = await context.$client.api.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
17
+ return { users: page.users ?? [], count: page.users?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
+ },
19
+ });
@@ -0,0 +1,19 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { currentGroup } from '../../../../conversation.js';
4
+
5
+ export default defineAgentTool({
6
+ description: '分页查询当前 QQ 群的成员黑名单;官方白名单能力',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ permissions: ['role(master,admin,trusted,owner)'],
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ inputSchema: z.object({
13
+ cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
+ }),
15
+ async execute({ cursor }, context) {
16
+ const page = await context.$client.api.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
17
+ return { users: page.users ?? [], count: page.users?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
+ },
19
+ });
@@ -0,0 +1,19 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { currentGroup } from '../../../../conversation.js';
5
+ export default defineAgentTool({
6
+ description: '分页列出当前 QQ 群待处理的入群申请(含验证信息、风险提示);官方白名单能力',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ permissions: ['role(master,admin,trusted,owner)'],
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ inputSchema: z.object({
13
+ cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
+ }),
15
+ async execute({ cursor }, context) {
16
+ const page = await context.$client.api.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
17
+ return { requests: page.list ?? [], count: page.list?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
+ },
19
+ });
@@ -0,0 +1,19 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { currentGroup } from '../../../../conversation.js';
4
+
5
+ export default defineAgentTool({
6
+ description: '分页列出当前 QQ 群待处理的入群申请(含验证信息、风险提示);官方白名单能力',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ permissions: ['role(master,admin,trusted,owner)'],
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ inputSchema: z.object({
13
+ cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
+ }),
15
+ async execute({ cursor }, context) {
16
+ const page = await context.$client.api.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
17
+ return { requests: page.list ?? [], count: page.list?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
+ },
19
+ });
@@ -0,0 +1,21 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { currentGroup } from '../../../../conversation.js';
5
+ export default defineAgentTool({
6
+ description: '查询当前 QQ 群的禁言状态:全员禁言规则与被禁言成员列表',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ requiresApproval: 'never',
10
+ readOnly: true,
11
+ inputSchema: z.object({}),
12
+ async execute(_input, context) {
13
+ const setting = await context.$client.api.groupService.getMuteSetting(currentGroup(context));
14
+ const now = Date.now();
15
+ const muted = (setting.members ?? []).filter((m) => {
16
+ const until = Date.parse(m.mute_expire_at);
17
+ return !Number.isFinite(until) || until > now;
18
+ });
19
+ return { global_rule: setting.global_rule, muted_members: muted, count: muted.length };
20
+ },
21
+ });
@@ -0,0 +1,21 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { currentGroup } from '../../../../conversation.js';
4
+
5
+ export default defineAgentTool({
6
+ description: '查询当前 QQ 群的禁言状态:全员禁言规则与被禁言成员列表',
7
+ adapter: 'qq',
8
+ scopes: ['group'],
9
+ requiresApproval: 'never',
10
+ readOnly: true,
11
+ inputSchema: z.object({}),
12
+ async execute(_input, context) {
13
+ const setting = await context.$client.api.groupService.getMuteSetting(currentGroup(context));
14
+ const now = Date.now();
15
+ const muted = (setting.members ?? []).filter((m) => {
16
+ const until = Date.parse(m.mute_expire_at);
17
+ return !Number.isFinite(until) || until > now;
18
+ });
19
+ return { global_rule: setting.global_rule, muted_members: muted, count: muted.length };
20
+ },
21
+ });
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: qq-messages
3
+ platforms:
4
+ - qq
5
+ description: QQ 官方机器人当前会话的引用回复,以及撤回机器人自己的发言。
6
+ keywords:
7
+ - 引用
8
+ - 回复原消息
9
+ - 撤回
10
+ - 自己的消息
11
+ tools:
12
+ - quote_reply
13
+ - recall_own_message
14
+ scopes:
15
+ - private
16
+ - group
17
+ - channel
18
+ ---
19
+ # qq-messages
20
+
21
+ Only use real message IDs from the current conversation. Do not invent IDs or source authors. Both tools stay inside the current conversation; cross-conversation forwarding does not exist on the official API.
22
+
23
+ `quote_reply` sends a text reply that references one original (passive reply `msg_id` / guild `message_reference`) through the unified outbound pipeline. Use `inspect_conversation_reference` to read an incoming quote when available; it does not send messages.
24
+
25
+ `recall_own_message` accepts only the bot's own send receipt ID and needs no management role or approval. QQ only recalls messages younger than 2 minutes; never promise success before confirmation or retry an unconfirmed result. To recall another member's message, load `qq-group-admin`.
26
+
27
+ Merged forward (`forward_messages`) is not available: the official API cannot send merged-forward records.
@@ -0,0 +1,21 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { resolveMessageTarget, sendToConversation } from '../../../../conversation.js';
5
+ export default defineAgentTool({
6
+ description: '引用当前 QQ 会话中的真实消息并回复文字(引用即被动回复的 msg_id / message_reference);发送走统一链路',
7
+ adapter: 'qq',
8
+ scopes: ['private', 'group', 'channel'],
9
+ requiresApproval: 'never',
10
+ inputSchema: z.object({
11
+ message_id: z.string().min(1).describe('当前会话中真实的原消息 ID,或消息头里的 m<n> 别名'),
12
+ text: z.string().min(1).describe('本次回复的文字'),
13
+ }),
14
+ async execute({ message_id, text }, context) {
15
+ const target = resolveMessageTarget(context, message_id);
16
+ return sendToConversation(context, [
17
+ { type: 'reply', data: { message_id: target.messageId } },
18
+ { type: 'text', data: { text } },
19
+ ]);
20
+ },
21
+ });
@@ -0,0 +1,21 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { resolveMessageTarget, sendToConversation } from '../../../../conversation.js';
4
+
5
+ export default defineAgentTool({
6
+ description: '引用当前 QQ 会话中的真实消息并回复文字(引用即被动回复的 msg_id / message_reference);发送走统一链路',
7
+ adapter: 'qq',
8
+ scopes: ['private', 'group', 'channel'],
9
+ requiresApproval: 'never',
10
+ inputSchema: z.object({
11
+ message_id: z.string().min(1).describe('当前会话中真实的原消息 ID,或消息头里的 m<n> 别名'),
12
+ text: z.string().min(1).describe('本次回复的文字'),
13
+ }),
14
+ async execute({ message_id, text }, context) {
15
+ const target = resolveMessageTarget(context, message_id);
16
+ return sendToConversation(context, [
17
+ { type: 'reply', data: { message_id: target.messageId } },
18
+ { type: 'text', data: { text } },
19
+ ]);
20
+ },
21
+ });
@@ -0,0 +1,18 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { recallTarget, resolveMessageTarget } from '../../../../conversation.js';
5
+ import { messages as t } from '../../../../messages.js';
6
+ export default defineAgentTool({
7
+ description: '撤回机器人在当前 QQ 会话中自己发送的消息;无需群管角色或逐次审批,官方只允许撤回 2 分钟内的消息',
8
+ adapter: 'qq',
9
+ scopes: ['private', 'group', 'channel'],
10
+ requiresApproval: 'never',
11
+ inputSchema: z.object({ message_id: z.string().min(1).describe('机器人在当前会话中发送的消息 ID(发送回执),或消息头里的 m<n> 别名') }),
12
+ async execute({ message_id }, context) {
13
+ const target = resolveMessageTarget(context, message_id);
14
+ if (!target.ownOutbound)
15
+ return { success: false, message: t(context.locale, 'ownMessageRequired') };
16
+ return recallTarget(context, target);
17
+ },
18
+ });
@@ -0,0 +1,17 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { recallTarget, resolveMessageTarget } from '../../../../conversation.js';
4
+ import { messages as t } from '../../../../messages.js';
5
+
6
+ export default defineAgentTool({
7
+ description: '撤回机器人在当前 QQ 会话中自己发送的消息;无需群管角色或逐次审批,官方只允许撤回 2 分钟内的消息',
8
+ adapter: 'qq',
9
+ scopes: ['private', 'group', 'channel'],
10
+ requiresApproval: 'never',
11
+ inputSchema: z.object({ message_id: z.string().min(1).describe('机器人在当前会话中发送的消息 ID(发送回执),或消息头里的 m<n> 别名') }),
12
+ async execute({ message_id }, context) {
13
+ const target = resolveMessageTarget(context, message_id);
14
+ if (!target.ownOutbound) return { success: false, message: t(context.locale, 'ownMessageRequired') };
15
+ return recallTarget(context, target);
16
+ },
17
+ });
@@ -8,11 +8,12 @@ import { messages as t } from "../../../lib/i18n.js";
8
8
  * 扫码绑定 add 流程(经套件 bindFlow 钩子接入)与 cancel,并导出
9
9
  * qqEndpointCommands 供 commands/qq/endpoint/ 下的命令文件默认导出。
10
10
  */
11
- import { buildEndpointEnvKey, createEndpointCommands, endpointCommandForbidden, endpointConfigurationStoreToken, extractEndpointCommandReply, formatEndpointList, isEndpointOperator, removeEndpointById, } from 'zhin.js/adapter';
11
+ import { createEndpointCommands, endpointCommandForbidden, endpointConfigurationStoreToken, extractEndpointCommandReply, formatEndpointList, isEndpointOperator, removeEndpointById, } from 'zhin.js/adapter';
12
12
  import { defineCommand } from 'zhin.js/command';
13
13
  import { qqBotKindPrompt, qqCommandSessionKey } from "../../../lib/qq-bot-kind-prompt.js";
14
14
  import { startQqBindFlow } from "../../../lib/qq-bind-flow.js";
15
- import { defaultQqEndpointIntentFields } from "../../../lib/qq-intents.js";
15
+ import { writeQqEndpointWithBotKind } from "../../../lib/qq-endpoint-write.js";
16
+ import { cancelQqConsoleBind } from "../../../lib/qq-console.js";
16
17
  import { qqRuntimeStateToken, } from "../../../lib/qq-runtime-state.js";
17
18
  export { qqRuntimeStateToken };
18
19
  /**
@@ -21,7 +22,17 @@ export { qqRuntimeStateToken };
21
22
  * legacy applyBindOwnership 会把 operator 写为新端点的 master)。
22
23
  */
23
24
  export function isQqEndpointOperator(config, input) {
24
- return isEndpointOperator(config, input);
25
+ if (isEndpointOperator(config, input))
26
+ return true;
27
+ // The outage-notify target is the operator's conversation on another adapter.
28
+ const cfg = (config ?? {});
29
+ const notifyIds = [
30
+ cfg.notify?.id,
31
+ ...(Array.isArray(cfg.endpoints) ? cfg.endpoints.map((endpoint) => endpoint.notify?.id) : []),
32
+ ];
33
+ const senderRaw = input?.sender;
34
+ const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? String(senderRaw.id).trim() : '';
35
+ return senderId !== '' && notifyIds.some((id) => id != null && String(id) === senderId);
25
36
  }
26
37
  export const QQ_ENDPOINT_FORBIDDEN = endpointCommandForbidden('QQ');
27
38
  /**
@@ -36,6 +47,9 @@ function busyFooter(state, locale) {
36
47
  if (state.pendingBotKind) {
37
48
  return t(locale, 'kindBusy', { id: state.pendingBotKind.endpointId });
38
49
  }
50
+ if (state.consoleBind?.state === 'scanned') {
51
+ return t(locale, 'kindBusy', { id: state.consoleBind.pending?.endpointId ?? 'QQ' });
52
+ }
39
53
  return undefined;
40
54
  }
41
55
  /** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
@@ -52,25 +66,18 @@ const qqEndpointListSpec = {
52
66
  describeEntry: (entry) => `appid: ${String(entry.appid)}`,
53
67
  };
54
68
  async function writeEndpointWithBotKind(pending, botKind, store) {
55
- const locale = pending.locale;
56
- const appidKey = buildEndpointEnvKey('qq', pending.endpointId, 'appid');
57
- const secretKey = buildEndpointEnvKey('qq', pending.endpointId, 'secret');
58
- const intentFields = defaultQqEndpointIntentFields(botKind);
59
- const { filePath } = await store.add({
60
- adapterKey: 'qq',
61
- entry: {
62
- id: pending.endpointId,
63
- appid: `\${${appidKey}}`,
64
- secret: `\${${secretKey}}`,
65
- botKind: intentFields.botKind,
66
- intents: [...intentFields.intents],
67
- },
68
- environment: {
69
- [appidKey]: pending.appId,
70
- [secretKey]: pending.appSecret,
71
- },
69
+ const result = await writeQqEndpointWithBotKind({
70
+ endpointId: pending.endpointId,
71
+ appId: pending.appId,
72
+ appSecret: pending.appSecret,
73
+ botKind,
74
+ }, store);
75
+ return t(pending.locale, 'endpointSaved', {
76
+ kind: result.botKind,
77
+ id: result.endpointId,
78
+ file: result.filePath,
79
+ intents: result.intents.join(', '),
72
80
  });
73
- return t(locale, 'endpointSaved', { kind: intentFields.botKind, id: pending.endpointId, file: filePath, intents: intentFields.intents.join(', ') });
74
81
  }
75
82
  /**
76
83
  * 完成 pending 的公域/私域选择:一次性写 .env + yaml。
@@ -86,9 +93,10 @@ export async function completeQqPendingBotKind(pending, botKind, store) {
86
93
  * 扫码成功后凭据暂存内存并询问公域/私域;确认后一次性写 .env + yaml。
87
94
  */
88
95
  export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN', cancelCommand, commandHeads) {
89
- if (state.bindFlow || state.pendingBotKind) {
96
+ if (state.bindFlow || state.pendingBotKind || state.consoleBind?.pending || state.consoleBind?.state === 'waiting') {
90
97
  return Promise.resolve(t(locale, 'existingFlow'));
91
98
  }
99
+ state.consoleBind = null;
92
100
  const endpointId = id?.trim() || undefined;
93
101
  const sessionKey = qqCommandSessionKey(input);
94
102
  return new Promise((resolve) => {
@@ -159,6 +167,7 @@ export function runQqEndpointCancel(state, preference) {
159
167
  if (state.bindFlow) {
160
168
  state.bindFlow.stop();
161
169
  state.bindFlow = null;
170
+ cancelQqConsoleBind(state);
162
171
  return t(locale, 'cancelled');
163
172
  }
164
173
  if (state.pendingBotKind) {
@@ -166,6 +175,10 @@ export function runQqEndpointCancel(state, preference) {
166
175
  state.pendingBotKind = null;
167
176
  return t(locale, 'pendingCancelled', { name });
168
177
  }
178
+ if (state.consoleBind) {
179
+ cancelQqConsoleBind(state);
180
+ return t(locale, 'cancelled');
181
+ }
169
182
  return t(locale, 'noFlow');
170
183
  }
171
184
  /** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
@@ -8,7 +8,6 @@ import { messages as t } from '../../../src/i18n.js';
8
8
  * qqEndpointCommands 供 commands/qq/endpoint/ 下的命令文件默认导出。
9
9
  */
10
10
  import {
11
- buildEndpointEnvKey,
12
11
  createEndpointCommands,
13
12
  endpointCommandForbidden,
14
13
  endpointConfigurationStoreToken,
@@ -23,7 +22,9 @@ import {
23
22
  import { defineCommand } from 'zhin.js/command';
24
23
  import { qqBotKindPrompt, qqCommandSessionKey } from '../../../src/qq-bot-kind-prompt.js';
25
24
  import { startQqBindFlow } from '../../../src/qq-bind-flow.js';
26
- import { defaultQqEndpointIntentFields, type QqBotKind } from '../../../src/qq-intents.js';
25
+ import { writeQqEndpointWithBotKind } from '../../../src/qq-endpoint-write.js';
26
+ import type { QqBotKind } from '../../../src/qq-intents.js';
27
+ import { cancelQqConsoleBind } from '../../../src/qq-console.js';
27
28
  import {
28
29
  qqRuntimeStateToken,
29
30
  type QqPendingBotKind,
@@ -40,7 +41,19 @@ export type QqCommandReply = EndpointCommandReply;
40
41
  * legacy applyBindOwnership 会把 operator 写为新端点的 master)。
41
42
  */
42
43
  export function isQqEndpointOperator(config: unknown, input: unknown): boolean {
43
- return isEndpointOperator(config, input);
44
+ if (isEndpointOperator(config, input)) return true;
45
+ // The outage-notify target is the operator's conversation on another adapter.
46
+ const cfg = (config ?? {}) as {
47
+ notify?: { id?: unknown };
48
+ endpoints?: readonly { notify?: { id?: unknown } }[];
49
+ };
50
+ const notifyIds = [
51
+ cfg.notify?.id,
52
+ ...(Array.isArray(cfg.endpoints) ? cfg.endpoints.map((endpoint) => endpoint.notify?.id) : []),
53
+ ];
54
+ const senderRaw = (input as { sender?: { id?: unknown } } | null | undefined)?.sender;
55
+ const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? String(senderRaw.id).trim() : '';
56
+ return senderId !== '' && notifyIds.some((id) => id != null && String(id) === senderId);
44
57
  }
45
58
 
46
59
  export const QQ_ENDPOINT_FORBIDDEN = endpointCommandForbidden('QQ');
@@ -57,6 +70,9 @@ function busyFooter(state: QqRuntimeState, locale?: Locale): string | undefined
57
70
  if (state.pendingBotKind) {
58
71
  return t(locale, 'kindBusy', { id: state.pendingBotKind.endpointId });
59
72
  }
73
+ if (state.consoleBind?.state === 'scanned') {
74
+ return t(locale, 'kindBusy', { id: state.consoleBind.pending?.endpointId ?? 'QQ' });
75
+ }
60
76
  return undefined;
61
77
  }
62
78
 
@@ -84,25 +100,18 @@ async function writeEndpointWithBotKind(
84
100
  botKind: QqBotKind,
85
101
  store: EndpointConfigurationStore,
86
102
  ): Promise<string> {
87
- const locale = pending.locale;
88
- const appidKey = buildEndpointEnvKey('qq', pending.endpointId, 'appid');
89
- const secretKey = buildEndpointEnvKey('qq', pending.endpointId, 'secret');
90
- const intentFields = defaultQqEndpointIntentFields(botKind);
91
- const { filePath } = await store.add({
92
- adapterKey: 'qq',
93
- entry: {
94
- id: pending.endpointId,
95
- appid: `\${${appidKey}}`,
96
- secret: `\${${secretKey}}`,
97
- botKind: intentFields.botKind,
98
- intents: [...intentFields.intents],
99
- },
100
- environment: {
101
- [appidKey]: pending.appId,
102
- [secretKey]: pending.appSecret,
103
- },
103
+ const result = await writeQqEndpointWithBotKind({
104
+ endpointId: pending.endpointId,
105
+ appId: pending.appId,
106
+ appSecret: pending.appSecret,
107
+ botKind,
108
+ }, store);
109
+ return t(pending.locale, 'endpointSaved', {
110
+ kind: result.botKind,
111
+ id: result.endpointId,
112
+ file: result.filePath,
113
+ intents: result.intents.join(', '),
104
114
  });
105
- return t(locale, 'endpointSaved', { kind: intentFields.botKind, id: pending.endpointId, file: filePath, intents: intentFields.intents.join(', ') });
106
115
  }
107
116
 
108
117
  /**
@@ -133,11 +142,12 @@ export function runQqEndpointAdd(
133
142
  cancelCommand?: string,
134
143
  commandHeads?: readonly string[],
135
144
  ): Promise<string> {
136
- if (state.bindFlow || state.pendingBotKind) {
145
+ if (state.bindFlow || state.pendingBotKind || state.consoleBind?.pending || state.consoleBind?.state === 'waiting') {
137
146
  return Promise.resolve(
138
147
  t(locale, 'existingFlow'),
139
148
  );
140
149
  }
150
+ state.consoleBind = null;
141
151
  const endpointId = id?.trim() || undefined;
142
152
  const sessionKey = qqCommandSessionKey(input);
143
153
  return new Promise<string>((resolve) => {
@@ -222,6 +232,7 @@ export function runQqEndpointCancel(state: QqRuntimeState, preference?: Locale):
222
232
  if (state.bindFlow) {
223
233
  state.bindFlow.stop();
224
234
  state.bindFlow = null;
235
+ cancelQqConsoleBind(state);
225
236
  return t(locale, 'cancelled');
226
237
  }
227
238
  if (state.pendingBotKind) {
@@ -229,6 +240,10 @@ export function runQqEndpointCancel(state: QqRuntimeState, preference?: Locale):
229
240
  state.pendingBotKind = null;
230
241
  return t(locale, 'pendingCancelled', { name });
231
242
  }
243
+ if (state.consoleBind) {
244
+ cancelQqConsoleBind(state);
245
+ return t(locale, 'cancelled');
246
+ }
232
247
  return t(locale, 'noFlow');
233
248
  }
234
249
 
@@ -0,0 +1,8 @@
1
+ /**
2
+ * QQ Console RPC:除通用 endpoint 管理外,把 `qq endpoint add` 的扫码绑定
3
+ * (以及公域 / 私域选择、取消)搬到 Console 页面。
4
+ */
5
+ import { type AdapterConsoleResources } from 'zhin.js/adapter';
6
+ import { type PluginConsoleRpc } from 'zhin.js';
7
+ import type { QqRuntimeState } from './qq-runtime-state.js';
8
+ export declare function createQqConsoleRpc(state: QqRuntimeState, resources: AdapterConsoleResources, getConfig: () => unknown): PluginConsoleRpc;
package/lib/console.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * QQ Console RPC:除通用 endpoint 管理外,把 `qq endpoint add` 的扫码绑定
3
+ * (以及公域 / 私域选择、取消)搬到 Console 页面。
4
+ */
5
+ import { endpointConfigurationStoreToken, createAdapterConsoleRpc, } from 'zhin.js/adapter';
6
+ import { PluginConsoleRpcError } from 'zhin.js';
7
+ import { qqConsoleSpec } from './qq-console-spec.js';
8
+ import { cancelQqConsoleBind, completeQqConsoleBind, startQqConsoleBind, } from './qq-console.js';
9
+ export function createQqConsoleRpc(state, resources, getConfig) {
10
+ const store = resources.has(endpointConfigurationStoreToken)
11
+ ? resources.use(endpointConfigurationStoreToken)
12
+ : undefined;
13
+ const spec = {
14
+ ...qqConsoleSpec,
15
+ running: () => state.endpoints.values(),
16
+ getConfig,
17
+ extraStatus: () => {
18
+ const bind = state.consoleBind;
19
+ if (!bind) {
20
+ return { bindState: state.bindFlow ? 'binding' : 'idle' };
21
+ }
22
+ return {
23
+ bindState: bind.state,
24
+ ...(bind.url ? { scanUrl: bind.url } : {}),
25
+ ...(bind.message ? { bindMessage: bind.message } : {}),
26
+ ...(bind.pending ? { bindEndpoint: bind.pending.endpointId } : {}),
27
+ ...(bind.error ? { bindError: bind.error } : {}),
28
+ };
29
+ },
30
+ availableActions: () => {
31
+ if (!store)
32
+ return [];
33
+ if (state.consoleBind?.pending)
34
+ return ['qq-bind-kind', 'qq-bind-cancel'];
35
+ if (state.bindFlow || state.consoleBind?.state === 'waiting')
36
+ return ['qq-bind-cancel'];
37
+ return ['qq-bind-start'];
38
+ },
39
+ actions: {
40
+ 'qq-bind-start': {
41
+ label: '扫码绑定新机器人',
42
+ labelI18n: { en: 'Bind a bot with QR' },
43
+ description: '用手机 QQ 扫码,成功后选择公域 / 私域,再一次性写入 .env 与配置。',
44
+ descriptionI18n: { en: 'Scan with mobile QQ, choose public/private, then save credentials to .env and the config.' },
45
+ handler: async () => {
46
+ if (!store)
47
+ throw new PluginConsoleRpcError('unsupported', '当前运行环境不支持写入 endpoint 配置');
48
+ const { url } = await startQqConsoleBind(state);
49
+ return { message: '请用手机 QQ 扫码;扫码成功后选择公域或私域', url };
50
+ },
51
+ },
52
+ 'qq-bind-kind': {
53
+ label: '选择公域 / 私域',
54
+ labelI18n: { en: 'Choose public/private' },
55
+ description: '公域订阅 PUBLIC_GUILD_MESSAGES;私域订阅 GUILD_MESSAGES。',
56
+ descriptionI18n: { en: 'Public subscribes PUBLIC_GUILD_MESSAGES; private subscribes GUILD_MESSAGES.' },
57
+ options: [
58
+ { value: 'public', label: '公域', labelI18n: { en: 'Public' } },
59
+ { value: 'private', label: '私域', labelI18n: { en: 'Private' } },
60
+ ],
61
+ handler: async (input) => {
62
+ if (!store)
63
+ throw new PluginConsoleRpcError('unsupported', '当前运行环境不支持写入 endpoint 配置');
64
+ const result = await completeQqConsoleBind(state, store, input.value);
65
+ return {
66
+ message: `已写入 endpoint「${result.endpointId}」(${result.botKind}),重启或热重载后生效`,
67
+ details: { endpoint: result.endpointId, botKind: result.botKind, intents: result.intents.join(', ') },
68
+ };
69
+ },
70
+ },
71
+ 'qq-bind-cancel': {
72
+ label: '取消扫码绑定',
73
+ labelI18n: { en: 'Cancel QR binding' },
74
+ description: '中止正在进行的扫码绑定;已写入的配置不会删除。',
75
+ descriptionI18n: { en: 'Stop the current QR binding flow; already-saved configuration is untouched.' },
76
+ handler: async () => {
77
+ const cancelled = cancelQqConsoleBind(state);
78
+ return { message: cancelled ? '已取消绑定' : '当前没有进行中的绑定流程' };
79
+ },
80
+ },
81
+ },
82
+ };
83
+ return createAdapterConsoleRpc(spec, resources);
84
+ }