@zhin.js/adapter-qq 1.1.18 → 1.1.19

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 (102) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +21 -18
  3. package/adapters/qq/index.js +4 -38
  4. package/adapters/qq/index.ts +3 -46
  5. package/agents/qq/conversation.js +1 -1
  6. package/agents/qq/conversation.ts +1 -1
  7. package/agents/qq/skills/qq/tools/add_role/index.js +2 -1
  8. package/agents/qq/skills/qq/tools/add_role/index.ts +2 -1
  9. package/agents/qq/skills/qq/tools/channel_info/index.js +1 -1
  10. package/agents/qq/skills/qq/tools/channel_info/index.ts +1 -1
  11. package/agents/qq/skills/qq/tools/create_role/index.js +1 -1
  12. package/agents/qq/skills/qq/tools/create_role/index.ts +1 -1
  13. package/agents/qq/skills/qq/tools/guild_kick_member/index.js +1 -1
  14. package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +1 -1
  15. package/agents/qq/skills/qq/tools/guild_mute_member/index.js +1 -1
  16. package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +1 -1
  17. package/agents/qq/skills/qq/tools/list_channels/index.js +1 -1
  18. package/agents/qq/skills/qq/tools/list_channels/index.ts +1 -1
  19. package/agents/qq/skills/qq/tools/list_roles/index.js +1 -1
  20. package/agents/qq/skills/qq/tools/list_roles/index.ts +1 -1
  21. package/agents/qq/skills/qq/tools/member_detail/index.js +1 -1
  22. package/agents/qq/skills/qq/tools/member_detail/index.ts +1 -1
  23. package/agents/qq/skills/qq/tools/remove_role/index.js +2 -1
  24. package/agents/qq/skills/qq/tools/remove_role/index.ts +2 -1
  25. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +1 -1
  26. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +1 -1
  27. package/agents/qq/skills/qq-directory/tools/list_guilds/index.js +1 -1
  28. package/agents/qq/skills/qq-directory/tools/list_guilds/index.ts +1 -1
  29. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +2 -2
  30. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +2 -2
  31. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +1 -1
  32. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +1 -1
  33. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +2 -2
  34. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +2 -2
  35. package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +1 -1
  36. package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +1 -1
  37. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +1 -1
  38. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +1 -1
  39. package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +1 -1
  40. package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +1 -1
  41. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +1 -1
  42. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +1 -1
  43. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +1 -1
  44. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +1 -1
  45. package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +1 -1
  46. package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +1 -1
  47. package/commands/qq/endpoint/definition.js +41 -36
  48. package/commands/qq/endpoint/definition.ts +41 -67
  49. package/lib/adapter.d.ts +13 -0
  50. package/lib/adapter.js +27 -0
  51. package/lib/client.d.ts +7 -9
  52. package/lib/client.js +1 -2
  53. package/lib/console.d.ts +1 -6
  54. package/lib/console.js +29 -20
  55. package/lib/endpoint.d.ts +8 -27
  56. package/lib/endpoint.js +218 -165
  57. package/lib/group-admin.d.ts +14 -9
  58. package/lib/group-admin.js +21 -20
  59. package/lib/i18n.js +4 -4
  60. package/lib/image-size.d.ts +6 -1
  61. package/lib/image-size.js +2 -3
  62. package/lib/index.d.ts +5 -4
  63. package/lib/index.js +3 -3
  64. package/lib/interaction.js +1 -1
  65. package/lib/outbound.d.ts +4 -1
  66. package/lib/outbound.js +3 -2
  67. package/lib/protocol.d.ts +2 -0
  68. package/lib/qq-console-spec.js +12 -1
  69. package/lib/qq-console.d.ts +3 -4
  70. package/lib/qq-console.js +16 -6
  71. package/lib/qq-endpoint-write.d.ts +6 -10
  72. package/lib/qq-endpoint-write.js +5 -32
  73. package/lib/qq-runtime-state.d.ts +11 -10
  74. package/lib/qq-runtime-state.js +4 -5
  75. package/lib/qq-send.d.ts +7 -14
  76. package/lib/qq-send.js +20 -6
  77. package/lib/webhook.d.ts +3 -6
  78. package/lib/webhook.js +6 -49
  79. package/lib/ws.d.ts +10 -33
  80. package/lib/ws.js +21 -70
  81. package/middlewares/bot-kind-reply/index.js +1 -2
  82. package/middlewares/bot-kind-reply/index.ts +0 -2
  83. package/package.json +19 -19
  84. package/plugin.js +5 -5
  85. package/src/adapter.ts +23 -0
  86. package/src/client.ts +4 -12
  87. package/src/console.ts +27 -20
  88. package/src/endpoint.ts +218 -160
  89. package/src/group-admin.ts +28 -26
  90. package/src/i18n.ts +4 -4
  91. package/src/image-size.ts +7 -3
  92. package/src/index.ts +5 -5
  93. package/src/interaction.ts +1 -1
  94. package/src/outbound.ts +3 -2
  95. package/src/protocol.ts +2 -0
  96. package/src/qq-console-spec.ts +12 -1
  97. package/src/qq-console.ts +14 -8
  98. package/src/qq-endpoint-write.ts +10 -38
  99. package/src/qq-runtime-state.ts +11 -16
  100. package/src/qq-send.ts +28 -12
  101. package/src/webhook.ts +7 -57
  102. package/src/ws.ts +27 -105
@@ -10,7 +10,7 @@ export default defineAgentTool({
10
10
  readOnly: true,
11
11
  inputSchema: z.object({}),
12
12
  async execute(_input, context) {
13
- const info = await context.$client.api.groupService.getInfo(currentGroup(context));
13
+ const info = await context.$client.groupService.getInfo(currentGroup(context));
14
14
  return { success: true, group: info };
15
15
  },
16
16
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  user_id: z.string().min(1).describe('目标成员的 member_openid(来自消息发送者或成员列表,不要猜测)'),
14
14
  }),
15
15
  async execute({ user_id }, context) {
16
- const member = await context.$client.api.groupService.getMemberInfo(currentGroup(context), user_id);
16
+ const member = await context.$client.groupService.getMemberInfo(currentGroup(context), user_id);
17
17
  return { success: true, member };
18
18
  },
19
19
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  user_id: z.string().min(1).describe('目标成员的 member_openid(来自消息发送者或成员列表,不要猜测)'),
14
14
  }),
15
15
  async execute({ user_id }, context) {
16
- const member = await context.$client.api.groupService.getMemberInfo(currentGroup(context), user_id);
16
+ const member = await context.$client.groupService.getMemberInfo(currentGroup(context), user_id);
17
17
  return { success: true, member };
18
18
  },
19
19
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
14
  }),
15
15
  async execute({ cursor }, context) {
16
- const page = await context.$client.api.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
16
+ const page = await context.$client.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
17
17
  return { success: true, members: page.members ?? [], count: page.members?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
18
  },
19
19
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
14
  }),
15
15
  async execute({ cursor }, context) {
16
- const page = await context.$client.api.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
16
+ const page = await context.$client.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
17
17
  return { success: true, members: page.members ?? [], count: page.members?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
18
  },
19
19
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
14
  }),
15
15
  async execute({ cursor }, context) {
16
- const page = await context.$client.api.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
16
+ const page = await context.$client.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
17
17
  return { users: page.users ?? [], count: page.users?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
18
  },
19
19
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
14
  }),
15
15
  async execute({ cursor }, context) {
16
- const page = await context.$client.api.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
16
+ const page = await context.$client.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
17
17
  return { users: page.users ?? [], count: page.users?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
18
  },
19
19
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
14
  }),
15
15
  async execute({ cursor }, context) {
16
- const page = await context.$client.api.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
16
+ const page = await context.$client.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
17
17
  return { requests: page.list ?? [], count: page.list?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
18
  },
19
19
  });
@@ -13,7 +13,7 @@ export default defineAgentTool({
13
13
  cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
14
14
  }),
15
15
  async execute({ cursor }, context) {
16
- const page = await context.$client.api.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
16
+ const page = await context.$client.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
17
17
  return { requests: page.list ?? [], count: page.list?.length ?? 0, next_cursor: page.next_cursor || undefined };
18
18
  },
19
19
  });
@@ -10,7 +10,7 @@ export default defineAgentTool({
10
10
  readOnly: true,
11
11
  inputSchema: z.object({}),
12
12
  async execute(_input, context) {
13
- const setting = await context.$client.api.groupService.getMuteSetting(currentGroup(context));
13
+ const setting = await context.$client.groupService.getMuteSetting(currentGroup(context));
14
14
  const now = Date.now();
15
15
  const muted = (setting.members ?? []).filter((m) => {
16
16
  const until = Date.parse(m.mute_expire_at);
@@ -10,7 +10,7 @@ export default defineAgentTool({
10
10
  readOnly: true,
11
11
  inputSchema: z.object({}),
12
12
  async execute(_input, context) {
13
- const setting = await context.$client.api.groupService.getMuteSetting(currentGroup(context));
13
+ const setting = await context.$client.groupService.getMuteSetting(currentGroup(context));
14
14
  const now = Date.now();
15
15
  const muted = (setting.members ?? []).filter((m) => {
16
16
  const until = Date.parse(m.mute_expire_at);
@@ -3,12 +3,11 @@ import { resolveLocale } from 'zhin.js/i18n';
3
3
  import { messages as t } from "../../../lib/i18n.js";
4
4
  /**
5
5
  * `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
6
- * 通用部分(权限 / list / remove / 配置写回 / .env 持久化)已迁移到
7
- * @zhin.js/adapter 的 createEndpointCommands 套件;本文件保留 QQ 特化的
8
- * 扫码绑定 add 流程(经套件 bindFlow 钩子接入)与 cancel,并导出
6
+ * list / remove 与权限共用 @zhin.js/adapter 的账号管理入口;
7
+ * 本文件保留 QQ 扫码绑定 add 与 cancel,保存委派捕获的所属 Adapter CAS,并导出
9
8
  * qqEndpointCommands 供 commands/qq/endpoint/ 下的命令文件默认导出。
10
9
  */
11
- import { createEndpointCommands, endpointCommandForbidden, endpointConfigurationStoreToken, extractEndpointCommandReply, formatEndpointList, isEndpointOperator, removeEndpointById, } from 'zhin.js/adapter';
10
+ import { createEndpointCommands, endpointCommandForbidden, createDurableEndpointCommandReply, isEndpointOperator, extractEndpointCommandReply, adapterFeatureId, isAdapterIndex } from 'zhin.js/adapter';
12
11
  import { defineCommand } from 'zhin.js/command';
13
12
  import { qqBotKindPrompt, qqCommandSessionKey } from "../../../lib/qq-bot-kind-prompt.js";
14
13
  import { startQqBindFlow } from "../../../lib/qq-bind-flow.js";
@@ -52,47 +51,38 @@ function busyFooter(state, locale) {
52
51
  }
53
52
  return undefined;
54
53
  }
55
- /** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
56
- export async function runQqEndpointList(state, store, locale) {
57
- return formatEndpointList(qqEndpointListSpec, {
58
- running: state.endpoints.values(),
59
- configured: await store.list('qq'),
60
- footer: busyFooter(state, locale), locale,
61
- });
62
- }
63
54
  const qqEndpointListSpec = {
64
55
  adapterKey: 'qq',
65
56
  adapterDisplayName: 'QQ',
66
57
  describeEntry: (entry) => `appid: ${String(entry.appid)}`,
67
58
  };
68
- async function writeEndpointWithBotKind(pending, botKind, store) {
59
+ async function writeEndpointWithBotKind(pending, botKind, save) {
69
60
  const result = await writeQqEndpointWithBotKind({
70
61
  endpointId: pending.endpointId,
71
62
  appId: pending.appId,
72
63
  appSecret: pending.appSecret,
73
64
  botKind,
74
- }, store);
65
+ }, save);
75
66
  return t(pending.locale, 'endpointSaved', {
76
67
  kind: result.botKind,
77
68
  id: result.endpointId,
78
- file: result.filePath,
79
69
  intents: result.intents.join(', '),
80
70
  });
81
71
  }
82
72
  /**
83
- * 完成 pending 的公域/私域选择:一次性写 .env + yaml。
73
+ * 完成 pending 的公域/私域选择:通过已捕获的所属 Adapter 保存一次账号配置。
84
74
  * @returns 成功文案;调用方负责清 pending。
85
75
  */
86
- export async function completeQqPendingBotKind(pending, botKind, store) {
87
- return await writeEndpointWithBotKind(pending, botKind, store);
76
+ export async function completeQqPendingBotKind(pending, botKind) {
77
+ return await writeEndpointWithBotKind(pending, botKind, pending.save);
88
78
  }
89
79
  /**
90
80
  * `qq endpoint add [id]`:启动扫码绑定流程。
91
81
  * 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
92
82
  * 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
93
- * 扫码成功后凭据暂存内存并询问公域/私域;确认后一次性写 .env + yaml。
83
+ * 扫码成功后凭据暂存内存并询问公域/私域;确认后保存到账号库,等待显式启动。
94
84
  */
95
- export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN', cancelCommand, commandHeads) {
85
+ export function runQqEndpointAdd(state, id, reply, save, input, locale = 'zh-CN', cancelCommand, commandHeads) {
96
86
  if (state.bindFlow || state.pendingBotKind || state.consoleBind?.pending || state.consoleBind?.state === 'waiting') {
97
87
  return Promise.resolve(t(locale, 'existingFlow'));
98
88
  }
@@ -136,8 +126,8 @@ export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN
136
126
  endpointId: finalName,
137
127
  appId,
138
128
  appSecret,
139
- sessionKey: '', locale, cancelCommand,
140
- }, 'public', store) +
129
+ sessionKey: '', locale, cancelCommand, save,
130
+ }, 'public', save) +
141
131
  t(locale, 'noInteractiveKind'));
142
132
  return;
143
133
  }
@@ -145,7 +135,7 @@ export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN
145
135
  endpointId: finalName,
146
136
  appId,
147
137
  appSecret,
148
- sessionKey, locale, cancelCommand, commandHeads,
138
+ sessionKey, locale, cancelCommand, commandHeads, save,
149
139
  };
150
140
  settle(t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }));
151
141
  }
@@ -181,23 +171,38 @@ export function runQqEndpointCancel(state, preference) {
181
171
  }
182
172
  return t(locale, 'noFlow');
183
173
  }
184
- /** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
185
- export async function runQqEndpointRemove(_state, id, store) {
186
- return await removeEndpointById(qqEndpointListSpec, id, store);
187
- }
188
174
  /**
189
175
  * 通用套件生成的 QQ endpoint 命令(add 经 bindFlow 钩子走扫码绑定)。
190
176
  * commands/qq/endpoint/ 下的 list / add / remove 直接默认导出这三项;cancel 为 QQ 特化,单独定义。
191
177
  */
192
- export const qqEndpointCommands = createEndpointCommands({
178
+ const standard = createEndpointCommands({
193
179
  ...qqEndpointListSpec,
194
- fields: [
195
- { key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
196
- { key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
197
- ],
198
- running: (use) => use(qqRuntimeStateToken).endpoints.values(),
180
+ fields: [{ key: 'appid', required: true }, { key: 'secret', required: true, secret: true }],
199
181
  listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
200
- addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
201
- addDescription: '手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
202
- bindFlow: ({ id, reply, input, use, locale, formatCommand }) => runQqEndpointAdd(use(qqRuntimeStateToken), id, reply, use(endpointConfigurationStoreToken), input, locale, formatCommand?.('qq endpoint cancel'), ['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = formatCommand?.(path); return head ? [head] : []; })),
203
182
  }, defineCommand);
183
+ export const qqEndpointCommands = {
184
+ ...standard,
185
+ add: defineCommand({
186
+ description: '手机 QQ 扫码绑定;确认公域/私域后保存账号,手动启动',
187
+ descriptionI18n: { en: 'Scan with mobile QQ, choose public/private, save the account and start explicitly' },
188
+ params: { id: { type: 'string', description: 'endpoint ID' } },
189
+ async execute(context) {
190
+ if (!isEndpointOperator(context.config, context.input))
191
+ return endpointCommandForbidden('QQ', context.locale);
192
+ const index = context.project(adapterFeatureId);
193
+ if (!isAdapterIndex(index))
194
+ throw new Error('QQ Adapter projection is unavailable');
195
+ const own = index.forOwner(context.owner.id).adapters().filter(adapter => adapter.descriptor.name === 'qq');
196
+ if (own.length !== 1)
197
+ throw new Error('QQ Adapter ownership is ambiguous');
198
+ const adapter = own[0];
199
+ if (adapter.needsMigration)
200
+ return '首次修改账号前,请由管理员在 Console 确认迁移账号清单';
201
+ if (!adapter.canManage)
202
+ return '此环境未提供持久化账号管理';
203
+ const revision = adapter.revision;
204
+ const save = config => adapter.add(revision, { endpointId: config.id, config: { ...config } }, { waitForExecution: false });
205
+ return runQqEndpointAdd(context.use(qqRuntimeStateToken), context.params.id, createDurableEndpointCommandReply(context.input, context.use), save, context.input, context.locale, context.formatCommand?.('qq endpoint cancel'), ['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = context.formatCommand?.(path); return head ? [head] : []; }));
206
+ },
207
+ }),
208
+ };
@@ -2,27 +2,16 @@ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
2
  import { messages as t } from '../../../src/i18n.js';
3
3
  /**
4
4
  * `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
5
- * 通用部分(权限 / list / remove / 配置写回 / .env 持久化)已迁移到
6
- * @zhin.js/adapter 的 createEndpointCommands 套件;本文件保留 QQ 特化的
7
- * 扫码绑定 add 流程(经套件 bindFlow 钩子接入)与 cancel,并导出
5
+ * list / remove 与权限共用 @zhin.js/adapter 的账号管理入口;
6
+ * 本文件保留 QQ 扫码绑定 add 与 cancel,保存委派捕获的所属 Adapter CAS,并导出
8
7
  * qqEndpointCommands 供 commands/qq/endpoint/ 下的命令文件默认导出。
9
8
  */
10
- import {
11
- createEndpointCommands,
12
- endpointCommandForbidden,
13
- endpointConfigurationStoreToken,
14
- extractEndpointCommandReply,
15
- formatEndpointList,
16
- isEndpointOperator,
17
- removeEndpointById,
18
- type ConfiguredEndpointEntry,
19
- type EndpointCommandReply,
20
- type EndpointConfigurationStore,
21
- } from 'zhin.js/adapter';
9
+ import { createEndpointCommands, endpointCommandForbidden, createDurableEndpointCommandReply, isEndpointOperator, extractEndpointCommandReply,
10
+ adapterFeatureId, isAdapterIndex, type ConfiguredEndpointEntry, type EndpointCommandReply } from 'zhin.js/adapter';
22
11
  import { defineCommand } from 'zhin.js/command';
23
12
  import { qqBotKindPrompt, qqCommandSessionKey } from '../../../src/qq-bot-kind-prompt.js';
24
13
  import { startQqBindFlow } from '../../../src/qq-bind-flow.js';
25
- import { writeQqEndpointWithBotKind } from '../../../src/qq-endpoint-write.js';
14
+ import { writeQqEndpointWithBotKind, type QqEndpointSavePort } from '../../../src/qq-endpoint-write.js';
26
15
  import type { QqBotKind } from '../../../src/qq-intents.js';
27
16
  import { cancelQqConsoleBind } from '../../../src/qq-console.js';
28
17
  import {
@@ -76,19 +65,6 @@ function busyFooter(state: QqRuntimeState, locale?: Locale): string | undefined
76
65
  return undefined;
77
66
  }
78
67
 
79
- /** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
80
- export async function runQqEndpointList(
81
- state: QqRuntimeState,
82
- store: EndpointConfigurationStore,
83
- locale?: Locale,
84
- ): Promise<string> {
85
- return formatEndpointList(qqEndpointListSpec, {
86
- running: state.endpoints.values(),
87
- configured: await store.list('qq'),
88
- footer: busyFooter(state, locale), locale,
89
- });
90
- }
91
-
92
68
  const qqEndpointListSpec = {
93
69
  adapterKey: 'qq',
94
70
  adapterDisplayName: 'QQ',
@@ -98,45 +74,43 @@ const qqEndpointListSpec = {
98
74
  async function writeEndpointWithBotKind(
99
75
  pending: QqPendingBotKind,
100
76
  botKind: QqBotKind,
101
- store: EndpointConfigurationStore,
77
+ save: QqEndpointSavePort,
102
78
  ): Promise<string> {
103
79
  const result = await writeQqEndpointWithBotKind({
104
80
  endpointId: pending.endpointId,
105
81
  appId: pending.appId,
106
82
  appSecret: pending.appSecret,
107
83
  botKind,
108
- }, store);
84
+ }, save);
109
85
  return t(pending.locale, 'endpointSaved', {
110
86
  kind: result.botKind,
111
87
  id: result.endpointId,
112
- file: result.filePath,
113
88
  intents: result.intents.join(', '),
114
89
  });
115
90
  }
116
91
 
117
92
  /**
118
- * 完成 pending 的公域/私域选择:一次性写 .env + yaml。
93
+ * 完成 pending 的公域/私域选择:通过已捕获的所属 Adapter 保存一次账号配置。
119
94
  * @returns 成功文案;调用方负责清 pending。
120
95
  */
121
96
  export async function completeQqPendingBotKind(
122
97
  pending: QqPendingBotKind,
123
98
  botKind: QqBotKind,
124
- store: EndpointConfigurationStore,
125
99
  ): Promise<string> {
126
- return await writeEndpointWithBotKind(pending, botKind, store);
100
+ return await writeEndpointWithBotKind(pending, botKind, pending.save);
127
101
  }
128
102
 
129
103
  /**
130
104
  * `qq endpoint add [id]`:启动扫码绑定流程。
131
105
  * 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
132
106
  * 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
133
- * 扫码成功后凭据暂存内存并询问公域/私域;确认后一次性写 .env + yaml。
107
+ * 扫码成功后凭据暂存内存并询问公域/私域;确认后保存到账号库,等待显式启动。
134
108
  */
135
109
  export function runQqEndpointAdd(
136
110
  state: QqRuntimeState,
137
111
  id: string | undefined,
138
112
  reply: QqCommandReply,
139
- store: EndpointConfigurationStore,
113
+ save: QqEndpointSavePort,
140
114
  input?: unknown,
141
115
  locale: Locale = 'zh-CN',
142
116
  cancelCommand?: string,
@@ -192,10 +166,10 @@ export function runQqEndpointAdd(
192
166
  endpointId: finalName,
193
167
  appId,
194
168
  appSecret,
195
- sessionKey: '', locale, cancelCommand,
169
+ sessionKey: '', locale, cancelCommand, save,
196
170
  },
197
171
  'public',
198
- store,
172
+ save,
199
173
  ) +
200
174
  t(locale, 'noInteractiveKind'),
201
175
  );
@@ -206,7 +180,7 @@ export function runQqEndpointAdd(
206
180
  endpointId: finalName,
207
181
  appId,
208
182
  appSecret,
209
- sessionKey, locale, cancelCommand, commandHeads,
183
+ sessionKey, locale, cancelCommand, commandHeads, save,
210
184
  };
211
185
  settle(
212
186
  t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }),
@@ -247,37 +221,37 @@ export function runQqEndpointCancel(state: QqRuntimeState, preference?: Locale):
247
221
  return t(locale, 'noFlow');
248
222
  }
249
223
 
250
- /** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
251
- export async function runQqEndpointRemove(
252
- _state: QqRuntimeState,
253
- id: string,
254
- store: EndpointConfigurationStore,
255
- ): Promise<string> {
256
- return await removeEndpointById(qqEndpointListSpec, id, store);
257
- }
258
-
259
224
  /**
260
225
  * 通用套件生成的 QQ endpoint 命令(add 经 bindFlow 钩子走扫码绑定)。
261
226
  * commands/qq/endpoint/ 下的 list / add / remove 直接默认导出这三项;cancel 为 QQ 特化,单独定义。
262
227
  */
263
- export const qqEndpointCommands = createEndpointCommands({
228
+ const standard = createEndpointCommands({
264
229
  ...qqEndpointListSpec,
265
- fields: [
266
- { key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
267
- { key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
268
- ],
269
- running: (use) => use(qqRuntimeStateToken).endpoints.values(),
230
+ fields: [{ key: 'appid', required: true }, { key: 'secret', required: true, secret: true }],
270
231
  listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
271
- addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
272
- addDescription:
273
- '手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
274
- bindFlow: ({ id, reply, input, use, locale, formatCommand }) =>
275
- runQqEndpointAdd(
276
- use(qqRuntimeStateToken),
277
- id,
278
- reply,
279
- use(endpointConfigurationStoreToken),
280
- input, locale, formatCommand?.('qq endpoint cancel'),
281
- ['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = formatCommand?.(path); return head ? [head] : []; }),
282
- ),
283
232
  }, defineCommand);
233
+
234
+ export const qqEndpointCommands = {
235
+ ...standard,
236
+ add: defineCommand({
237
+ description: '手机 QQ 扫码绑定;确认公域/私域后保存账号,手动启动',
238
+ descriptionI18n: { en: 'Scan with mobile QQ, choose public/private, save the account and start explicitly' },
239
+ params: { id: { type: 'string', description: 'endpoint ID' } },
240
+ async execute(context) {
241
+ if (!isEndpointOperator(context.config, context.input)) return endpointCommandForbidden('QQ', context.locale);
242
+ const index = context.project<unknown>(adapterFeatureId);
243
+ if (!isAdapterIndex(index)) throw new Error('QQ Adapter projection is unavailable');
244
+ const own = index.forOwner(context.owner.id).adapters().filter(adapter => adapter.descriptor.name === 'qq');
245
+ if (own.length !== 1) throw new Error('QQ Adapter ownership is ambiguous');
246
+ const adapter = own[0];
247
+ if (adapter.needsMigration) return '首次修改账号前,请由管理员在 Console 确认迁移账号清单';
248
+ if (!adapter.canManage) return '此环境未提供持久化账号管理';
249
+ const revision = adapter.revision;
250
+ const save: QqEndpointSavePort = config => adapter.add(revision, { endpointId: config.id, config: { ...config } }, { waitForExecution: false });
251
+ return runQqEndpointAdd(context.use(qqRuntimeStateToken), context.params.id,
252
+ createDurableEndpointCommandReply(context.input, context.use), save, context.input, context.locale,
253
+ context.formatCommand?.('qq endpoint cancel'),
254
+ ['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = context.formatCommand?.(path); return head ? [head] : []; }));
255
+ },
256
+ }),
257
+ };
@@ -0,0 +1,13 @@
1
+ import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
2
+ import { QqHttpEndpoint, QqWebsocketEndpoint } from './endpoint.js';
3
+ import { type QqEndpointConfig } from './protocol.js';
4
+ import type { QqClientEventMap } from './client.js';
5
+ export declare class QqAdapter extends Adapter<QqHttpEndpoint | QqWebsocketEndpoint, QqEndpointConfig, QqClientEventMap> {
6
+ private readonly context;
7
+ constructor(context: AdapterContext<QqEndpointConfig>);
8
+ get configurationIdentityFields(): readonly string[];
9
+ get configurationIdentityProbeFields(): readonly string[];
10
+ get configurationUpdateFields(): readonly string[];
11
+ protected validateConfiguration(input: QqEndpointConfig): void;
12
+ protected createEndpoint(input: QqEndpointConfig, execution: EndpointCreationContext): QqHttpEndpoint | QqWebsocketEndpoint;
13
+ }
package/lib/adapter.js ADDED
@@ -0,0 +1,27 @@
1
+ import { Adapter, createAdapterOutageNotifier } from 'zhin.js/adapter';
2
+ import { outboundHostToken } from 'zhin.js';
3
+ import { httpHostToken } from '@zhin.js/host-http';
4
+ import { QqHttpEndpoint, QqWebsocketEndpoint } from './endpoint.js';
5
+ import { resolveQqConfig } from './protocol.js';
6
+ export class QqAdapter extends Adapter {
7
+ context;
8
+ constructor(context) {
9
+ super();
10
+ this.context = context;
11
+ }
12
+ get configurationIdentityFields() { return ['id', 'appid']; }
13
+ get configurationIdentityProbeFields() { return ['accessTokenUrl', 'gatewayUrl']; }
14
+ get configurationUpdateFields() {
15
+ return ['secret', 'mode', 'sandbox', 'intents', 'botKind',
16
+ 'accessTokenUrl', 'gatewayUrl', 'streamProxy', 'streamProxy.port', 'streamProxy.serverName', 'webhookPath'];
17
+ }
18
+ validateConfiguration(input) { resolveQqConfig(input); }
19
+ createEndpoint(input, execution) {
20
+ const config = resolveQqConfig(input);
21
+ const outbound = this.context.has?.(outboundHostToken) ? this.context.use(outboundHostToken) : undefined;
22
+ const notifier = createAdapterOutageNotifier(outbound, config.notify);
23
+ return config.mode === 'websocket'
24
+ ? new QqWebsocketEndpoint({ id: execution.canonicalId, config, notifier })
25
+ : new QqHttpEndpoint({ id: execution.canonicalId, config, notifier, http: this.context.use(httpHostToken) });
26
+ }
27
+ }
package/lib/client.d.ts CHANGED
@@ -1,12 +1,10 @@
1
- import type { QqInboundMessage } from './protocol.js';
2
- import type { QqBotTransport } from './ws.js';
3
- export type QqClientEventMap = Record<string, QqInboundMessage | unknown>;
1
+ import type { EventMap } from 'qq-official-bot';
2
+ import type { QqAdapter } from './adapter.js';
3
+ export type QqClientEventMap = {
4
+ readonly [K in keyof EventMap]: EventMap[K] extends (event: infer E, ...args: never[]) => unknown ? E : never;
5
+ };
4
6
  declare module '@zhin.js/feature-kit' {
5
- interface AdapterClientRegistry {
6
- readonly qq: {
7
- readonly client: QqBotTransport;
8
- readonly events: QqClientEventMap;
9
- };
7
+ interface AdapterRegistry {
8
+ readonly qq: QqAdapter;
10
9
  }
11
10
  }
12
- export declare const qqClient: import("@zhin.js/adapter").EndpointClientToken<QqBotTransport, QqClientEventMap>;
package/lib/client.js CHANGED
@@ -1,2 +1 @@
1
- import { defineEndpointClient } from 'zhin.js/adapter';
2
- export const qqClient = defineEndpointClient('qq');
1
+ export {};
package/lib/console.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * QQ Console RPC:除通用 endpoint 管理外,把 `qq endpoint add` 的扫码绑定
3
- * (以及公域 / 私域选择、取消)搬到 Console 页面。
4
- */
5
- import { type AdapterConsoleResources } from 'zhin.js/adapter';
6
1
  import { type PluginConsoleRpc } from 'zhin.js';
7
2
  import type { QqRuntimeState } from './qq-runtime-state.js';
8
- export declare function createQqConsoleRpc(state: QqRuntimeState, resources: AdapterConsoleResources, getConfig: () => unknown): PluginConsoleRpc;
3
+ export declare function createQqConsoleRpc(state: QqRuntimeState, getConfig: () => unknown): PluginConsoleRpc;
package/lib/console.js CHANGED
@@ -2,17 +2,13 @@
2
2
  * QQ Console RPC:除通用 endpoint 管理外,把 `qq endpoint add` 的扫码绑定
3
3
  * (以及公域 / 私域选择、取消)搬到 Console 页面。
4
4
  */
5
- import { endpointConfigurationStoreToken, createAdapterConsoleRpc, } from 'zhin.js/adapter';
5
+ import { prepareConsoleAdapterMutation, ownedConsoleAdapter, createAdapterConsoleRpc, } from 'zhin.js/adapter';
6
6
  import { PluginConsoleRpcError } from 'zhin.js';
7
7
  import { qqConsoleSpec } from './qq-console-spec.js';
8
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;
9
+ export function createQqConsoleRpc(state, getConfig) {
13
10
  const spec = {
14
11
  ...qqConsoleSpec,
15
- running: () => state.endpoints.values(),
16
12
  getConfig,
17
13
  extraStatus: () => {
18
14
  const bind = state.consoleBind;
@@ -28,8 +24,6 @@ export function createQqConsoleRpc(state, resources, getConfig) {
28
24
  };
29
25
  },
30
26
  availableActions: () => {
31
- if (!store)
32
- return [];
33
27
  if (state.consoleBind?.pending)
34
28
  return ['qq-bind-kind', 'qq-bind-cancel'];
35
29
  if (state.bindFlow || state.consoleBind?.state === 'waiting')
@@ -40,16 +34,25 @@ export function createQqConsoleRpc(state, resources, getConfig) {
40
34
  'qq-bind-start': {
41
35
  label: '扫码绑定新机器人',
42
36
  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);
37
+ description: '用手机 QQ 扫码,选择公域 / 私域后保存到账号库;保存后需手动启动。',
38
+ descriptionI18n: { en: 'Scan with mobile QQ, choose public/private and save the account; start it explicitly afterwards.' },
39
+ handler: async (input, call) => {
40
+ if (!call.runtime)
41
+ throw new PluginConsoleRpcError('unsupported', '账号运行目录不可用');
42
+ const adapter = ownedConsoleAdapter(spec, call);
43
+ if (!adapter.canManage)
44
+ throw new PluginConsoleRpcError('unsupported', '此环境未提供持久化账号管理');
45
+ if (input.expectedGeneration !== call.runtime.generation || input.expectedRevision !== adapter.revision) {
46
+ throw new PluginConsoleRpcError('conflict', '账号清单已变化,请刷新核实');
47
+ }
48
+ const starting = startQqConsoleBind(state);
49
+ state.consoleAdmission = { adapterId: adapter.descriptor.id, generation: call.runtime.generation, revision: adapter.revision };
50
+ const { url } = await starting;
49
51
  return { message: '请用手机 QQ 扫码;扫码成功后选择公域或私域', url };
50
52
  },
51
53
  },
52
54
  'qq-bind-kind': {
55
+ accountMutation: true,
53
56
  label: '选择公域 / 私域',
54
57
  labelI18n: { en: 'Choose public/private' },
55
58
  description: '公域订阅 PUBLIC_GUILD_MESSAGES;私域订阅 GUILD_MESSAGES。',
@@ -58,12 +61,18 @@ export function createQqConsoleRpc(state, resources, getConfig) {
58
61
  { value: 'public', label: '公域', labelI18n: { en: 'Public' } },
59
62
  { value: 'private', label: '私域', labelI18n: { en: 'Private' } },
60
63
  ],
61
- handler: async (input) => {
62
- if (!store)
63
- throw new PluginConsoleRpcError('unsupported', '当前运行环境不支持写入 endpoint 配置');
64
- const result = await completeQqConsoleBind(state, store, input.value);
64
+ handler: async (input, call) => {
65
+ const captured = state.consoleAdmission;
66
+ if (!captured || captured.generation !== input.expectedGeneration || captured.revision !== input.expectedRevision) {
67
+ throw new PluginConsoleRpcError('conflict', '扫码期间账号清单已变化,请刷新核实');
68
+ }
69
+ const admission = prepareConsoleAdapterMutation(spec, call, input);
70
+ if (admission.adapter.descriptor.id !== captured.adapterId)
71
+ throw new PluginConsoleRpcError('conflict', '扫码所属 Adapter 已变化');
72
+ const result = await completeQqConsoleBind(state, config => admission.adapter.add(admission.revision, { endpointId: config.id, config: { ...config } }, admission.options), input.value);
65
73
  return {
66
- message: `已写入 endpoint「${result.endpointId}」(${result.botKind}),重启或热重载后生效`,
74
+ message: `已保存账号「${result.endpointId}」(${result.botKind}),尚未启动`,
75
+ saved: true, restartRequired: false, execution: { ...result.mutation.execution },
67
76
  details: { endpoint: result.endpointId, botKind: result.botKind, intents: result.intents.join(', ') },
68
77
  };
69
78
  },
@@ -80,5 +89,5 @@ export function createQqConsoleRpc(state, resources, getConfig) {
80
89
  },
81
90
  },
82
91
  };
83
- return createAdapterConsoleRpc(spec, resources);
92
+ return createAdapterConsoleRpc(spec);
84
93
  }