@xmanrui/dsh-im 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.en.md +16 -4
  2. package/README.md +16 -4
  3. package/lib/index.js +208 -178
  4. package/package.json +1 -1
  5. package/plugin-src/host/index.mjs +2 -0
  6. package/src/channels/dingtalk/config-store.mjs +5 -3
  7. package/src/channels/dingtalk/dingtalk-api.mjs +2 -1
  8. package/src/channels/dingtalk/dingtalk-bridge.mjs +45 -40
  9. package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
  10. package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
  11. package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
  12. package/src/channels/dingtalk/state-store.mjs +3 -1
  13. package/src/channels/discord/config-store.mjs +2 -1
  14. package/src/channels/discord/discord-api.mjs +25 -1
  15. package/src/channels/discord/discord-runtime.mjs +237 -11
  16. package/src/channels/feishu/bridge.mjs +131 -123
  17. package/src/channels/feishu/feishu-cards.mjs +62 -54
  18. package/src/channels/feishu/feishu-channel.mjs +5 -4
  19. package/src/channels/feishu/feishu-runtime.mjs +10 -9
  20. package/src/channels/feishu/message-utils.mjs +4 -3
  21. package/src/channels/office/office-job-executor.mjs +15 -14
  22. package/src/channels/office/office-runtime.mjs +6 -5
  23. package/src/channels/qq/config-store.mjs +3 -1
  24. package/src/channels/qq/markdown-reply.mjs +4 -2
  25. package/src/channels/qq/qq-bridge.mjs +57 -52
  26. package/src/channels/qq/qq-controller.mjs +17 -16
  27. package/src/channels/qq/qq-runtime.mjs +3 -2
  28. package/src/channels/shared/agent-preset.mjs +3 -1
  29. package/src/channels/shared/compact-command.mjs +15 -14
  30. package/src/channels/shared/connection-test.mjs +6 -4
  31. package/src/channels/shared/control-command.mjs +13 -11
  32. package/src/channels/shared/editable-message-stream.mjs +4 -2
  33. package/src/channels/shared/harness-approval.mjs +23 -21
  34. package/src/channels/shared/harness-client.mjs +3 -2
  35. package/src/channels/shared/harness-question.mjs +8 -6
  36. package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
  37. package/src/channels/shared/i18n-en/discord.mjs +8 -0
  38. package/src/channels/shared/i18n-en/feishu.mjs +217 -0
  39. package/src/channels/shared/i18n-en/office.mjs +23 -0
  40. package/src/channels/shared/i18n-en/qq.mjs +54 -0
  41. package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
  42. package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
  43. package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
  44. package/src/channels/shared/i18n-en/slack.mjs +27 -0
  45. package/src/channels/shared/i18n-en/telegram.mjs +21 -0
  46. package/src/channels/shared/i18n-en/wecom.mjs +38 -0
  47. package/src/channels/shared/i18n-en/weixin.mjs +80 -0
  48. package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
  49. package/src/channels/shared/i18n-en.mjs +35 -0
  50. package/src/channels/shared/i18n.mjs +35 -0
  51. package/src/channels/shared/image-prompt.mjs +17 -15
  52. package/src/channels/shared/inbound-file.mjs +5 -3
  53. package/src/channels/shared/model-command.mjs +35 -31
  54. package/src/channels/shared/preset-command.mjs +41 -38
  55. package/src/channels/shared/semantic/artifact-delivery.mjs +1 -1
  56. package/src/channels/shared/semantic/delivery.mjs +38 -0
  57. package/src/channels/shared/text-harness-bridge.mjs +130 -63
  58. package/src/channels/shared/token-bot-controller.mjs +32 -11
  59. package/src/channels/shared/workspace-command.mjs +69 -58
  60. package/src/channels/slack/config-store.mjs +3 -1
  61. package/src/channels/slack/slack-api.mjs +6 -5
  62. package/src/channels/slack/slack-controller.mjs +10 -9
  63. package/src/channels/slack/slack-runtime.mjs +16 -4
  64. package/src/channels/telegram/config-store.mjs +2 -1
  65. package/src/channels/telegram/telegram-api.mjs +72 -6
  66. package/src/channels/telegram/telegram-rich-message.mjs +147 -0
  67. package/src/channels/telegram/telegram-runtime.mjs +373 -5
  68. package/src/channels/wecom/config-store.mjs +3 -1
  69. package/src/channels/wecom/wecom-bridge.mjs +59 -55
  70. package/src/channels/wecom/wecom-controller.mjs +20 -19
  71. package/src/channels/wecom/wecom-runtime.mjs +2 -1
  72. package/src/channels/weixin/config-store.mjs +2 -1
  73. package/src/channels/weixin/weixin-bridge.mjs +50 -49
  74. package/src/channels/weixin/weixin-controller.mjs +22 -21
  75. package/src/channels/weixin/weixin-runtime.mjs +3 -2
  76. package/src/channels/whatsapp/config-store.mjs +3 -1
  77. package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
  78. package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
  79. package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -4
  80. package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
@@ -0,0 +1,35 @@
1
+ // English translations for host-side user-facing text.
2
+ // Keys are the exact Chinese literals passed to t() in src/channels/**.
3
+ // Chinese output is the identity default and needs no entries here.
4
+ // Entries are maintained per area in ./i18n-en/*.mjs and merged here.
5
+
6
+ import sharedA from './i18n-en/shared-a.mjs';
7
+ import sharedB from './i18n-en/shared-b.mjs';
8
+ import sharedC from './i18n-en/shared-c.mjs';
9
+ import feishu from './i18n-en/feishu.mjs';
10
+ import dingtalk from './i18n-en/dingtalk.mjs';
11
+ import wecom from './i18n-en/wecom.mjs';
12
+ import qq from './i18n-en/qq.mjs';
13
+ import weixin from './i18n-en/weixin.mjs';
14
+ import slack from './i18n-en/slack.mjs';
15
+ import telegram from './i18n-en/telegram.mjs';
16
+ import discord from './i18n-en/discord.mjs';
17
+ import whatsapp from './i18n-en/whatsapp.mjs';
18
+ import office from './i18n-en/office.mjs';
19
+
20
+ export const EN = Object.freeze(Object.assign(
21
+ {},
22
+ sharedA,
23
+ sharedB,
24
+ sharedC,
25
+ feishu,
26
+ dingtalk,
27
+ wecom,
28
+ qq,
29
+ weixin,
30
+ slack,
31
+ telegram,
32
+ discord,
33
+ whatsapp,
34
+ office,
35
+ ));
@@ -0,0 +1,35 @@
1
+ // Host-side i18n for dsh-im. Mirrors the conventions of the settings-UI
2
+ // translator in plugin-src/client/i18n.js: dictionary keys are the exact
3
+ // Chinese source literals, and Chinese (zh) is the identity default, so
4
+ // untranslated or unrecognized text always falls back to the original
5
+ // Chinese output unchanged.
6
+ //
7
+ // The English dictionary lives in ./i18n-en.mjs to keep this module small.
8
+
9
+ import { EN } from './i18n-en.mjs';
10
+
11
+ let language = 'zh';
12
+
13
+ // Accepts 'en', 'en-US', 'english' (any case) as English; anything else
14
+ // (including undefined and unrecognized values) selects Chinese.
15
+ export function setImHostLanguage(lang) {
16
+ const normalized = typeof lang === 'string' ? lang.trim().toLowerCase() : '';
17
+ language = normalized === 'english' || /^en(?:[-_].*)?$/u.test(normalized) ? 'en' : 'zh';
18
+ }
19
+
20
+ export function getImHostLanguage() {
21
+ return language;
22
+ }
23
+
24
+ // Translate a user-facing Chinese literal. In zh mode (the default) this is
25
+ // the identity function. Optional `params` fills `{name}` placeholders in
26
+ // both the Chinese key and its translation, e.g.
27
+ // t('共 {count} 个机器人', { count: 3 })
28
+ export function t(text, params) {
29
+ if (typeof text !== 'string') return text;
30
+ const translated = language === 'en' ? EN[text] ?? text : text;
31
+ if (params == null) return translated;
32
+ return translated.replace(/\{(\w+)\}/g, (match, name) =>
33
+ Object.hasOwn(params, name) ? String(params[name]) : match,
34
+ );
35
+ }
@@ -1,3 +1,5 @@
1
+ import { t } from './i18n.mjs';
2
+
1
3
  const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
2
4
  const DEFAULT_MAX_IMAGES = 20;
3
5
  const DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
@@ -67,7 +69,7 @@ export async function fetchImageBuffer(url, {
67
69
  throw new ImagePromptError(
68
70
  'image-redirect-blocked',
69
71
  `Image download redirect was blocked (HTTP ${response.status})`,
70
- '图片下载地址发生了重定向,暂时无法读取。',
72
+ t('图片下载地址发生了重定向,暂时无法读取。'),
71
73
  );
72
74
  }
73
75
  if (!response?.ok) {
@@ -75,7 +77,7 @@ export async function fetchImageBuffer(url, {
75
77
  throw new ImagePromptError(
76
78
  'image-http-error',
77
79
  `Image download failed with HTTP ${response?.status ?? 'unknown'}`,
78
- `图片下载失败(HTTP ${response?.status ?? 'unknown'}),请重新发送后再试。`,
80
+ t('图片下载失败(HTTP {status}),请重新发送后再试。', { status: response?.status ?? 'unknown' }),
79
81
  );
80
82
  }
81
83
  const declaredLength = Number(response.headers?.get?.('content-length'));
@@ -84,7 +86,7 @@ export async function fetchImageBuffer(url, {
84
86
  throw new ImagePromptError(
85
87
  'image-too-large',
86
88
  `Image response declares ${declaredLength} bytes; the limit is ${maxBytes}`,
87
- '图片超过 5 MB,请压缩后重试。',
89
+ t('图片超过 5 MB,请压缩后重试。'),
88
90
  );
89
91
  }
90
92
 
@@ -99,7 +101,7 @@ export async function fetchImageBuffer(url, {
99
101
  throw new ImagePromptError(
100
102
  'image-too-large',
101
103
  `Image response exceeded ${maxBytes} bytes`,
102
- '图片超过 5 MB,请压缩后重试。',
104
+ t('图片超过 5 MB,请压缩后重试。'),
103
105
  );
104
106
  }
105
107
  chunks.push(data);
@@ -112,7 +114,7 @@ export async function fetchImageBuffer(url, {
112
114
  throw new ImagePromptError(
113
115
  'image-too-large',
114
116
  `Image response contains ${data.length} bytes; the limit is ${maxBytes}`,
115
- '图片超过 5 MB,请压缩后重试。',
117
+ t('图片超过 5 MB,请压缩后重试。'),
116
118
  );
117
119
  }
118
120
  return data;
@@ -192,7 +194,7 @@ export async function promptContentForMessage(message, {
192
194
  throw new ImagePromptError(
193
195
  'too-many-images',
194
196
  `Image message contains ${sources.length} images; the limit is ${maxImages}`,
195
- `一次最多只能处理 ${maxImages} 张图片。`,
197
+ t('一次最多只能处理 {maxImages} 张图片。', { maxImages }),
196
198
  );
197
199
  }
198
200
 
@@ -200,7 +202,7 @@ export async function promptContentForMessage(message, {
200
202
  const content = [];
201
203
  let totalImageBytes = 0;
202
204
  if (text) content.push({ type: 'text', text });
203
- else if (sources.length > 0) content.push({ type: 'text', text: DEFAULT_IMAGE_PROMPT });
205
+ else if (sources.length > 0) content.push({ type: 'text', text: t(DEFAULT_IMAGE_PROMPT) });
204
206
 
205
207
  for (const [index, source] of sources.entries()) {
206
208
  signal?.throwIfAborted();
@@ -208,14 +210,14 @@ export async function promptContentForMessage(message, {
208
210
  throw new ImagePromptError(
209
211
  'image-too-large',
210
212
  `Image ${index + 1} declares ${source.size} bytes; the limit is ${maxImageBytes}`,
211
- '图片超过 5 MB,请压缩后重试。',
213
+ t('图片超过 5 MB,请压缩后重试。'),
212
214
  );
213
215
  }
214
216
  if (Number.isFinite(source?.size) && totalImageBytes + source.size > maxTotalImageBytes) {
215
217
  throw new ImagePromptError(
216
218
  'images-too-large',
217
219
  `Images declare more than ${maxTotalImageBytes} bytes in total`,
218
- '一次发送的图片总大小过大,请减少图片数量或压缩后重试。',
220
+ t('一次发送的图片总大小过大,请减少图片数量或压缩后重试。'),
219
221
  );
220
222
  }
221
223
 
@@ -230,7 +232,7 @@ export async function promptContentForMessage(message, {
230
232
  throw new ImagePromptError(
231
233
  'image-download-failed',
232
234
  `Unable to download image ${index + 1}: ${error?.message ?? String(error)}`,
233
- '图片下载失败,请重新发送后再试。',
235
+ t('图片下载失败,请重新发送后再试。'),
234
236
  { cause: error },
235
237
  );
236
238
  }
@@ -239,21 +241,21 @@ export async function promptContentForMessage(message, {
239
241
  throw new ImagePromptError(
240
242
  'invalid-image-data',
241
243
  `Image ${index + 1} returned no data`,
242
- '未能读取图片内容,请重新发送。',
244
+ t('未能读取图片内容,请重新发送。'),
243
245
  );
244
246
  }
245
247
  if (loaded.data.length > maxImageBytes) {
246
248
  throw new ImagePromptError(
247
249
  'image-too-large',
248
250
  `Image ${index + 1} contains ${loaded.data.length} bytes; the limit is ${maxImageBytes}`,
249
- '图片超过 5 MB,请压缩后重试。',
251
+ t('图片超过 5 MB,请压缩后重试。'),
250
252
  );
251
253
  }
252
254
  if (totalImageBytes + loaded.data.length > maxTotalImageBytes) {
253
255
  throw new ImagePromptError(
254
256
  'images-too-large',
255
257
  `Images contain more than ${maxTotalImageBytes} bytes in total`,
256
- '一次发送的图片总大小过大,请减少图片数量或压缩后重试。',
258
+ t('一次发送的图片总大小过大,请减少图片数量或压缩后重试。'),
257
259
  );
258
260
  }
259
261
  totalImageBytes += loaded.data.length;
@@ -262,7 +264,7 @@ export async function promptContentForMessage(message, {
262
264
  throw new ImagePromptError(
263
265
  'unsupported-image-type',
264
266
  `Image ${index + 1} is not JPEG, PNG, GIF, or WebP`,
265
- '暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。',
267
+ t('暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。'),
266
268
  );
267
269
  }
268
270
  content.push({
@@ -289,7 +291,7 @@ export function imagePromptDiagnostic(error) {
289
291
  }
290
292
  const reason = error.details.reason;
291
293
  const userMessage = Object.hasOwn(HOST_ATTACHMENT_USER_MESSAGES, reason)
292
- ? HOST_ATTACHMENT_USER_MESSAGES[reason]
294
+ ? t(HOST_ATTACHMENT_USER_MESSAGES[reason])
293
295
  : null;
294
296
  return userMessage ? { code: 'attachment-error', reason, userMessage } : null;
295
297
  }
@@ -3,10 +3,12 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
3
  import { isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { pipeline } from 'node:stream/promises';
5
5
 
6
+ import { t } from './i18n.mjs';
7
+
6
8
  const FILES_DIRECTORY = join('.dsh-im', 'inbound');
7
9
 
8
10
  export class InboundFileError extends Error {
9
- constructor(code, message, userMessage = '文件接收失败,请重新发送后再试。', options = {}) {
11
+ constructor(code, message, userMessage = t('文件接收失败,请重新发送后再试。'), options = {}) {
10
12
  super(message, options);
11
13
  this.name = 'InboundFileError';
12
14
  this.code = code;
@@ -124,7 +126,7 @@ export async function stageInboundFiles(message, {
124
126
  throw new InboundFileError(
125
127
  'inbound-file-download-failed',
126
128
  `Unable to download inbound file ${index + 1}: ${error?.message ?? String(error)}`,
127
- '文件下载失败,请重新发送后再试。',
129
+ t('文件下载失败,请重新发送后再试。'),
128
130
  { cause: error },
129
131
  );
130
132
  }
@@ -152,7 +154,7 @@ export async function stageInboundFiles(message, {
152
154
  throw new InboundFileError(
153
155
  'inbound-file-download-failed',
154
156
  `Unable to stream inbound file ${index + 1}: ${error?.message ?? String(error)}`,
155
- '文件下载失败,请重新发送后再试。',
157
+ t('文件下载失败,请重新发送后再试。'),
156
158
  { cause: error },
157
159
  );
158
160
  }
@@ -1,4 +1,5 @@
1
1
  import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
2
+ import { t } from './i18n.mjs';
2
3
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
3
4
  import { withSessionBindingLock } from './session-binding-lock.mjs';
4
5
 
@@ -109,9 +110,9 @@ function modelNumberRequest(requested) {
109
110
 
110
111
  function invalidModelNumberMessage(requested) {
111
112
  return [
112
- `模型序号无效:${safeDisplayText(requested)}`,
113
+ t('模型序号无效:{input}', { input: safeDisplayText(requested) }),
113
114
  '',
114
- '请发送 /models 查看并输入有效的正整数序号。',
115
+ t('请发送 /models 查看并输入有效的正整数序号。'),
115
116
  ].join('\n');
116
117
  }
117
118
 
@@ -119,43 +120,43 @@ function formatCatalog(catalog) {
119
120
  const currentId = catalog.current
120
121
  ? modelId(catalog.current.provider, catalog.current.model)
121
122
  : null;
122
- const lines = ['可用模型:'];
123
+ const lines = [t('可用模型:')];
123
124
  let index = 0;
124
- if (catalog.groups.length === 0) lines.push('', '当前没有可用模型。');
125
+ if (catalog.groups.length === 0) lines.push('', t('当前没有可用模型。'));
125
126
  for (const group of catalog.groups) {
126
127
  lines.push('', safeDisplayText(group.name) || safeDisplayText(group.id));
127
128
  for (const model of group.models) {
128
129
  index += 1;
129
130
  const id = modelId(group.id, model.id);
130
- lines.push(`${index}. ${safeDisplayText(id)}${id === currentId ? '(当前)' : ''}`);
131
+ lines.push(`${index}. ${safeDisplayText(id)}${id === currentId ? t('(当前)') : ''}`);
131
132
  }
132
133
  }
133
134
  if (catalog.failures.length > 0) {
134
- lines.push('', '以下模型提供方暂时不可用:');
135
+ lines.push('', t('以下模型提供方暂时不可用:'));
135
136
  for (const failure of catalog.failures) {
136
137
  lines.push(`- ${safeDisplayText(failure.name) || safeDisplayText(failure.id)}`);
137
138
  }
138
139
  }
139
- if (index > 0) lines.push('', '切换模型:/model <序号>');
140
+ if (index > 0) lines.push('', t('切换模型:/model <序号>'));
140
141
  return lines.join('\n');
141
142
  }
142
143
 
143
144
  function currentModelMessage(current) {
144
145
  return [
145
- '当前模型:',
146
+ t('当前模型:'),
146
147
  modelId(current.provider, current.model),
147
148
  '',
148
- '查看全部模型:/models',
149
- '切换模型:/model <序号>',
149
+ t('查看全部模型:/models'),
150
+ t('切换模型:/model <序号>'),
150
151
  ].join('\n');
151
152
  }
152
153
 
153
154
  function noSessionMessage() {
154
155
  return [
155
- '当前聊天还没有会话。',
156
+ t('当前聊天还没有会话。'),
156
157
  '',
157
- '查看模型:/models',
158
- '选择模型:/model <序号>',
158
+ t('查看模型:/models'),
159
+ t('选择模型:/model <序号>'),
159
160
  ].join('\n');
160
161
  }
161
162
 
@@ -166,26 +167,26 @@ function errorCode(error) {
166
167
  function modelErrorMessage(error, action) {
167
168
  const code = errorCode(error);
168
169
  if (code === 'agent-busy') {
169
- return '当前任务正在运行,请等待完成或先发送 /stop。';
170
+ return t('当前任务正在运行,请等待完成或先发送 /stop。');
170
171
  }
171
172
  if (code === 'session-not-found') {
172
- return '当前聊天绑定的会话已不存在,请重试。';
173
+ return t('当前聊天绑定的会话已不存在,请重试。');
173
174
  }
174
175
  if (code === 'model-unavailable') {
175
- return '无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。';
176
+ return t('无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。');
176
177
  }
177
178
  if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
178
- return '工作区或机器人状态已发生变化,请重试。';
179
+ return t('工作区或机器人状态已发生变化,请重试。');
179
180
  }
180
181
  if (code === SESSION_BINDING_CHANGED) {
181
- return '当前聊天绑定的会话已发生变化,请重试。';
182
+ return t('当前聊天绑定的会话已发生变化,请重试。');
182
183
  }
183
184
  if (code === 'cancelled' || error?.name === 'AbortError') {
184
- return action === 'list' ? '获取模型列表已取消。' : '模型切换已取消。';
185
+ return action === 'list' ? t('获取模型列表已取消。') : t('模型切换已取消。');
185
186
  }
186
187
  return action === 'list'
187
- ? '暂时无法获取模型列表,请稍后重试。'
188
- : '模型切换失败,请稍后重试。';
188
+ ? t('暂时无法获取模型列表,请稍后重试。')
189
+ : t('模型切换失败,请稍后重试。');
189
190
  }
190
191
 
191
192
  async function boundSession(harness, state, key, options) {
@@ -243,12 +244,12 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
243
244
  if (!isModelCommand(text)) return null;
244
245
  const command = text.trim();
245
246
  if (options.hasImages) {
246
- return commandResult('模型命令仅支持纯文字,请移除图片后重试。');
247
+ return commandResult(t('模型命令仅支持纯文字,请移除图片后重试。'));
247
248
  }
248
249
  const requestOptions = rpcOptions(options.signal);
249
250
 
250
251
  if (isModelsCommand(command)) {
251
- if (!/^\/models[ \t]*$/iu.test(command)) return commandResult(MODELS_USAGE);
252
+ if (!/^\/models[ \t]*$/iu.test(command)) return commandResult(t(MODELS_USAGE));
252
253
  try {
253
254
  const bound = await boundSession(harness, state, key, requestOptions);
254
255
  const catalog = bound
@@ -261,7 +262,7 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
261
262
  }
262
263
 
263
264
  const match = /^\/model(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
264
- if (!match) return commandResult(MODEL_USAGE);
265
+ if (!match) return commandResult(t(MODEL_USAGE));
265
266
  const requested = match[1];
266
267
  if (!requested) {
267
268
  try {
@@ -279,13 +280,13 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
279
280
  }
280
281
  if (!numberRequest
281
282
  && (!requested.includes('/') || requested.startsWith('/') || requested.endsWith('/'))) {
282
- return commandResult(MODEL_USAGE);
283
+ return commandResult(t(MODEL_USAGE));
283
284
  }
284
285
  if (options.pendingInteraction) {
285
286
  return commandResult([
286
- '当前任务正在等待你的回答或审批。',
287
+ t('当前任务正在等待你的回答或审批。'),
287
288
  '',
288
- '请先处理当前请求,或者发送 /stop 停止任务。',
289
+ t('请先处理当前请求,或者发送 /stop 停止任务。'),
289
290
  ].join('\n'));
290
291
  }
291
292
 
@@ -293,7 +294,7 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
293
294
  return await withSessionBindingLock(state, key, async () => {
294
295
  const bound = await boundSession(harness, state, key, requestOptions);
295
296
  if (bound && await sessionIsBusy(bound.session, options.control, requestOptions)) {
296
- return commandResult('当前任务正在运行,请等待完成或先发送 /stop。');
297
+ return commandResult(t('当前任务正在运行,请等待完成或先发送 /stop。'));
297
298
  }
298
299
 
299
300
  const catalog = bound
@@ -305,9 +306,9 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
305
306
  if (!selection) {
306
307
  if (numberRequest) return commandResult(invalidModelNumberMessage(requested));
307
308
  return commandResult([
308
- `没有找到模型:${safeDisplayText(requested)}`,
309
+ t('没有找到模型:{model}', { model: safeDisplayText(requested) }),
309
310
  '',
310
- '请发送 /models 查看可用模型。',
311
+ t('请发送 /models 查看可用模型。'),
311
312
  ].join('\n'));
312
313
  }
313
314
 
@@ -344,7 +345,10 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
344
345
  throw stale;
345
346
  }
346
347
  }
347
- return commandResult(`模型已切换为:\n${modelId(selection.provider, selection.model)}\n\n后续消息将使用该模型。`);
348
+ return commandResult(t(`模型已切换为:
349
+ {model}
350
+
351
+ 后续消息将使用该模型。`, { model: modelId(selection.provider, selection.model) }));
348
352
  });
349
353
  } catch (error) {
350
354
  return commandResult(modelErrorMessage(error, 'select'));
@@ -2,6 +2,7 @@ import {
2
2
  normalizeAgentPresetCatalog,
3
3
  normalizeAgentPresetId,
4
4
  } from './agent-preset.mjs';
5
+ import { t } from './i18n.mjs';
5
6
  import { withSessionBindingLock } from './session-binding-lock.mjs';
6
7
  import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
7
8
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
@@ -59,7 +60,7 @@ function normalizeSettings(value) {
59
60
 
60
61
  function presetItemText(item) {
61
62
  const label = safeDisplayText(item.label) || item.id;
62
- return `${label}(${item.id})`;
63
+ return t('{label}{id})', { label, id: item.id });
63
64
  }
64
65
 
65
66
  function itemFor(catalog, id) {
@@ -67,11 +68,11 @@ function itemFor(catalog, id) {
67
68
  }
68
69
 
69
70
  function defaultDescription(catalog) {
70
- if (!catalog.defaultId) return '未设置或当前不可用';
71
+ if (!catalog.defaultId) return t('未设置或当前不可用');
71
72
  const item = itemFor(catalog, catalog.defaultId);
72
73
  return item
73
74
  ? presetItemText(item)
74
- : `${catalog.defaultId}(当前不可用)`;
75
+ : t('{id}(当前不可用)', { id: catalog.defaultId });
75
76
  }
76
77
 
77
78
  function currentDescription(settings) {
@@ -79,63 +80,63 @@ function currentDescription(settings) {
79
80
  if (agentPreset === null) {
80
81
  const item = itemFor(catalog, catalog.defaultId);
81
82
  return item
82
- ? `跟随 Host 默认:${presetItemText(item)}`
83
- : '跟随 Host 默认(Host 默认当前不可用)';
83
+ ? t('跟随 Host 默认:{preset}', { preset: presetItemText(item) })
84
+ : t('跟随 Host 默认(Host 默认当前不可用)');
84
85
  }
85
86
  const item = itemFor(catalog, agentPreset);
86
87
  return item
87
88
  ? presetItemText(item)
88
- : `${agentPreset}(已不可用)`;
89
+ : t('{id}(已不可用)', { id: agentPreset });
89
90
  }
90
91
 
91
92
  function formatCurrent(settings) {
92
93
  return [
93
- '当前机器人用于新会话的 Agent Preset:',
94
+ t('当前机器人用于新会话的 Agent Preset:'),
94
95
  currentDescription(settings),
95
96
  '',
96
- '已有会话不会受此设置影响。',
97
- '查看可用项:/presetlist',
98
- '恢复跟随 Host 默认:/preset --default',
97
+ t('已有会话不会受此设置影响。'),
98
+ t('查看可用项:/presetlist'),
99
+ t('恢复跟随 Host 默认:/preset --default'),
99
100
  ].join('\n');
100
101
  }
101
102
 
102
103
  function formatList(settings) {
103
104
  const { agentPreset, agentPresetCatalog: catalog } = settings;
104
105
  const lines = [
105
- '当前机器人用于新会话的 Agent Preset:',
106
+ t('当前机器人用于新会话的 Agent Preset:'),
106
107
  currentDescription(settings),
107
108
  '',
108
- `Host 默认:${defaultDescription(catalog)}`,
109
+ t('Host 默认:{preset}', { preset: defaultDescription(catalog) }),
109
110
  '',
110
- `可用 Agent Preset(${catalog.items.length}):`,
111
+ t('可用 Agent Preset{count}):', { count: catalog.items.length }),
111
112
  ];
112
113
  if (catalog.items.length === 0) {
113
- lines.push('当前没有可用 Agent Preset。');
114
+ lines.push(t('当前没有可用 Agent Preset。'));
114
115
  } else {
115
116
  catalog.items.forEach((item, index) => {
116
117
  const markers = [];
117
- if (item.id === catalog.defaultId) markers.push('Host 默认');
118
- if (item.id === agentPreset) markers.push('当前选择');
119
- if (agentPreset === null && item.id === catalog.defaultId) markers.push('当前生效');
118
+ if (item.id === catalog.defaultId) markers.push(t('Host 默认'));
119
+ if (item.id === agentPreset) markers.push(t('当前选择'));
120
+ if (agentPreset === null && item.id === catalog.defaultId) markers.push(t('当前生效'));
120
121
  const annotation = markers.length > 0 ? `(${markers.join(',')})` : '';
121
122
  lines.push(`${index + 1}. ${presetItemText(item)}${annotation}`);
122
123
  });
123
124
  }
124
125
  lines.push(
125
126
  '',
126
- '选择:/preset <序号或 ID>',
127
- '纯数字 ID:/preset id:<ID>',
128
- '恢复跟随 Host 默认:/preset --default',
127
+ t('选择:/preset <序号或 ID>'),
128
+ t('纯数字 ID:/preset id:<ID>'),
129
+ t('恢复跟随 Host 默认:/preset --default'),
129
130
  );
130
131
  return lines.join('\n');
131
132
  }
132
133
 
133
134
  function formatUpdated(settings) {
134
135
  return [
135
- '当前机器人用于新会话的 Agent Preset 已设置为:',
136
+ t('当前机器人用于新会话的 Agent Preset 已设置为:'),
136
137
  currentDescription(settings),
137
138
  '',
138
- '已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。',
139
+ t('已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。'),
139
140
  ].join('\n');
140
141
  }
141
142
 
@@ -189,16 +190,16 @@ function presetFromSnapshot(state, key, requested) {
189
190
  if (!/^\d+$/u.test(requested)) return { numeric: false, id: null };
190
191
  const index = Number(requested);
191
192
  if (!Number.isSafeInteger(index) || index < 1) {
192
- return { numeric: true, error: 'Agent Preset 序号无效,请先执行 /presetlist。' };
193
+ return { numeric: true, error: t('Agent Preset 序号无效,请先执行 /presetlist。') };
193
194
  }
194
195
  const snapshot = loadSnapshot(state, key);
195
196
  if (!snapshot) {
196
- return { numeric: true, error: '请先执行 /presetlist,再按列表序号选择 Agent Preset。' };
197
+ return { numeric: true, error: t('请先执行 /presetlist,再按列表序号选择 Agent Preset。') };
197
198
  }
198
199
  const id = snapshot[index - 1];
199
200
  return id
200
201
  ? { numeric: true, id }
201
- : { numeric: true, error: 'Agent Preset 序号不存在,请重新执行 /presetlist。' };
202
+ : { numeric: true, error: t('Agent Preset 序号不存在,请重新执行 /presetlist。') };
202
203
  }
203
204
 
204
205
  function errorCode(error) {
@@ -208,22 +209,23 @@ function errorCode(error) {
208
209
  function presetErrorMessage(error, action) {
209
210
  const code = errorCode(error);
210
211
  if (code === 'agent-preset-invalid') {
211
- return `Agent Preset ID 格式无效。\n${PRESET_USAGE}`;
212
+ return t(`Agent Preset ID 格式无效。
213
+ {usage}`, { usage: t(PRESET_USAGE) });
212
214
  }
213
215
  if (code === 'agent-preset-unavailable') {
214
- return 'Agent Preset 不存在或当前不可用,请重新执行 /presetlist。';
216
+ return t('Agent Preset 不存在或当前不可用,请重新执行 /presetlist。');
215
217
  }
216
218
  if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
217
- return '工作区或机器人状态已发生变化,请重试。';
219
+ return t('工作区或机器人状态已发生变化,请重试。');
218
220
  }
219
221
  if (code === 'cancelled' || error?.name === 'AbortError') {
220
- if (action === 'list') return '获取 Agent Preset 列表已取消。';
221
- if (action === 'current') return '获取 Agent Preset 设置已取消。';
222
- return 'Agent Preset 修改已取消。';
222
+ if (action === 'list') return t('获取 Agent Preset 列表已取消。');
223
+ if (action === 'current') return t('获取 Agent Preset 设置已取消。');
224
+ return t('Agent Preset 修改已取消。');
223
225
  }
224
- if (action === 'list') return '暂时无法获取 Agent Preset 列表,请稍后重试。';
225
- if (action === 'current') return '暂时无法获取 Agent Preset 设置,请稍后重试。';
226
- return 'Agent Preset 修改失败,请稍后重试。';
226
+ if (action === 'list') return t('暂时无法获取 Agent Preset 列表,请稍后重试。');
227
+ if (action === 'current') return t('暂时无法获取 Agent Preset 设置,请稍后重试。');
228
+ return t('Agent Preset 修改失败,请稍后重试。');
227
229
  }
228
230
 
229
231
  async function settings(harness, options) {
@@ -250,12 +252,12 @@ export async function runPresetCommand(text, harness, state, key, options = {})
250
252
  if (!isPresetCommand(text)) return null;
251
253
  const command = text.trim();
252
254
  if (options.hasImages) {
253
- return commandResult('Agent Preset 命令仅支持纯文字,请移除图片后重试。');
255
+ return commandResult(t('Agent Preset 命令仅支持纯文字,请移除图片后重试。'));
254
256
  }
255
257
  const requestOptions = rpcOptions(options.signal);
256
258
 
257
259
  if (PRESET_LIST_COMMAND.test(command)) {
258
- if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(PRESET_LIST_USAGE);
260
+ if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
259
261
  try {
260
262
  const current = await settings(harness, requestOptions);
261
263
  saveSnapshot(state, key, current.agentPresetCatalog.items);
@@ -266,7 +268,7 @@ export async function runPresetCommand(text, harness, state, key, options = {})
266
268
  }
267
269
 
268
270
  const match = /^\/preset(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
269
- if (!match) return commandResult(PRESET_USAGE);
271
+ if (!match) return commandResult(t(PRESET_USAGE));
270
272
  const requested = match[1];
271
273
  if (!requested) {
272
274
  try {
@@ -290,7 +292,8 @@ export async function runPresetCommand(text, harness, state, key, options = {})
290
292
  selected = fromSnapshot.id;
291
293
  } else {
292
294
  selected = normalizeAgentPresetId(requested);
293
- if (!selected) return commandResult(`Agent Preset ID 格式无效。\n${PRESET_USAGE}`);
295
+ if (!selected) return commandResult(t(`Agent Preset ID 格式无效。
296
+ {usage}`, { usage: t(PRESET_USAGE) }));
294
297
  }
295
298
  }
296
299
  }
@@ -76,7 +76,7 @@ export async function deliverOutboundArtifacts({
76
76
  logger,
77
77
  }) {
78
78
  const receipts = baseReceipt ? [baseReceipt] : [];
79
- let userVisible = Boolean(baseReceipt);
79
+ let userVisible = Boolean(baseReceipt) && baseReceipt.deliveryOutcome !== 'failed';
80
80
  let failureNoticeVisible = false;
81
81
  let artifactsSent = 0;
82
82
  let artifactSendErrors = 0;