@zhin.js/adapter-qq 1.1.15 → 1.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @zhin.js/adapter-qq
2
2
 
3
+ ## 1.1.16
4
+
5
+ ### Patch Changes
6
+
7
+ - 53bc6a0: 适配器配置标签、端点命令和工具反馈支持中英文;复用当前操作语言,保持协议值、用户内容、身份、权限与发送链路不变。Sandbox 展示使用项目语言,QQ 绑定与交互状态保留完整会话身份。
8
+ - Updated dependencies [334e92d]
9
+ - Updated dependencies [f3b7c9f]
10
+ - Updated dependencies [658a5fb]
11
+ - Updated dependencies [144a17b]
12
+ - Updated dependencies [18296cf]
13
+ - Updated dependencies [08e64de]
14
+ - Updated dependencies [9cade30]
15
+ - Updated dependencies [53bc6a0]
16
+ - Updated dependencies [7595136]
17
+ - Updated dependencies [66b6404]
18
+ - Updated dependencies [adb4af2]
19
+ - Updated dependencies [69f9ef6]
20
+ - Updated dependencies [17d1301]
21
+ - @zhin.js/adapter@1.1.23
22
+ - @zhin.js/host-http@1.1.10
23
+ - @zhin.js/core@1.1.51
24
+ - @zhin.js/permission@1.1.9
25
+ - @zhin.js/tool@1.1.10
26
+ - @zhin.js/skill@1.1.10
27
+ - @zhin.js/im-contract@1.1.7
28
+ - @zhin.js/agent-feature@1.1.10
29
+ - @zhin.js/feature-kit@1.1.9
30
+
3
31
  ## 1.1.15
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -237,3 +237,9 @@ QQ 当前未实现 canonical `share` 的标题/描述/URL 消息卡片。适配
237
237
  可选 `streamProxy: { port: 18443, serverName: "实际网关 hostname" }` 将 WSS 的 TCP 连接导向 `127.0.0.1:port`。需代理固定转发到相同官方网关的 443 端口;SDK 每次仍动态调用 `getWsUrl()`,hostname/443 不匹配会拒绝连接并只记录 hostname,不能自动绕过代理。原始 URL、HTTP Host、TLS SNI 保留,证书验证始终开启。不要填带 query 的 URL。
238
238
 
239
239
  仅 WSS 入站、心跳和会话认证经过此代理;AppSecret 换 token、网关发现 HTTP、消息 API 和媒体上传仍直连。故障恢复由 `createEndpointLifecycle` 负责,SDK 内置重连关闭,避免双重重连。此入口用于受控网络验收,不提供全局代理或 TLS 验证绕过。
240
+
241
+ ## 语言与绑定会话
242
+
243
+ Endpoint 管理命令和工具反馈使用当前操作语言。扫码绑定在开始时固定语言,后续二维码刷新、公域/私域选择和结果回复保持一致;`public` / `private` 是稳定的配置值,两种语言都可使用,中文别名只在中文流程中接受。
244
+
245
+ 取消指引使用发起命令时的真实前缀及插件命名空间,不猜测调用方式。确认回复仅接受发起者所在的完整会话(Endpoint、会话、父容器、线程和发送者);其他成员或线程不会完成该绑定。
@@ -0,0 +1,24 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { createMessages } from 'zhin.js/i18n';
3
+ export const messages = createMessages({
4
+ "operationFailed": {
5
+ "zh-CN": "操作失败",
6
+ "en": "Operation failed"
7
+ },
8
+ "roleAdded": {
9
+ "zh-CN": "已给成员添加角色",
10
+ "en": "Role added to the member"
11
+ },
12
+ "createFailed": {
13
+ "zh-CN": "创建失败",
14
+ "en": "Creation failed"
15
+ },
16
+ "namedRoleCreated": {
17
+ "zh-CN": "角色 \"{p0}\" 创建成功",
18
+ "en": "Role \"{p0}\" created"
19
+ },
20
+ "roleRemoved": {
21
+ "zh-CN": "已移除成员的角色",
22
+ "en": "Role removed from the member"
23
+ }
24
+ });
@@ -0,0 +1,24 @@
1
+ import { createMessages } from 'zhin.js/i18n';
2
+
3
+ export const messages = createMessages({
4
+ "operationFailed": {
5
+ "zh-CN": "操作失败",
6
+ "en": "Operation failed"
7
+ },
8
+ "roleAdded": {
9
+ "zh-CN": "已给成员添加角色",
10
+ "en": "Role added to the member"
11
+ },
12
+ "createFailed": {
13
+ "zh-CN": "创建失败",
14
+ "en": "Creation failed"
15
+ },
16
+ "namedRoleCreated": {
17
+ "zh-CN": "角色 \"{p0}\" 创建成功",
18
+ "en": "Role \"{p0}\" created"
19
+ },
20
+ "roleRemoved": {
21
+ "zh-CN": "已移除成员的角色",
22
+ "en": "Role removed from the member"
23
+ }
24
+ });
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages as t } from "../../../../messages.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { platformPermission } from '@zhin.js/permission';
@@ -17,6 +18,6 @@ export default defineAgentTool({
17
18
  async execute({ guild_id, channel_id, user_id, role_id }, context) {
18
19
  const client = context.$client;
19
20
  const success = await client.addMemberRole(guild_id, channel_id, user_id, role_id);
20
- return { success, message: success ? '已给成员添加角色' : '操作失败' };
21
+ return { success, message: success ? t(context.locale, 'roleAdded') : t(context.locale, 'operationFailed') };
21
22
  },
22
23
  });
@@ -1,3 +1,4 @@
1
+ import { messages as t } from "../../../../messages.js";
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { z } from 'zod';
3
4
  import { platformPermission } from '@zhin.js/permission';
@@ -17,6 +18,6 @@ export default defineAgentTool<{ guild_id: string; channel_id: string; user_id:
17
18
  async execute({ guild_id, channel_id, user_id, role_id }: { guild_id: string; channel_id: string; user_id: string; role_id: string }, context) {
18
19
  const client = context.$client;
19
20
  const success = await client.addMemberRole(guild_id, channel_id, user_id, role_id);
20
- return { success, message: success ? '已给成员添加角色' : '操作失败' };
21
+ return { success, message: success ? t(context.locale, 'roleAdded') : t(context.locale, 'operationFailed') };
21
22
  },
22
23
  });
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages as t } from "../../../../messages.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { platformPermission } from '@zhin.js/permission';
@@ -16,6 +17,6 @@ export default defineAgentTool({
16
17
  async execute({ guild_id, name, color }, context) {
17
18
  const client = context.$client;
18
19
  const role = await client.createGuildRole(guild_id, name, color);
19
- return { success: !!role, role, message: role ? `角色 "${name}" 创建成功` : '创建失败' };
20
+ return { success: !!role, role, message: role ? t(context.locale, 'namedRoleCreated', { p0: name }) : t(context.locale, 'createFailed') };
20
21
  },
21
22
  });
@@ -1,3 +1,4 @@
1
+ import { messages as t } from "../../../../messages.js";
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { z } from 'zod';
3
4
  import { platformPermission } from '@zhin.js/permission';
@@ -16,6 +17,6 @@ export default defineAgentTool<{ guild_id: string; name: string; color?: number
16
17
  async execute({ guild_id, name, color }: { guild_id: string; name: string; color?: number }, context) {
17
18
  const client = context.$client;
18
19
  const role = await client.createGuildRole(guild_id, name, color);
19
- return { success: !!role, role, message: role ? `角色 "${name}" 创建成功` : '创建失败' };
20
+ return { success: !!role, role, message: role ? t(context.locale, 'namedRoleCreated', { p0: name }) : t(context.locale, 'createFailed') };
20
21
  },
21
22
  });
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages as t } from "../../../../messages.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { platformPermission } from '@zhin.js/permission';
@@ -17,6 +18,6 @@ export default defineAgentTool({
17
18
  async execute({ guild_id, channel_id, user_id, role_id }, context) {
18
19
  const client = context.$client;
19
20
  const success = await client.removeMemberRole(guild_id, channel_id, user_id, role_id);
20
- return { success, message: success ? '已移除成员的角色' : '操作失败' };
21
+ return { success, message: success ? t(context.locale, 'roleRemoved') : t(context.locale, 'operationFailed') };
21
22
  },
22
23
  });
@@ -1,3 +1,4 @@
1
+ import { messages as t } from "../../../../messages.js";
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { z } from 'zod';
3
4
  import { platformPermission } from '@zhin.js/permission';
@@ -17,6 +18,6 @@ export default defineAgentTool<{ guild_id: string; channel_id: string; user_id:
17
18
  async execute({ guild_id, channel_id, user_id, role_id }: { guild_id: string; channel_id: string; user_id: string; role_id: string }, context) {
18
19
  const client = context.$client;
19
20
  const success = await client.removeMemberRole(guild_id, channel_id, user_id, role_id);
20
- return { success, message: success ? '已移除成员的角色' : '操作失败' };
21
+ return { success, message: success ? t(context.locale, 'roleRemoved') : t(context.locale, 'operationFailed') };
21
22
  },
22
23
  });
@@ -1,11 +1,12 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { endpointCommandForbidden } from 'zhin.js/adapter';
2
3
  import { defineCommand } from 'zhin.js/command';
3
- import { isQqEndpointOperator, QQ_ENDPOINT_FORBIDDEN, qqRuntimeStateToken, runQqEndpointCancel, } from '../definition.js';
4
+ import { isQqEndpointOperator, qqRuntimeStateToken, runQqEndpointCancel, } from '../definition.js';
4
5
  export default defineCommand({
5
- description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
6
- execute({ config, input, use }) {
6
+ description: '取消进行中的 QQ 扫码绑定或公域/私域选择', descriptionI18n: { en: "Cancel an active QQ QR binding or public/private-domain selection" },
7
+ execute({ config, input, use, locale }) {
7
8
  if (!isQqEndpointOperator(config, input))
8
- return QQ_ENDPOINT_FORBIDDEN;
9
- return runQqEndpointCancel(use(qqRuntimeStateToken));
9
+ return endpointCommandForbidden('QQ', locale);
10
+ return runQqEndpointCancel(use(qqRuntimeStateToken), locale);
10
11
  },
11
12
  });
@@ -1,15 +1,15 @@
1
+ import { endpointCommandForbidden } from 'zhin.js/adapter';
1
2
  import { defineCommand } from 'zhin.js/command';
2
3
  import {
3
4
  isQqEndpointOperator,
4
- QQ_ENDPOINT_FORBIDDEN,
5
5
  qqRuntimeStateToken,
6
6
  runQqEndpointCancel,
7
7
  } from '../definition.js';
8
8
 
9
9
  export default defineCommand({
10
- description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
11
- execute({ config, input, use }) {
12
- if (!isQqEndpointOperator(config, input)) return QQ_ENDPOINT_FORBIDDEN;
13
- return runQqEndpointCancel(use(qqRuntimeStateToken));
10
+ description: '取消进行中的 QQ 扫码绑定或公域/私域选择', descriptionI18n: { en: "Cancel an active QQ QR binding or public/private-domain selection" },
11
+ execute({ config, input, use, locale }) {
12
+ if (!isQqEndpointOperator(config, input)) return endpointCommandForbidden('QQ', locale);
13
+ return runQqEndpointCancel(use(qqRuntimeStateToken), locale);
14
14
  },
15
15
  });
@@ -1,4 +1,6 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { resolveLocale } from 'zhin.js/i18n';
3
+ import { messages as t } from "../../../lib/i18n.js";
2
4
  /**
3
5
  * `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
4
6
  * 通用部分(权限 / list / remove / 配置写回 / .env 持久化)已迁移到
@@ -8,7 +10,7 @@
8
10
  */
9
11
  import { buildEndpointEnvKey, createEndpointCommands, endpointCommandForbidden, endpointConfigurationStoreToken, extractEndpointCommandReply, formatEndpointList, isEndpointOperator, removeEndpointById, } from 'zhin.js/adapter';
10
12
  import { defineCommand } from 'zhin.js/command';
11
- import { QQ_BOT_KIND_PROMPT, qqCommandSessionKey } from "../../../lib/qq-bot-kind-prompt.js";
13
+ import { qqBotKindPrompt, qqCommandSessionKey } from "../../../lib/qq-bot-kind-prompt.js";
12
14
  import { startQqBindFlow } from "../../../lib/qq-bind-flow.js";
13
15
  import { defaultQqEndpointIntentFields } from "../../../lib/qq-intents.js";
14
16
  import { qqRuntimeStateToken, } from "../../../lib/qq-runtime-state.js";
@@ -28,20 +30,20 @@ export const QQ_ENDPOINT_FORBIDDEN = endpointCommandForbidden('QQ');
28
30
  export function extractQqCommandReply(input) {
29
31
  return extractEndpointCommandReply(input);
30
32
  }
31
- function busyFooter(state) {
33
+ function busyFooter(state, locale) {
32
34
  if (state.bindFlow)
33
- return '⚠️ 有进行中的扫码绑定,可用 qq endpoint cancel 取消';
35
+ return t(locale, 'bindingBusy') + (state.bindFlow.cancelCommand ? `\n${t(locale, 'cancelHint', { command: state.bindFlow.cancelCommand })}` : '');
34
36
  if (state.pendingBotKind) {
35
- return `⚠️ endpoint「${state.pendingBotKind.endpointId}」等待公域/私域选择,回复 public/private 或 qq endpoint cancel`;
37
+ return t(locale, 'kindBusy', { id: state.pendingBotKind.endpointId });
36
38
  }
37
39
  return undefined;
38
40
  }
39
41
  /** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
40
- export async function runQqEndpointList(state, store) {
42
+ export async function runQqEndpointList(state, store, locale) {
41
43
  return formatEndpointList(qqEndpointListSpec, {
42
44
  running: state.endpoints.values(),
43
45
  configured: await store.list('qq'),
44
- footer: busyFooter(state),
46
+ footer: busyFooter(state, locale), locale,
45
47
  });
46
48
  }
47
49
  const qqEndpointListSpec = {
@@ -50,6 +52,7 @@ const qqEndpointListSpec = {
50
52
  describeEntry: (entry) => `appid: ${String(entry.appid)}`,
51
53
  };
52
54
  async function writeEndpointWithBotKind(pending, botKind, store) {
55
+ const locale = pending.locale;
53
56
  const appidKey = buildEndpointEnvKey('qq', pending.endpointId, 'appid');
54
57
  const secretKey = buildEndpointEnvKey('qq', pending.endpointId, 'secret');
55
58
  const intentFields = defaultQqEndpointIntentFields(botKind);
@@ -67,9 +70,7 @@ async function writeEndpointWithBotKind(pending, botKind, store) {
67
70
  [secretKey]: pending.appSecret,
68
71
  },
69
72
  });
70
- return (`✅ 已按 botKind=${intentFields.botKind} 将 endpoint「${pending.endpointId}」写入 .env 与 ${filePath}。\n` +
71
- `intents: ${intentFields.intents.join(', ')}。\n` +
72
- '⚠️ 需重启 zhin 后新 endpoint 才会生效。');
73
+ return t(locale, 'endpointSaved', { kind: intentFields.botKind, id: pending.endpointId, file: filePath, intents: intentFields.intents.join(', ') });
73
74
  }
74
75
  /**
75
76
  * 完成 pending 的公域/私域选择:一次性写 .env + yaml。
@@ -84,9 +85,9 @@ export async function completeQqPendingBotKind(pending, botKind, store) {
84
85
  * 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
85
86
  * 扫码成功后凭据暂存内存并询问公域/私域;确认后一次性写 .env + yaml。
86
87
  */
87
- export function runQqEndpointAdd(state, id, reply, store, input) {
88
+ export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN', cancelCommand, commandHeads) {
88
89
  if (state.bindFlow || state.pendingBotKind) {
89
- return Promise.resolve('已有进行中的 QQ 机器人绑定或待确认的公域/私域选择,请先发送 qq endpoint cancel 取消后再试');
90
+ return Promise.resolve(t(locale, 'existingFlow'));
90
91
  }
91
92
  const endpointId = id?.trim() || undefined;
92
93
  const sessionKey = qqCommandSessionKey(input);
@@ -106,11 +107,11 @@ export function runQqEndpointAdd(state, id, reply, store, input) {
106
107
  const stop = startQqBindFlow({
107
108
  onQrDisplayed: (url) => {
108
109
  // QQ 出站当前仅支持纯文本(富媒体未迁移),二维码只能发链接文本
109
- settle(`请用手机 QQ 打开以下链接完成扫码绑定(二维码图片出站暂未支持,故发送链接):\n${url}`);
110
+ settle(t(locale, 'scanLink', { url }));
110
111
  },
111
112
  onQrExpired: async () => {
112
113
  try {
113
- await reply('二维码已过期,正在刷新,请扫描新链接…');
114
+ await reply(t(locale, 'qrExpired'));
114
115
  }
115
116
  catch {
116
117
  // same as settle follow-up: best-effort after command scope ends
@@ -127,45 +128,45 @@ export function runQqEndpointAdd(state, id, reply, store, input) {
127
128
  endpointId: finalName,
128
129
  appId,
129
130
  appSecret,
130
- sessionKey: '',
131
+ sessionKey: '', locale, cancelCommand,
131
132
  }, 'public', store) +
132
- '\n(当前会话无法交互选择公/私域;私域请手动改配置。)');
133
+ t(locale, 'noInteractiveKind'));
133
134
  return;
134
135
  }
135
136
  state.pendingBotKind = {
136
137
  endpointId: finalName,
137
138
  appId,
138
139
  appSecret,
139
- sessionKey,
140
+ sessionKey, locale, cancelCommand, commandHeads,
140
141
  };
141
- settle(`✅ 扫码成功!endpoint「${finalName}」。\n` +
142
- QQ_BOT_KIND_PROMPT);
142
+ settle(t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }));
143
143
  }
144
144
  catch (error) {
145
- settle(`绑定成功但写入配置失败:${error instanceof Error ? error.message : String(error)}`);
145
+ settle(t(locale, 'writeFailed', { error: error instanceof Error ? error.message : String(error) }));
146
146
  }
147
147
  },
148
148
  onFailure: (error) => {
149
149
  state.bindFlow = null;
150
- settle(`❌ QQ 绑定失败:${error.message}`);
150
+ settle(t(locale, 'bindingFailed', { error: error.message }));
151
151
  },
152
- }, { source: 'zhin' });
153
- state.bindFlow = { id: endpointId, stop };
152
+ }, { source: 'zhin', locale });
153
+ state.bindFlow = { id: endpointId, stop, locale, cancelCommand, commandHeads };
154
154
  });
155
155
  }
156
156
  /** `qq endpoint cancel`:中止进行中的绑定或待选 botKind */
157
- export function runQqEndpointCancel(state) {
157
+ export function runQqEndpointCancel(state, preference) {
158
+ const locale = resolveLocale(state.pendingBotKind?.locale ?? state.bindFlow?.locale ?? preference);
158
159
  if (state.bindFlow) {
159
160
  state.bindFlow.stop();
160
161
  state.bindFlow = null;
161
- return '已取消进行中的 QQ 绑定流程';
162
+ return t(locale, 'cancelled');
162
163
  }
163
164
  if (state.pendingBotKind) {
164
165
  const name = state.pendingBotKind.endpointId;
165
166
  state.pendingBotKind = null;
166
- return `已取消 endpoint「${name}」的绑定(尚未写入任何文件)`;
167
+ return t(locale, 'pendingCancelled', { name });
167
168
  }
168
- return '当前没有进行中的 QQ 绑定流程';
169
+ return t(locale, 'noFlow');
169
170
  }
170
171
  /** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
171
172
  export async function runQqEndpointRemove(_state, id, store) {
@@ -178,11 +179,12 @@ export async function runQqEndpointRemove(_state, id, store) {
178
179
  export const qqEndpointCommands = createEndpointCommands({
179
180
  ...qqEndpointListSpec,
180
181
  fields: [
181
- { key: 'appid', required: true, env: true, description: 'QQ 机器人 appid' },
182
- { key: 'secret', required: true, env: true, description: 'QQ 机器人 secret' },
182
+ { key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
183
+ { key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
183
184
  ],
184
185
  running: (use) => use(qqRuntimeStateToken).endpoints.values(),
185
- listFooter: (use) => busyFooter(use(qqRuntimeStateToken)),
186
+ listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
187
+ addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
186
188
  addDescription: '手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
187
- bindFlow: ({ id, reply, input, use }) => runQqEndpointAdd(use(qqRuntimeStateToken), id, reply, use(endpointConfigurationStoreToken), input),
189
+ 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] : []; })),
188
190
  }, defineCommand);
@@ -1,3 +1,5 @@
1
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
+ import { messages as t } from '../../../src/i18n.js';
1
3
  /**
2
4
  * `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
3
5
  * 通用部分(权限 / list / remove / 配置写回 / .env 持久化)已迁移到
@@ -19,7 +21,7 @@ import {
19
21
  type EndpointConfigurationStore,
20
22
  } from 'zhin.js/adapter';
21
23
  import { defineCommand } from 'zhin.js/command';
22
- import { QQ_BOT_KIND_PROMPT, qqCommandSessionKey } from '../../../src/qq-bot-kind-prompt.js';
24
+ import { qqBotKindPrompt, qqCommandSessionKey } from '../../../src/qq-bot-kind-prompt.js';
23
25
  import { startQqBindFlow } from '../../../src/qq-bind-flow.js';
24
26
  import { defaultQqEndpointIntentFields, type QqBotKind } from '../../../src/qq-intents.js';
25
27
  import {
@@ -50,10 +52,10 @@ export function extractQqCommandReply(input: unknown): QqCommandReply {
50
52
  return extractEndpointCommandReply(input);
51
53
  }
52
54
 
53
- function busyFooter(state: QqRuntimeState): string | undefined {
54
- if (state.bindFlow) return '⚠️ 有进行中的扫码绑定,可用 qq endpoint cancel 取消';
55
+ function busyFooter(state: QqRuntimeState, locale?: Locale): string | undefined {
56
+ if (state.bindFlow) return t(locale, 'bindingBusy') + (state.bindFlow.cancelCommand ? `\n${t(locale, 'cancelHint', { command: state.bindFlow.cancelCommand })}` : '');
55
57
  if (state.pendingBotKind) {
56
- return `⚠️ endpoint「${state.pendingBotKind.endpointId}」等待公域/私域选择,回复 public/private 或 qq endpoint cancel`;
58
+ return t(locale, 'kindBusy', { id: state.pendingBotKind.endpointId });
57
59
  }
58
60
  return undefined;
59
61
  }
@@ -62,11 +64,12 @@ function busyFooter(state: QqRuntimeState): string | undefined {
62
64
  export async function runQqEndpointList(
63
65
  state: QqRuntimeState,
64
66
  store: EndpointConfigurationStore,
67
+ locale?: Locale,
65
68
  ): Promise<string> {
66
69
  return formatEndpointList(qqEndpointListSpec, {
67
70
  running: state.endpoints.values(),
68
71
  configured: await store.list('qq'),
69
- footer: busyFooter(state),
72
+ footer: busyFooter(state, locale), locale,
70
73
  });
71
74
  }
72
75
 
@@ -81,6 +84,7 @@ async function writeEndpointWithBotKind(
81
84
  botKind: QqBotKind,
82
85
  store: EndpointConfigurationStore,
83
86
  ): Promise<string> {
87
+ const locale = pending.locale;
84
88
  const appidKey = buildEndpointEnvKey('qq', pending.endpointId, 'appid');
85
89
  const secretKey = buildEndpointEnvKey('qq', pending.endpointId, 'secret');
86
90
  const intentFields = defaultQqEndpointIntentFields(botKind);
@@ -98,11 +102,7 @@ async function writeEndpointWithBotKind(
98
102
  [secretKey]: pending.appSecret,
99
103
  },
100
104
  });
101
- return (
102
- `✅ 已按 botKind=${intentFields.botKind} 将 endpoint「${pending.endpointId}」写入 .env 与 ${filePath}。\n` +
103
- `intents: ${intentFields.intents.join(', ')}。\n` +
104
- '⚠️ 需重启 zhin 后新 endpoint 才会生效。'
105
- );
105
+ return t(locale, 'endpointSaved', { kind: intentFields.botKind, id: pending.endpointId, file: filePath, intents: intentFields.intents.join(', ') });
106
106
  }
107
107
 
108
108
  /**
@@ -129,10 +129,13 @@ export function runQqEndpointAdd(
129
129
  reply: QqCommandReply,
130
130
  store: EndpointConfigurationStore,
131
131
  input?: unknown,
132
+ locale: Locale = 'zh-CN',
133
+ cancelCommand?: string,
134
+ commandHeads?: readonly string[],
132
135
  ): Promise<string> {
133
136
  if (state.bindFlow || state.pendingBotKind) {
134
137
  return Promise.resolve(
135
- '已有进行中的 QQ 机器人绑定或待确认的公域/私域选择,请先发送 qq endpoint cancel 取消后再试',
138
+ t(locale, 'existingFlow'),
136
139
  );
137
140
  }
138
141
  const endpointId = id?.trim() || undefined;
@@ -155,12 +158,12 @@ export function runQqEndpointAdd(
155
158
  onQrDisplayed: (url) => {
156
159
  // QQ 出站当前仅支持纯文本(富媒体未迁移),二维码只能发链接文本
157
160
  settle(
158
- `请用手机 QQ 打开以下链接完成扫码绑定(二维码图片出站暂未支持,故发送链接):\n${url}`,
161
+ t(locale, 'scanLink', { url }),
159
162
  );
160
163
  },
161
164
  onQrExpired: async () => {
162
165
  try {
163
- await reply('二维码已过期,正在刷新,请扫描新链接…');
166
+ await reply(t(locale, 'qrExpired'));
164
167
  } catch {
165
168
  // same as settle follow-up: best-effort after command scope ends
166
169
  }
@@ -179,12 +182,12 @@ export function runQqEndpointAdd(
179
182
  endpointId: finalName,
180
183
  appId,
181
184
  appSecret,
182
- sessionKey: '',
185
+ sessionKey: '', locale, cancelCommand,
183
186
  },
184
187
  'public',
185
188
  store,
186
189
  ) +
187
- '\n(当前会话无法交互选择公/私域;私域请手动改配置。)',
190
+ t(locale, 'noInteractiveKind'),
188
191
  );
189
192
  return;
190
193
  }
@@ -193,40 +196,40 @@ export function runQqEndpointAdd(
193
196
  endpointId: finalName,
194
197
  appId,
195
198
  appSecret,
196
- sessionKey,
199
+ sessionKey, locale, cancelCommand, commandHeads,
197
200
  };
198
201
  settle(
199
- `✅ 扫码成功!endpoint「${finalName}」。\n` +
200
- QQ_BOT_KIND_PROMPT,
202
+ t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }),
201
203
  );
202
204
  } catch (error) {
203
- settle(`绑定成功但写入配置失败:${error instanceof Error ? error.message : String(error)}`);
205
+ settle(t(locale, 'writeFailed', { error: error instanceof Error ? error.message : String(error) }));
204
206
  }
205
207
  },
206
208
  onFailure: (error) => {
207
209
  state.bindFlow = null;
208
- settle(`❌ QQ 绑定失败:${error.message}`);
210
+ settle(t(locale, 'bindingFailed', { error: error.message }));
209
211
  },
210
212
  },
211
- { source: 'zhin' },
213
+ { source: 'zhin', locale },
212
214
  );
213
- state.bindFlow = { id: endpointId, stop };
215
+ state.bindFlow = { id: endpointId, stop, locale, cancelCommand, commandHeads };
214
216
  });
215
217
  }
216
218
 
217
219
  /** `qq endpoint cancel`:中止进行中的绑定或待选 botKind */
218
- export function runQqEndpointCancel(state: QqRuntimeState): string {
220
+ export function runQqEndpointCancel(state: QqRuntimeState, preference?: Locale): string {
221
+ const locale = resolveLocale(state.pendingBotKind?.locale ?? state.bindFlow?.locale ?? preference);
219
222
  if (state.bindFlow) {
220
223
  state.bindFlow.stop();
221
224
  state.bindFlow = null;
222
- return '已取消进行中的 QQ 绑定流程';
225
+ return t(locale, 'cancelled');
223
226
  }
224
227
  if (state.pendingBotKind) {
225
228
  const name = state.pendingBotKind.endpointId;
226
229
  state.pendingBotKind = null;
227
- return `已取消 endpoint「${name}」的绑定(尚未写入任何文件)`;
230
+ return t(locale, 'pendingCancelled', { name });
228
231
  }
229
- return '当前没有进行中的 QQ 绑定流程';
232
+ return t(locale, 'noFlow');
230
233
  }
231
234
 
232
235
  /** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
@@ -245,19 +248,21 @@ export async function runQqEndpointRemove(
245
248
  export const qqEndpointCommands = createEndpointCommands({
246
249
  ...qqEndpointListSpec,
247
250
  fields: [
248
- { key: 'appid', required: true, env: true, description: 'QQ 机器人 appid' },
249
- { key: 'secret', required: true, env: true, description: 'QQ 机器人 secret' },
251
+ { key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
252
+ { key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
250
253
  ],
251
254
  running: (use) => use(qqRuntimeStateToken).endpoints.values(),
252
- listFooter: (use) => busyFooter(use(qqRuntimeStateToken)),
255
+ listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
256
+ addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
253
257
  addDescription:
254
258
  '手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
255
- bindFlow: ({ id, reply, input, use }) =>
259
+ bindFlow: ({ id, reply, input, use, locale, formatCommand }) =>
256
260
  runQqEndpointAdd(
257
261
  use(qqRuntimeStateToken),
258
262
  id,
259
263
  reply,
260
264
  use(endpointConfigurationStoreToken),
261
- input,
265
+ input, locale, formatCommand?.('qq endpoint cancel'),
266
+ ['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = formatCommand?.(path); return head ? [head] : []; }),
262
267
  ),
263
268
  }, defineCommand);
package/lib/i18n.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const messages: (locale: import("zhin.js").Locale | undefined, key: "bindTaskFailed" | "kindPrompt" | "cancelHint" | "bindingBusy" | "kindBusy" | "endpointSaved" | "existingFlow" | "scanLink" | "qrExpired" | "noInteractiveKind" | "scanned" | "writeFailed" | "bindingFailed" | "cancelled" | "pendingCancelled" | "noFlow" | "kindUnrecognized" | "configFailed", values?: import("zhin.js").MessageValues) => string;
package/lib/i18n.js ADDED
@@ -0,0 +1,72 @@
1
+ import { createMessages } from 'zhin.js/i18n';
2
+ export const messages = createMessages({
3
+ bindTaskFailed: { 'zh-CN': '获取绑定任务失败: {error}', en: 'Could not create the binding task: {error}' },
4
+ "kindPrompt": {
5
+ "zh-CN": "请回复 public(公域)或 private(私域),确认后一次性写入 .env 与配置。",
6
+ "en": "Reply public or private to save .env and configuration together."
7
+ },
8
+ "cancelHint": {
9
+ "zh-CN": "也可发 {command} 取消。",
10
+ "en": "Send {command} to cancel."
11
+ },
12
+ "bindingBusy": {
13
+ "zh-CN": "⚠️ 有进行中的扫码绑定,请先取消当前绑定流程",
14
+ "en": "⚠️ A QR binding is in progress; cancel it before starting another"
15
+ },
16
+ "kindBusy": {
17
+ "zh-CN": "⚠️ endpoint「{id}」等待公域/私域选择,请回复 public/private",
18
+ "en": "⚠️ Endpoint “{id}” is waiting for public/private selection"
19
+ },
20
+ "endpointSaved": {
21
+ "zh-CN": "✅ 已按 botKind={kind} 将 endpoint「{id}」写入 .env 与 {file}。\nintents: {intents}。\n⚠️ 需重启 zhin 后新 endpoint 才会生效。",
22
+ "en": "✅ Saved endpoint “{id}” with botKind={kind} to .env and {file}.\nintents: {intents}.\n⚠️ Restart zhin to activate the new endpoint."
23
+ },
24
+ "existingFlow": {
25
+ "zh-CN": "已有进行中的 QQ 机器人绑定或待确认的公域/私域选择,请先取消后再试",
26
+ "en": "A QQ binding or public/private selection is already in progress; cancel it before trying again"
27
+ },
28
+ "scanLink": {
29
+ "zh-CN": "请用手机 QQ 打开以下链接完成扫码绑定(二维码图片出站暂未支持,故发送链接):\n{url}",
30
+ "en": "Open this link in mobile QQ to scan and bind the bot:\n{url}"
31
+ },
32
+ "qrExpired": {
33
+ "zh-CN": "二维码已过期,正在刷新,请扫描新链接…",
34
+ "en": "The QR code expired. Refreshing; scan the new link…"
35
+ },
36
+ "noInteractiveKind": {
37
+ "zh-CN": "\n(当前会话无法交互选择公/私域;私域请手动改配置。)",
38
+ "en": "\n(This source cannot answer public/private selection. Edit the configuration manually for a private bot.)"
39
+ },
40
+ "scanned": {
41
+ "zh-CN": "✅ 扫码成功!endpoint「{id}」。\n{prompt}",
42
+ "en": "✅ QR binding succeeded for endpoint “{id}”.\n{prompt}"
43
+ },
44
+ "writeFailed": {
45
+ "zh-CN": "绑定成功但写入配置失败:{error}",
46
+ "en": "Binding succeeded but configuration could not be saved: {error}"
47
+ },
48
+ "bindingFailed": {
49
+ "zh-CN": "❌ QQ 绑定失败:{error}",
50
+ "en": "❌ QQ binding failed: {error}"
51
+ },
52
+ "cancelled": {
53
+ "zh-CN": "已取消进行中的 QQ 绑定流程",
54
+ "en": "The active QQ binding was cancelled"
55
+ },
56
+ "pendingCancelled": {
57
+ "zh-CN": "已取消 endpoint「{name}」的绑定(尚未写入任何文件)",
58
+ "en": "Binding for endpoint “{name}” cancelled; no files were written"
59
+ },
60
+ "noFlow": {
61
+ "zh-CN": "当前没有进行中的 QQ 绑定流程",
62
+ "en": "No QQ binding is in progress"
63
+ },
64
+ "kindUnrecognized": {
65
+ "zh-CN": "未识别「{raw}」。{prompt}",
66
+ "en": "Unrecognized answer “{raw}”. {prompt}"
67
+ },
68
+ "configFailed": {
69
+ "zh-CN": "写入配置失败:{error}",
70
+ "en": "Could not save configuration: {error}"
71
+ }
72
+ });
@@ -1,3 +1,4 @@
1
+ import type { Locale } from 'zhin.js/i18n';
1
2
  export interface QqBindCredentials {
2
3
  appId: string;
3
4
  appSecret: string;
@@ -10,6 +11,7 @@ export interface QqBindCallbacks {
10
11
  onQrExpired?: () => void | Promise<void>;
11
12
  }
12
13
  export interface QqBindFlowOptions {
14
+ locale?: Locale;
13
15
  signal?: AbortSignal;
14
16
  source?: string;
15
17
  }
@@ -1,3 +1,4 @@
1
+ import { messages as t } from './i18n.js';
1
2
  /**
2
3
  * QQ Bot 扫码绑定流程(内联,等价 qqbot-connector startQrConnect)
3
4
  */
@@ -60,7 +61,7 @@ export function startQqBindFlow(callbacks, options = {}) {
60
61
  task = await createBindTask();
61
62
  }
62
63
  catch (err) {
63
- throw new Error(`获取绑定任务失败: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
64
+ throw new Error(t(options.locale, 'bindTaskFailed', { error: err instanceof Error ? err.message : String(err) }), { cause: err });
64
65
  }
65
66
  const connectUrl = buildConnectUrl(task.taskId, options.source ?? 'zhin');
66
67
  await callbacks.onQrDisplayed?.(connectUrl);
@@ -1,9 +1,8 @@
1
+ import { type Locale } from 'zhin.js/i18n';
1
2
  import { type QqBotKind } from './qq-intents.js';
2
- /** 从命令/中间件入站消息提取会话键;无法识别时返回 undefined。 */
3
+ /** Bind replies to the complete canonical conversation and actual sender. */
3
4
  export declare function qqCommandSessionKey(input: unknown): string | undefined;
4
- /**
5
- * 解析用户对公域/私域的回复。
6
- * 接受:public / private / 公域 / 私域(大小写不敏感,可带前后空白)。
7
- */
8
- export declare function parseQqBotKindAnswer(raw: unknown): QqBotKind | undefined;
5
+ /** Public/private are stable botKind values; Chinese aliases apply only to Chinese flows. */
6
+ export declare function parseQqBotKindAnswer(raw: unknown, locale?: Locale): QqBotKind | undefined;
7
+ export declare function qqBotKindPrompt(locale?: Locale, cancelCommand?: string): string;
9
8
  export declare const QQ_BOT_KIND_PROMPT: string;
@@ -1,37 +1,37 @@
1
+ import { conversationRefKey } from '@zhin.js/im-contract';
2
+ import { createDeliveryTarget } from 'zhin.js';
3
+ import { resolveLocale } from 'zhin.js/i18n';
4
+ import { messages as t } from './i18n.js';
1
5
  import { parseQqBotKind } from './qq-intents.js';
2
- /** 从命令/中间件入站消息提取会话键;无法识别时返回 undefined。 */
6
+ /** Bind replies to the complete canonical conversation and actual sender. */
3
7
  export function qqCommandSessionKey(input) {
8
+ if (!input || typeof input !== 'object')
9
+ return undefined;
4
10
  const message = input;
5
- const conversation = message?.conversation;
6
- const adapter = conversation?.endpoint?.adapter;
7
- const endpointKey = conversation?.endpoint?.id;
8
- const kind = conversation?.kind;
9
- const id = conversation?.id;
10
- const sender = message?.sender;
11
- if (adapter == null
12
- || endpointKey == null
13
- || (kind !== 'private' && kind !== 'group' && kind !== 'channel')
14
- || typeof id !== 'string'
15
- || !id
16
- || sender == null
17
- || String(sender).trim() === '') {
11
+ if (typeof message.sender?.id !== 'string' || !message.sender.id.trim())
12
+ return undefined;
13
+ try {
14
+ const conversation = createDeliveryTarget({ conversation: message.conversation });
15
+ return JSON.stringify([conversationRefKey(conversation), message.sender.id]);
16
+ }
17
+ catch {
18
18
  return undefined;
19
19
  }
20
- return `${String(adapter)}\0${String(endpointKey)}\0${kind}\0${id}\0${String(sender).trim()}`;
21
20
  }
22
- /**
23
- * 解析用户对公域/私域的回复。
24
- * 接受:public / private / 公域 / 私域(大小写不敏感,可带前后空白)。
25
- */
26
- export function parseQqBotKindAnswer(raw) {
21
+ /** Public/private are stable botKind values; Chinese aliases apply only to Chinese flows. */
22
+ export function parseQqBotKindAnswer(raw, locale) {
27
23
  const text = String(raw ?? '').trim().toLowerCase();
28
24
  if (!text)
29
25
  return undefined;
30
- if (text === '公域' || text === '公开' || text === '1')
31
- return 'public';
32
- if (text === '私域' || text === '2')
33
- return 'private';
26
+ if (resolveLocale(locale) === 'zh-CN') {
27
+ if (text === '公域' || text === '公开' || text === '1')
28
+ return 'public';
29
+ if (text === '私域' || text === '2')
30
+ return 'private';
31
+ }
34
32
  return parseQqBotKind(text);
35
33
  }
36
- export const QQ_BOT_KIND_PROMPT = '请回复 public(公域)或 private(私域),确认后一次性写入 .env 与配置。\n'
37
- + '也可发 qq endpoint cancel 取消。';
34
+ export function qqBotKindPrompt(locale, cancelCommand) {
35
+ return t(locale, 'kindPrompt') + (cancelCommand ? '\n' + t(locale, 'cancelHint', { command: cancelCommand }) : '');
36
+ }
37
+ export const QQ_BOT_KIND_PROMPT = qqBotKindPrompt();
@@ -6,6 +6,9 @@ export interface QqBindFlowHandle {
6
6
  /** 绑定期望的 endpoint id(未指定时为 undefined,成功后取 appId) */
7
7
  id?: string;
8
8
  stop: () => void;
9
+ readonly locale?: import('zhin.js/i18n').Locale;
10
+ readonly cancelCommand?: string;
11
+ readonly commandHeads?: readonly string[];
9
12
  }
10
13
  /**
11
14
  * 扫码成功后暂存在内存:确认公域/私域后再一次性写 .env + yaml。
@@ -17,6 +20,9 @@ export interface QqPendingBotKind {
17
20
  readonly appSecret: string;
18
21
  /** 发起 add 的会话键;仅同会话回复可完成选择 */
19
22
  readonly sessionKey: string;
23
+ readonly locale?: import('zhin.js/i18n').Locale;
24
+ readonly cancelCommand?: string;
25
+ readonly commandHeads?: readonly string[];
20
26
  }
21
27
  export interface QqRuntimeState {
22
28
  /** 当前 generation 已成功创建的 endpoint(id → 描述) */
@@ -1,8 +1,9 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages as t } from "../../lib/i18n.js";
2
3
  import { defineMiddleware } from 'zhin.js/middleware';
3
4
  import { endpointConfigurationStoreToken } from 'zhin.js/adapter';
4
5
  import { completeQqPendingBotKind, } from '../../commands/qq/endpoint/definition.js';
5
- import { parseQqBotKindAnswer, qqCommandSessionKey, QQ_BOT_KIND_PROMPT, } from "../../lib/qq-bot-kind-prompt.js";
6
+ import { parseQqBotKindAnswer, qqCommandSessionKey, qqBotKindPrompt, } from "../../lib/qq-bot-kind-prompt.js";
6
7
  import { qqRuntimeStateToken } from "../../lib/qq-runtime-state.js";
7
8
  /**
8
9
  * 扫码绑定成功后,在同一会话收取 public/private(或 公域/私域)回复并写入 intents。
@@ -26,13 +27,13 @@ export default defineMiddleware({
26
27
  }
27
28
  const raw = String(context.input.content ?? '').trim();
28
29
  // 让 qq endpoint cancel 等命令继续走命令链
29
- if (raw.toLowerCase().startsWith('qq endpoint')) {
30
+ if ([...(pending.commandHeads ?? []), ...(pending.cancelCommand ? [pending.cancelCommand] : [])].some(head => raw.toLowerCase() === head.toLowerCase() || raw.toLowerCase().startsWith(`${head.toLowerCase()} `))) {
30
31
  await next();
31
32
  return;
32
33
  }
33
- const botKind = parseQqBotKindAnswer(raw);
34
+ const botKind = parseQqBotKindAnswer(raw, pending.locale);
34
35
  if (!botKind) {
35
- await context.input.$reply?.(`未识别「${raw}」。${QQ_BOT_KIND_PROMPT}`);
36
+ await context.input.$reply?.(t(pending.locale, 'kindUnrecognized', { raw, prompt: qqBotKindPrompt(pending.locale, pending.cancelCommand) }));
36
37
  return;
37
38
  }
38
39
  try {
@@ -41,7 +42,7 @@ export default defineMiddleware({
41
42
  await context.input.$reply?.(text);
42
43
  }
43
44
  catch (error) {
44
- await context.input.$reply?.(`写入配置失败:${error instanceof Error ? error.message : String(error)}`);
45
+ await context.input.$reply?.(t(pending.locale, 'configFailed', { error: error instanceof Error ? error.message : String(error) }));
45
46
  }
46
47
  },
47
48
  });
@@ -1,3 +1,4 @@
1
+ import { messages as t } from '../../src/i18n.js';
1
2
  import { defineMiddleware } from 'zhin.js/middleware';
2
3
  import type { CommandMessage } from 'zhin.js/command';
3
4
  import { endpointConfigurationStoreToken } from 'zhin.js/adapter';
@@ -7,7 +8,7 @@ import {
7
8
  import {
8
9
  parseQqBotKindAnswer,
9
10
  qqCommandSessionKey,
10
- QQ_BOT_KIND_PROMPT,
11
+ qqBotKindPrompt,
11
12
  } from '../../src/qq-bot-kind-prompt.js';
12
13
  import { qqRuntimeStateToken } from '../../src/qq-runtime-state.js';
13
14
 
@@ -35,15 +36,15 @@ export default defineMiddleware<CommandMessage>({
35
36
 
36
37
  const raw = String(context.input.content ?? '').trim();
37
38
  // 让 qq endpoint cancel 等命令继续走命令链
38
- if (raw.toLowerCase().startsWith('qq endpoint')) {
39
+ if ([...(pending.commandHeads ?? []), ...(pending.cancelCommand ? [pending.cancelCommand] : [])].some(head => raw.toLowerCase() === head.toLowerCase() || raw.toLowerCase().startsWith(`${head.toLowerCase()} `))) {
39
40
  await next();
40
41
  return;
41
42
  }
42
43
 
43
- const botKind = parseQqBotKindAnswer(raw);
44
+ const botKind = parseQqBotKindAnswer(raw, pending.locale);
44
45
  if (!botKind) {
45
46
  await context.input.$reply?.(
46
- `未识别「${raw}」。${QQ_BOT_KIND_PROMPT}`,
47
+ t(pending.locale, 'kindUnrecognized', { raw, prompt: qqBotKindPrompt(pending.locale, pending.cancelCommand) }),
47
48
  );
48
49
  return;
49
50
  }
@@ -58,7 +59,7 @@ export default defineMiddleware<CommandMessage>({
58
59
  await context.input.$reply?.(text);
59
60
  } catch (error) {
60
61
  await context.input.$reply?.(
61
- `写入配置失败:${error instanceof Error ? error.message : String(error)}`,
62
+ t(pending.locale, 'configFailed', { error: error instanceof Error ? error.message : String(error) }),
62
63
  );
63
64
  }
64
65
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-qq",
3
- "version": "1.1.15",
3
+ "version": "1.1.16",
4
4
  "description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,24 +34,24 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "qq-official-bot": "^1.3.3",
37
- "@zhin.js/adapter": "1.1.22",
38
- "@zhin.js/agent-feature": "1.1.9",
39
- "@zhin.js/core": "1.1.50",
40
- "@zhin.js/feature-kit": "1.1.8",
41
- "@zhin.js/host-http": "1.1.9",
42
- "@zhin.js/im-contract": "1.1.6",
37
+ "@zhin.js/adapter": "1.1.23",
38
+ "@zhin.js/agent-feature": "1.1.10",
39
+ "@zhin.js/core": "1.1.51",
40
+ "@zhin.js/feature-kit": "1.1.9",
41
+ "@zhin.js/host-http": "1.1.10",
42
+ "@zhin.js/im-contract": "1.1.7",
43
43
  "@zhin.js/logger": "1.1.2",
44
- "@zhin.js/permission": "1.1.8",
45
- "@zhin.js/skill": "1.1.9",
46
- "@zhin.js/tool": "1.1.9"
44
+ "@zhin.js/permission": "1.1.9",
45
+ "@zhin.js/skill": "1.1.10",
46
+ "@zhin.js/tool": "1.1.10"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "zod": "^4.0.0",
50
- "@zhin.js/adapter": "^1.1.22",
51
- "@zhin.js/command": "^1.1.10",
52
- "@zhin.js/core": "^1.1.50",
53
- "@zhin.js/middleware": "^1.1.9",
54
- "zhin.js": "^1.1.15"
50
+ "@zhin.js/adapter": "^1.1.23",
51
+ "@zhin.js/command": "^1.1.11",
52
+ "@zhin.js/core": "^1.1.51",
53
+ "@zhin.js/middleware": "^1.1.10",
54
+ "zhin.js": "^1.1.16"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@zhin.js/command": {
@@ -72,8 +72,8 @@
72
72
  "typescript": "^6.0.3",
73
73
  "vitest": "^4.1.10",
74
74
  "zod": "^4.4.3",
75
- "@zhin.js/host-http": "1.1.9",
76
- "zhin.js": "1.1.15"
75
+ "@zhin.js/host-http": "1.1.10",
76
+ "zhin.js": "1.1.16"
77
77
  },
78
78
  "files": [
79
79
  "adapters",
package/schema.json CHANGED
@@ -24,14 +24,24 @@
24
24
  "private"
25
25
  ],
26
26
  "default": "public",
27
- "description": "公域/私域:均含 GROUP_AND_C2C_EVENT;频道消息分别为 PUBLIC_GUILD_MESSAGES / GUILD_MESSAGES。未显式配置 intents 时按此展开"
27
+ "description": "公域/私域:均含 GROUP_AND_C2C_EVENT;频道消息分别为 PUBLIC_GUILD_MESSAGES / GUILD_MESSAGES。未显式配置 intents 时按此展开",
28
+ "x-i18n": {
29
+ "en": {
30
+ "description": "Public/private bot type. Both include GROUP_AND_C2C_EVENT; channel messages use PUBLIC_GUILD_MESSAGES or GUILD_MESSAGES respectively. Used to derive intents when they are not explicitly configured."
31
+ }
32
+ }
28
33
  },
29
34
  "master": {
30
35
  "type": [
31
36
  "string",
32
37
  "number"
33
38
  ],
34
- "description": "框架 master(endpoint 管理命令、AI/工具权限);openid。endpoints[i].master 可逐项覆盖"
39
+ "description": "框架 master(endpoint 管理命令、AI/工具权限);openid。endpoints[i].master 可逐项覆盖",
40
+ "x-i18n": {
41
+ "en": {
42
+ "description": "Framework masters (openid) for endpoint management and AI/tool permissions. endpoints[i].master can override this value."
43
+ }
44
+ }
35
45
  },
36
46
  "trusted": {
37
47
  "type": "array",
@@ -42,14 +52,24 @@
42
52
  ],
43
53
  "description": "Trusted openid"
44
54
  },
45
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
55
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
56
+ "x-i18n": {
57
+ "en": {
58
+ "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
59
+ }
60
+ }
46
61
  },
47
62
  "intents": {
48
63
  "type": "array",
49
64
  "items": {
50
65
  "type": "string"
51
66
  },
52
- "description": "WebSocket Identify intents;省略时按 botKind 自动生成(public→含 PUBLIC_GUILD_MESSAGES)"
67
+ "description": "WebSocket Identify intents;省略时按 botKind 自动生成(public→含 PUBLIC_GUILD_MESSAGES)",
68
+ "x-i18n": {
69
+ "en": {
70
+ "description": "WebSocket Identify intents. When omitted, derived from botKind; public includes PUBLIC_GUILD_MESSAGES."
71
+ }
72
+ }
53
73
  },
54
74
  "accessTokenUrl": {
55
75
  "type": "string"
@@ -94,7 +114,12 @@
94
114
  "string",
95
115
  "number"
96
116
  ],
97
- "description": "本 endpoint 的框架 master(openid);覆盖顶层 master"
117
+ "description": "本 endpoint 的框架 master(openid);覆盖顶层 master",
118
+ "x-i18n": {
119
+ "en": {
120
+ "description": "Framework masters for this endpoint (openid), overriding the top-level master list."
121
+ }
122
+ }
98
123
  },
99
124
  "trusted": {
100
125
  "type": "array",
@@ -105,7 +130,12 @@
105
130
  ],
106
131
  "description": "Trusted openid"
107
132
  },
108
- "description": "本 endpoint 的 trusted 列表"
133
+ "description": "本 endpoint 的 trusted 列表",
134
+ "x-i18n": {
135
+ "en": {
136
+ "description": "Trusted users for this endpoint."
137
+ }
138
+ }
109
139
  },
110
140
  "botKind": {
111
141
  "type": "string",
@@ -113,14 +143,24 @@
113
143
  "public",
114
144
  "private"
115
145
  ],
116
- "description": "覆盖顶层 botKind(公域/私域),用于按 endpoint 生成 intents"
146
+ "description": "覆盖顶层 botKind(公域/私域),用于按 endpoint 生成 intents",
147
+ "x-i18n": {
148
+ "en": {
149
+ "description": "Override top-level botKind (public/private) when deriving this endpoint's intents."
150
+ }
151
+ }
117
152
  },
118
153
  "intents": {
119
154
  "type": "array",
120
155
  "items": {
121
156
  "type": "string"
122
157
  },
123
- "description": "覆盖顶层 intents"
158
+ "description": "覆盖顶层 intents",
159
+ "x-i18n": {
160
+ "en": {
161
+ "description": "Override top-level intents."
162
+ }
163
+ }
124
164
  },
125
165
  "accessTokenUrl": {
126
166
  "type": "string"
@@ -164,12 +204,22 @@
164
204
  "appid",
165
205
  "secret"
166
206
  ]
207
+ },
208
+ "x-i18n": {
209
+ "en": {
210
+ "description": "Multiple accounts: each entry defines an endpoint. id, appid and secret are required; other fields override instance defaults."
211
+ }
167
212
  }
168
213
  },
169
214
  "commandPrefix": {
170
215
  "type": "string",
171
216
  "default": "",
172
- "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
217
+ "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖",
218
+ "x-i18n": {
219
+ "en": {
220
+ "description": "Command prefix. The default empty string allows unprefixed commands; '/' requires a leading slash. Each endpoints[i] can override it."
221
+ }
222
+ }
173
223
  },
174
224
  "streamProxy": {
175
225
  "type": "object",
package/src/i18n.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { createMessages } from 'zhin.js/i18n';
2
+
3
+ export const messages = createMessages({
4
+ bindTaskFailed: { 'zh-CN': '获取绑定任务失败: {error}', en: 'Could not create the binding task: {error}' },
5
+ "kindPrompt": {
6
+ "zh-CN": "请回复 public(公域)或 private(私域),确认后一次性写入 .env 与配置。",
7
+ "en": "Reply public or private to save .env and configuration together."
8
+ },
9
+ "cancelHint": {
10
+ "zh-CN": "也可发 {command} 取消。",
11
+ "en": "Send {command} to cancel."
12
+ },
13
+ "bindingBusy": {
14
+ "zh-CN": "⚠️ 有进行中的扫码绑定,请先取消当前绑定流程",
15
+ "en": "⚠️ A QR binding is in progress; cancel it before starting another"
16
+ },
17
+ "kindBusy": {
18
+ "zh-CN": "⚠️ endpoint「{id}」等待公域/私域选择,请回复 public/private",
19
+ "en": "⚠️ Endpoint “{id}” is waiting for public/private selection"
20
+ },
21
+ "endpointSaved": {
22
+ "zh-CN": "✅ 已按 botKind={kind} 将 endpoint「{id}」写入 .env 与 {file}。\nintents: {intents}。\n⚠️ 需重启 zhin 后新 endpoint 才会生效。",
23
+ "en": "✅ Saved endpoint “{id}” with botKind={kind} to .env and {file}.\nintents: {intents}.\n⚠️ Restart zhin to activate the new endpoint."
24
+ },
25
+ "existingFlow": {
26
+ "zh-CN": "已有进行中的 QQ 机器人绑定或待确认的公域/私域选择,请先取消后再试",
27
+ "en": "A QQ binding or public/private selection is already in progress; cancel it before trying again"
28
+ },
29
+ "scanLink": {
30
+ "zh-CN": "请用手机 QQ 打开以下链接完成扫码绑定(二维码图片出站暂未支持,故发送链接):\n{url}",
31
+ "en": "Open this link in mobile QQ to scan and bind the bot:\n{url}"
32
+ },
33
+ "qrExpired": {
34
+ "zh-CN": "二维码已过期,正在刷新,请扫描新链接…",
35
+ "en": "The QR code expired. Refreshing; scan the new link…"
36
+ },
37
+ "noInteractiveKind": {
38
+ "zh-CN": "\n(当前会话无法交互选择公/私域;私域请手动改配置。)",
39
+ "en": "\n(This source cannot answer public/private selection. Edit the configuration manually for a private bot.)"
40
+ },
41
+ "scanned": {
42
+ "zh-CN": "✅ 扫码成功!endpoint「{id}」。\n{prompt}",
43
+ "en": "✅ QR binding succeeded for endpoint “{id}”.\n{prompt}"
44
+ },
45
+ "writeFailed": {
46
+ "zh-CN": "绑定成功但写入配置失败:{error}",
47
+ "en": "Binding succeeded but configuration could not be saved: {error}"
48
+ },
49
+ "bindingFailed": {
50
+ "zh-CN": "❌ QQ 绑定失败:{error}",
51
+ "en": "❌ QQ binding failed: {error}"
52
+ },
53
+ "cancelled": {
54
+ "zh-CN": "已取消进行中的 QQ 绑定流程",
55
+ "en": "The active QQ binding was cancelled"
56
+ },
57
+ "pendingCancelled": {
58
+ "zh-CN": "已取消 endpoint「{name}」的绑定(尚未写入任何文件)",
59
+ "en": "Binding for endpoint “{name}” cancelled; no files were written"
60
+ },
61
+ "noFlow": {
62
+ "zh-CN": "当前没有进行中的 QQ 绑定流程",
63
+ "en": "No QQ binding is in progress"
64
+ },
65
+ "kindUnrecognized": {
66
+ "zh-CN": "未识别「{raw}」。{prompt}",
67
+ "en": "Unrecognized answer “{raw}”. {prompt}"
68
+ },
69
+ "configFailed": {
70
+ "zh-CN": "写入配置失败:{error}",
71
+ "en": "Could not save configuration: {error}"
72
+ }
73
+ });
@@ -1,3 +1,5 @@
1
+ import type { Locale } from 'zhin.js/i18n';
2
+ import { messages as t } from './i18n.js';
1
3
  /**
2
4
  * QQ Bot 扫码绑定流程(内联,等价 qqbot-connector startQrConnect)
3
5
  */
@@ -25,6 +27,7 @@ export interface QqBindCallbacks {
25
27
  }
26
28
 
27
29
  export interface QqBindFlowOptions {
30
+ locale?: Locale;
28
31
  signal?: AbortSignal;
29
32
  source?: string;
30
33
  }
@@ -99,7 +102,7 @@ export function startQqBindFlow(
99
102
  task = await createBindTask();
100
103
  } catch (err) {
101
104
  throw new Error(
102
- `获取绑定任务失败: ${err instanceof Error ? err.message : String(err)}`,
105
+ t(options.locale, 'bindTaskFailed', { error: err instanceof Error ? err.message : String(err) }),
103
106
  { cause: err },
104
107
  );
105
108
  }
@@ -1,47 +1,32 @@
1
+ import { conversationRefKey } from '@zhin.js/im-contract';
2
+ import { createDeliveryTarget } from 'zhin.js';
3
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
4
+ import { messages as t } from './i18n.js';
1
5
  import { parseQqBotKind, type QqBotKind } from './qq-intents.js';
2
6
 
3
- /** 从命令/中间件入站消息提取会话键;无法识别时返回 undefined。 */
7
+ /** Bind replies to the complete canonical conversation and actual sender. */
4
8
  export function qqCommandSessionKey(input: unknown): string | undefined {
5
- const message = input as {
6
- conversation?: {
7
- endpoint?: { id?: unknown; adapter?: unknown };
8
- kind?: unknown;
9
- id?: unknown;
10
- };
11
- sender?: unknown;
12
- } | null | undefined;
13
- const conversation = message?.conversation;
14
- const adapter = conversation?.endpoint?.adapter;
15
- const endpointKey = conversation?.endpoint?.id;
16
- const kind = conversation?.kind;
17
- const id = conversation?.id;
18
- const sender = message?.sender;
19
- if (
20
- adapter == null
21
- || endpointKey == null
22
- || (kind !== 'private' && kind !== 'group' && kind !== 'channel')
23
- || typeof id !== 'string'
24
- || !id
25
- || sender == null
26
- || String(sender).trim() === ''
27
- ) {
28
- return undefined;
29
- }
30
- return `${String(adapter)}\0${String(endpointKey)}\0${kind}\0${id}\0${String(sender).trim()}`;
9
+ if (!input || typeof input !== 'object') return undefined;
10
+ const message = input as { conversation?: unknown; sender?: { id?: unknown } };
11
+ if (typeof message.sender?.id !== 'string' || !message.sender.id.trim()) return undefined;
12
+ try {
13
+ const conversation = createDeliveryTarget({ conversation: message.conversation as Parameters<typeof createDeliveryTarget>[0]['conversation'] });
14
+ return JSON.stringify([conversationRefKey(conversation), message.sender.id]);
15
+ } catch { return undefined; }
31
16
  }
32
17
 
33
- /**
34
- * 解析用户对公域/私域的回复。
35
- * 接受:public / private / 公域 / 私域(大小写不敏感,可带前后空白)。
36
- */
37
- export function parseQqBotKindAnswer(raw: unknown): QqBotKind | undefined {
18
+ /** Public/private are stable botKind values; Chinese aliases apply only to Chinese flows. */
19
+ export function parseQqBotKindAnswer(raw: unknown, locale?: Locale): QqBotKind | undefined {
38
20
  const text = String(raw ?? '').trim().toLowerCase();
39
21
  if (!text) return undefined;
40
- if (text === '公域' || text === '公开' || text === '1') return 'public';
41
- if (text === '私域' || text === '2') return 'private';
22
+ if (resolveLocale(locale) === 'zh-CN') {
23
+ if (text === '公域' || text === '公开' || text === '1') return 'public';
24
+ if (text === '私域' || text === '2') return 'private';
25
+ }
42
26
  return parseQqBotKind(text);
43
27
  }
44
28
 
45
- export const QQ_BOT_KIND_PROMPT =
46
- '请回复 public(公域)或 private(私域),确认后一次性写入 .env 与配置。\n'
47
- + '也可发 qq endpoint cancel 取消。';
29
+ export function qqBotKindPrompt(locale?: Locale, cancelCommand?: string): string {
30
+ return t(locale, 'kindPrompt') + (cancelCommand ? '\n' + t(locale, 'cancelHint', { command: cancelCommand }) : '');
31
+ }
32
+ export const QQ_BOT_KIND_PROMPT = qqBotKindPrompt();
@@ -14,6 +14,9 @@ export interface QqBindFlowHandle {
14
14
  /** 绑定期望的 endpoint id(未指定时为 undefined,成功后取 appId) */
15
15
  id?: string;
16
16
  stop: () => void;
17
+ readonly locale?: import('zhin.js/i18n').Locale;
18
+ readonly cancelCommand?: string;
19
+ readonly commandHeads?: readonly string[];
17
20
  }
18
21
 
19
22
  /**
@@ -26,6 +29,9 @@ export interface QqPendingBotKind {
26
29
  readonly appSecret: string;
27
30
  /** 发起 add 的会话键;仅同会话回复可完成选择 */
28
31
  readonly sessionKey: string;
32
+ readonly locale?: import('zhin.js/i18n').Locale;
33
+ readonly cancelCommand?: string;
34
+ readonly commandHeads?: readonly string[];
29
35
  }
30
36
 
31
37
  export interface QqRuntimeState {