@xmanrui/dsh-im 1.4.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 (82) hide show
  1. package/README.en.md +18 -6
  2. package/README.md +18 -6
  3. package/lib/index.js +206 -176
  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 +105 -82
  8. package/src/channels/dingtalk/dingtalk-bridge.mjs +79 -110
  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 +169 -180
  17. package/src/channels/feishu/feishu-cards.mjs +62 -54
  18. package/src/channels/feishu/feishu-channel.mjs +57 -21
  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 +149 -130
  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 +170 -0
  56. package/src/channels/shared/semantic/artifact.mjs +2 -2
  57. package/src/channels/shared/semantic/delivery.mjs +40 -0
  58. package/src/channels/shared/text-harness-bridge.mjs +154 -136
  59. package/src/channels/shared/token-bot-controller.mjs +32 -11
  60. package/src/channels/shared/workspace-command.mjs +69 -58
  61. package/src/channels/slack/config-store.mjs +3 -1
  62. package/src/channels/slack/slack-api.mjs +6 -5
  63. package/src/channels/slack/slack-controller.mjs +10 -9
  64. package/src/channels/slack/slack-runtime.mjs +16 -4
  65. package/src/channels/telegram/config-store.mjs +2 -1
  66. package/src/channels/telegram/telegram-api.mjs +111 -19
  67. package/src/channels/telegram/telegram-rich-message.mjs +147 -0
  68. package/src/channels/telegram/telegram-runtime.mjs +383 -5
  69. package/src/channels/wecom/config-store.mjs +3 -1
  70. package/src/channels/wecom/wecom-bridge.mjs +154 -153
  71. package/src/channels/wecom/wecom-controller.mjs +20 -19
  72. package/src/channels/wecom/wecom-runtime.mjs +2 -1
  73. package/src/channels/weixin/config-store.mjs +2 -1
  74. package/src/channels/weixin/weixin-api.mjs +140 -107
  75. package/src/channels/weixin/weixin-bridge.mjs +86 -123
  76. package/src/channels/weixin/weixin-controller.mjs +22 -21
  77. package/src/channels/weixin/weixin-runtime.mjs +3 -2
  78. package/src/channels/whatsapp/config-store.mjs +3 -1
  79. package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
  80. package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
  81. package/src/channels/whatsapp/whatsapp-runtime.mjs +47 -13
  82. package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
@@ -1,6 +1,7 @@
1
1
  import { realpath, stat } from 'node:fs/promises';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
3
 
4
+ import { t } from './i18n.mjs';
4
5
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
5
6
 
6
7
  const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
@@ -60,28 +61,34 @@ async function existingWorkspacePaths(values) {
60
61
 
61
62
  async function selectedWorkspacePath(value) {
62
63
  if (typeof value !== 'string' || !isAbsolute(value.trim())) {
63
- return { error: `工作区必须是绝对路径。\n${SESSION_LIST_USAGE}` };
64
+ return { error: t(`工作区必须是绝对路径。
65
+ {usage}`, { usage: t(SESSION_LIST_USAGE) }) };
64
66
  }
65
67
  const workspace = normalizedWorkspacePath(value.trim());
66
68
  if (!workspace) {
67
- return { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
69
+ return { error: t(`工作区路径包含不支持的字符或长度超过限制。
70
+ {usage}`, { usage: t(SESSION_LIST_USAGE) }) };
68
71
  }
69
72
  let info;
70
73
  try {
71
74
  info = await stat(workspace);
72
75
  } catch {
73
- return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
76
+ return { error: t(`工作区路径不存在。
77
+ {usage}`, { usage: t(SESSION_LIST_USAGE) }) };
74
78
  }
75
79
  if (!info.isDirectory()) {
76
- return { error: `工作区路径必须指向一个目录。\n${SESSION_LIST_USAGE}` };
80
+ return { error: t(`工作区路径必须指向一个目录。
81
+ {usage}`, { usage: t(SESSION_LIST_USAGE) }) };
77
82
  }
78
83
  try {
79
84
  const canonical = normalizedWorkspacePath(await realpath(workspace));
80
85
  return canonical
81
86
  ? { workspace: canonical }
82
- : { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
87
+ : { error: t(`工作区路径包含不支持的字符或长度超过限制。
88
+ {usage}`, { usage: t(SESSION_LIST_USAGE) }) };
83
89
  } catch {
84
- return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
90
+ return { error: t(`工作区路径不存在。
91
+ {usage}`, { usage: t(SESSION_LIST_USAGE) }) };
85
92
  }
86
93
  }
87
94
 
@@ -120,38 +127,38 @@ export function splitWorkspaceCommandMessage(message) {
120
127
  }
121
128
 
122
129
  async function runWorkspaceListCommand(match, harness) {
123
- if (match[1]?.trim()) return commandResult('用法:/workspacelist');
130
+ if (match[1]?.trim()) return commandResult(t('用法:/workspacelist'));
124
131
  if (typeof harness?.listWorkspaces !== 'function') {
125
- return commandResult('当前机器人暂不支持列出工作区。');
132
+ return commandResult(t('当前机器人暂不支持列出工作区。'));
126
133
  }
127
134
  try {
128
135
  const { current, paths } = await workspacePathSnapshot(harness);
129
136
  if (paths.length === 0) {
130
- return commandResult('当前 Harness Host 上没有仍然存在的已登记工作区。');
137
+ return commandResult(t('当前 Harness Host 上没有仍然存在的已登记工作区。'));
131
138
  }
132
139
  const lines = [
133
- `当前 Harness Host 上存在的工作区(${paths.length}):`,
140
+ t('当前 Harness Host 上存在的工作区({count}):', { count: paths.length }),
134
141
  ...paths.map((workspace, index) => (
135
- `${index + 1}. ${workspace}${workspace === current ? '(当前)' : ''}`
142
+ `${index + 1}. ${workspace}${workspace === current ? t('(当前)') : ''}`
136
143
  )),
137
144
  '',
138
- '切换用法:/workspace 工作区绝对路径',
139
- '查看会话:/sessionlist 工作区序号或绝对路径',
145
+ t('切换用法:/workspace 工作区绝对路径'),
146
+ t('查看会话:/sessionlist 工作区序号或绝对路径'),
140
147
  ];
141
148
  const message = lines.join('\n');
142
149
  return commandResult(message, splitWorkspaceCommandMessage(message));
143
150
  } catch (error) {
144
151
  if (error?.code === 'workspace-bot-not-found') {
145
- return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区。');
152
+ return commandResult(t('机器人正在移除或已重新接入,无法列出原会话的工作区。'));
146
153
  }
147
- return commandResult('暂时无法获取工作区列表,请稍后重试。');
154
+ return commandResult(t('暂时无法获取工作区列表,请稍后重试。'));
148
155
  }
149
156
  }
150
157
 
151
158
  export async function resolveSessionListWorkspace(selector, harness) {
152
159
  if (!selector) {
153
160
  if (typeof harness?.currentWorkspace !== 'function') {
154
- return { error: '当前机器人没有可用的工作区。' };
161
+ return { error: t('当前机器人没有可用的工作区。') };
155
162
  }
156
163
  const selected = await selectedWorkspacePath(harness.currentWorkspace());
157
164
  harness.assertWorkspaceScope?.();
@@ -160,12 +167,12 @@ export async function resolveSessionListWorkspace(selector, harness) {
160
167
 
161
168
  if (/^\d+$/u.test(selector)) {
162
169
  if (typeof harness?.listWorkspaces !== 'function') {
163
- return { error: '当前机器人暂不支持按序号选择工作区。' };
170
+ return { error: t('当前机器人暂不支持按序号选择工作区。') };
164
171
  }
165
172
  const { paths } = await workspacePathSnapshot(harness);
166
173
  const position = Number(selector);
167
174
  if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
168
- return { error: '工作区序号不存在,请先执行 /workspacelist。' };
175
+ return { error: t('工作区序号不存在,请先执行 /workspacelist。') };
169
176
  }
170
177
  return { workspace: paths[position - 1] };
171
178
  }
@@ -185,13 +192,13 @@ function formatSessionRelativeTime(value) {
185
192
  const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
186
193
  const now = new Date();
187
194
  const dayDiff = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);
188
- if (dayDiff === 0) return `今天 ${hm}`;
189
- if (dayDiff === 1) return `昨天 ${hm}`;
190
- if (dayDiff === 2) return `前天 ${hm}`;
195
+ if (dayDiff === 0) return t('今天 {time}', { time: hm });
196
+ if (dayDiff === 1) return t('昨天 {time}', { time: hm });
197
+ if (dayDiff === 2) return t('前天 {time}', { time: hm });
191
198
  if (date.getFullYear() === now.getFullYear()) {
192
- return `${date.getMonth() + 1}月${date.getDate()}日 ${hm}`;
199
+ return t('{month}月{day}日 {time}', { month: date.getMonth() + 1, day: date.getDate(), time: hm });
193
200
  }
194
- return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
201
+ return t('{year}年{month}月{day}日', { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() });
195
202
  }
196
203
 
197
204
  function sessionListMessage(workspace, sessions, { currentWorkspace = false } = {}) {
@@ -199,22 +206,24 @@ function sessionListMessage(workspace, sessions, { currentWorkspace = false } =
199
206
  const sessionId = safeDisplayText(session?.sessionId);
200
207
  if (!sessionId) throw new TypeError('Harness returned an invalid session id');
201
208
  const title = session?.summaryAvailable === false
202
- ? '标题暂不可用'
203
- : safeDisplayText(session?.title) || '暂无标题';
209
+ ? t('标题暂不可用')
210
+ : safeDisplayText(session?.title) || t('暂无标题');
204
211
  const timeText = formatSessionRelativeTime(session?.time);
205
- const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? '(已归档)' : ''}`;
212
+ const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? t('(已归档)') : ''}`;
206
213
  return `${title}${annotation}\n ID: ${sessionId}`;
207
214
  });
208
- if (rows.length === 0) return `工作区:${workspace}\n该工作区暂无会话。`;
215
+ if (rows.length === 0) return t(`工作区:{workspace}
216
+ 该工作区暂无会话。`, { workspace });
209
217
  return [
210
- `工作区:${workspace}`,
211
- `会话(${rows.length}):`,
218
+ t('工作区:{workspace}', { workspace }),
219
+ t('会话({count}):', { count: rows.length }),
212
220
  '',
213
221
  ...rows.map((row, index) => `${index + 1}. ${row}`),
214
222
  '',
215
223
  currentWorkspace
216
- ? '绑定用法:/session Session ID 或当前工作区序号(/session N)'
217
- : '绑定用法:/session Session ID\n提示:/session N 只按机器人当前工作区的序号绑定。',
224
+ ? t('绑定用法:/session Session ID 或当前工作区序号(/session N)')
225
+ : t(`绑定用法:/session Session ID
226
+ 提示:/session N 只按机器人当前工作区的序号绑定。`),
218
227
  ].join('\n');
219
228
  }
220
229
 
@@ -227,7 +236,7 @@ async function currentSessionListWorkspace(harness) {
227
236
 
228
237
  async function runSessionListCommand(match, harness) {
229
238
  if (typeof harness?.listWorkspaceSessions !== 'function') {
230
- return commandResult('当前机器人暂不支持列出工作区会话。');
239
+ return commandResult(t('当前机器人暂不支持列出工作区会话。'));
231
240
  }
232
241
  const selector = match[1]?.trim() ?? '';
233
242
  try {
@@ -246,36 +255,37 @@ async function runSessionListCommand(match, harness) {
246
255
  return commandResult(message, splitWorkspaceCommandMessage(message));
247
256
  } catch (error) {
248
257
  if (error?.code === 'workspace-bot-not-found') {
249
- return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区会话。');
258
+ return commandResult(t('机器人正在移除或已重新接入,无法列出原会话的工作区会话。'));
250
259
  }
251
- return commandResult('暂时无法获取工作区会话列表,请稍后重试。');
260
+ return commandResult(t('暂时无法获取工作区会话列表,请稍后重试。'));
252
261
  }
253
262
  }
254
263
 
255
264
  function sessionBindErrorMessage(error) {
256
265
  if (error?.code === 'session-id-invalid') {
257
- return `Session ID 格式无效。\n${SESSION_BIND_USAGE}`;
266
+ return t(`Session ID 格式无效。
267
+ {usage}`, { usage: t(SESSION_BIND_USAGE) });
258
268
  }
259
269
  if (['session-not-registered', 'session-not-found'].includes(error?.code)) {
260
- return '未找到该会话,请先执行 /sessionlist 确认 Session ID。';
270
+ return t('未找到该会话,请先执行 /sessionlist 确认 Session ID。');
261
271
  }
262
272
  if (error?.code === 'session-subagent-unsupported') {
263
- return '子代理会话不能绑定到机器人对话,请选择普通会话。';
273
+ return t('子代理会话不能绑定到机器人对话,请选择普通会话。');
264
274
  }
265
275
  if (error?.code === 'session-workspace-ambiguous') {
266
- return '该会话的工作区归属不明确,暂时无法绑定。';
276
+ return t('该会话的工作区归属不明确,暂时无法绑定。');
267
277
  }
268
278
  if (error?.code === 'session-summary-unavailable') {
269
- return '暂时无法读取该会话的信息,请稍后重试。';
279
+ return t('暂时无法读取该会话的信息,请稍后重试。');
270
280
  }
271
281
  if (error?.code === 'workspace-bot-not-found') {
272
- return '机器人正在移除或已重新接入,无法绑定原对话的会话。';
282
+ return t('机器人正在移除或已重新接入,无法绑定原对话的会话。');
273
283
  }
274
284
  if ([WORKSPACE_SESSION_STALE, 'agent-busy', 'session-conflict', 'workspace-conflict']
275
285
  .includes(error?.code)) {
276
- return '工作区或会话状态已发生变化,请重试。';
286
+ return t('工作区或会话状态已发生变化,请重试。');
277
287
  }
278
- return '暂时无法绑定会话,请稍后重试。';
288
+ return t('暂时无法绑定会话,请稍后重试。');
279
289
  }
280
290
 
281
291
  async function runSessionBindCommand(command, harness, conversationKey) {
@@ -285,7 +295,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
285
295
  // 序号模式:把 /session N 解析成当前工作区会话列表中的第 N 个会话
286
296
  if (typeof harness?.listWorkspaceSessions !== 'function'
287
297
  || typeof harness?.currentWorkspace !== 'function') {
288
- return commandResult('当前机器人暂不支持按序号绑定,请使用 /session Session ID。');
298
+ return commandResult(t('当前机器人暂不支持按序号绑定,请使用 /session Session ID。'));
289
299
  }
290
300
  try {
291
301
  const selected = await selectedWorkspacePath(harness.currentWorkspace());
@@ -298,7 +308,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
298
308
  const position = Number(sessionId);
299
309
  if (!Number.isSafeInteger(position) || position < 1
300
310
  || position > listed.sessions.length) {
301
- return commandResult('会话序号不存在,请先执行 /sessionlist 查看序号。');
311
+ return commandResult(t('会话序号不存在,请先执行 /sessionlist 查看序号。'));
302
312
  }
303
313
  const selectedSessionId = listed.sessions[position - 1]?.sessionId;
304
314
  if (!validSessionId(selectedSessionId)) {
@@ -309,15 +319,15 @@ async function runSessionBindCommand(command, harness, conversationKey) {
309
319
  if (error?.code === 'workspace-bot-not-found') {
310
320
  return commandResult(sessionBindErrorMessage(error));
311
321
  }
312
- return commandResult('暂时无法获取会话列表,请稍后重试。');
322
+ return commandResult(t('暂时无法获取会话列表,请稍后重试。'));
313
323
  }
314
324
  }
315
- if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
325
+ if (!validSessionId(sessionId)) return commandResult(t(SESSION_BIND_USAGE));
316
326
  if (typeof harness?.bindWorkspaceSession !== 'function') {
317
- return commandResult('当前机器人暂不支持绑定已有会话。');
327
+ return commandResult(t('当前机器人暂不支持绑定已有会话。'));
318
328
  }
319
329
  if (typeof conversationKey !== 'string' || !conversationKey) {
320
- return commandResult('当前消息缺少可绑定的会话上下文。');
330
+ return commandResult(t('当前消息缺少可绑定的会话上下文。'));
321
331
  }
322
332
  try {
323
333
  const bound = await harness.bindWorkspaceSession(conversationKey, sessionId);
@@ -327,13 +337,13 @@ async function runSessionBindCommand(command, harness, conversationKey) {
327
337
  if (!workspace || !boundSessionId) {
328
338
  throw new TypeError('Harness returned an invalid bound session');
329
339
  }
330
- const title = safeDisplayText(bound?.title) || '暂无标题';
340
+ const title = safeDisplayText(bound?.title) || t('暂无标题');
331
341
  const message = [
332
- '当前聊天已绑定会话:',
333
- `工作区:${workspace}`,
334
- `标题:${title}`,
342
+ t('当前聊天已绑定会话:'),
343
+ t('工作区:{workspace}', { workspace }),
344
+ t('标题:{title}', { title }),
335
345
  `ID:${boundSessionId}`,
336
- `归档:${bound?.archived === true ? '是' : '否'}`,
346
+ t('归档:{archived}', { archived: bound?.archived === true ? t('是') : t('否') }),
337
347
  ].join('\n');
338
348
  return commandResult(message, splitWorkspaceCommandMessage(message));
339
349
  } catch (error) {
@@ -355,20 +365,21 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
355
365
  if (!match) return null;
356
366
  const workspace = match[1]?.trim();
357
367
  if (!workspace) {
358
- return commandResult('用法:/workspace 工作区绝对路径');
368
+ return commandResult(t('用法:/workspace 工作区绝对路径'));
359
369
  }
360
370
  if (typeof harness?.switchWorkspace !== 'function') {
361
- return commandResult('当前机器人暂不支持切换工作区。');
371
+ return commandResult(t('当前机器人暂不支持切换工作区。'));
362
372
  }
363
373
  try {
364
374
  const current = await harness.switchWorkspace(workspace);
365
- return commandResult(`工作区已切换为:${current}`);
375
+ return commandResult(t('工作区已切换为:{workspace}', { workspace: current }));
366
376
  } catch (error) {
367
377
  if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
368
- return commandResult(`${error.message}\n用法:/workspace 工作区绝对路径`);
378
+ return commandResult(t(`{message}
379
+ 用法:/workspace 工作区绝对路径`, { message: error.message }));
369
380
  }
370
381
  if (error?.code === 'workspace-bot-not-found') {
371
- return commandResult('机器人正在移除或已重新接入,无法切换原会话的工作区。');
382
+ return commandResult(t('机器人正在移除或已重新接入,无法切换原会话的工作区。'));
372
383
  }
373
384
  throw error;
374
385
  }
@@ -2,6 +2,8 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
4
 
5
+ import { t } from '../shared/i18n.mjs';
6
+
5
7
  const EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) });
6
8
  const BOT_ID_PATTERN = /^slack_[a-f0-9]{24}$/;
7
9
  const BOT_TOKEN_REF_PATTERN = /^DSH_SLACK_BOT_TOKEN_[A-F0-9]{24}$/;
@@ -27,7 +29,7 @@ export function maskSlackBotId(platformId) {
27
29
  const value = cleanString(platformId) ?? '';
28
30
  const [teamId, userId] = value.split(':');
29
31
  if (teamId && userId) return `${teamId.slice(0, 5)}••• · ${userId.slice(0, 5)}•••`;
30
- return value ? `${value.slice(0, 6)}••••` : 'Slack机器人';
32
+ return value ? `${value.slice(0, 6)}••••` : t('Slack机器人');
31
33
  }
32
34
 
33
35
  export class SlackConfigStore {
@@ -1,5 +1,6 @@
1
1
  import { fetchFileStream } from '../shared/file-download.mjs';
2
2
  import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
3
+ import { t } from '../shared/i18n.mjs';
3
4
 
4
5
  const DEFAULT_BASE_URL = 'https://slack.com/api/';
5
6
  const SLACK_FILE_HOST = 'files.slack.com';
@@ -430,7 +431,7 @@ export class SlackApi {
430
431
  throw new ImagePromptError(
431
432
  'slack-file-access-required',
432
433
  'Slack redirected a private file request to the workspace because file access was not granted',
433
- 'Slack 未授权机器人读取该文件。请为应用添加 files:read 后重新安装,再重新发送图片。',
434
+ t('Slack 未授权机器人读取该文件。请为应用添加 files:read 后重新安装,再重新发送图片。'),
434
435
  );
435
436
  }
436
437
  return response;
@@ -505,19 +506,19 @@ export class SlackApi {
505
506
 
506
507
  export async function inspectSlackCredentials({ botToken, appToken }, options = {}) {
507
508
  if (!validSlackBotToken(botToken)) {
508
- const error = new TypeError('Slack Bot Token 必须以 xoxb- 开头。');
509
+ const error = new TypeError(t('Slack Bot Token 必须以 xoxb- 开头。'));
509
510
  error.code = 'slack-invalid-bot-token';
510
511
  throw error;
511
512
  }
512
513
  if (!validSlackAppToken(appToken)) {
513
- const error = new TypeError('Slack App Token 必须以 xapp- 开头。');
514
+ const error = new TypeError(t('Slack App Token 必须以 xapp- 开头。'));
514
515
  error.code = 'slack-invalid-app-token';
515
516
  throw error;
516
517
  }
517
518
  const api = new SlackApi({ botToken, appToken, ...options });
518
519
  const [identity, connection] = await Promise.all([api.authTest(), api.openConnection()]);
519
520
  if (!identity?.team_id || !identity?.user_id || !identity?.bot_id) {
520
- throw new Error('Slack Bot Token 没有返回完整的机器人身份。');
521
+ throw new Error(t('Slack Bot Token 没有返回完整的机器人身份。'));
521
522
  }
522
523
  let socketUrl;
523
524
  try {
@@ -526,7 +527,7 @@ export async function inspectSlackCredentials({ botToken, appToken }, options =
526
527
  socketUrl = null;
527
528
  }
528
529
  if (!socketUrl || socketUrl.protocol !== 'wss:') {
529
- const error = new Error('Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。');
530
+ const error = new Error(t('Slack App Token 无法创建 Socket Mode 连接,请确认已启用 Socket Mode 和 connections:write。'));
530
531
  error.code = 'slack-socket-mode';
531
532
  throw error;
532
533
  }
@@ -1,4 +1,5 @@
1
1
  import { connectionTestMessage } from '../shared/connection-test.mjs';
2
+ import { t } from '../shared/i18n.mjs';
2
3
  import { deriveSlackBotIdentity, maskSlackBotId } from './config-store.mjs';
3
4
  import { inspectSlackCredentials } from './slack-api.mjs';
4
5
  import { SLACK_DESCRIPTOR } from './slack-bridge.mjs';
@@ -60,7 +61,7 @@ export class SlackController {
60
61
  if (!resolved) {
61
62
  this.#errors.set(config.botId, safeError(
62
63
  'missing-token',
63
- 'Slack机器人凭据缺失,请移除后重新接入。',
64
+ t('Slack机器人凭据缺失,请移除后重新接入。'),
64
65
  ));
65
66
  return;
66
67
  }
@@ -70,7 +71,7 @@ export class SlackController {
70
71
  } catch (error) {
71
72
  this.#errors.set(config.botId, safeError(
72
73
  'connection-failed',
73
- 'Slack Socket Mode 连接未就绪,插件会自动重试。',
74
+ t('Slack Socket Mode 连接未就绪,插件会自动重试。'),
74
75
  ));
75
76
  this.#logger.warn?.(
76
77
  `[dsh-im:slack] bot ${config.botId} failed to initialize:`,
@@ -135,7 +136,7 @@ export class SlackController {
135
136
  } catch (error) {
136
137
  this.#errors.set(identity.botId, safeError(
137
138
  'connection-failed',
138
- 'Slack机器人已接入,Socket Mode 连接暂未就绪。',
139
+ t('Slack机器人已接入,Socket Mode 连接暂未就绪。'),
139
140
  ));
140
141
  this.#logger.warn?.(
141
142
  `[dsh-im:slack] bot ${identity.botId} credential connection failed:`,
@@ -159,7 +160,7 @@ export class SlackController {
159
160
  } catch (error) {
160
161
  this.#errors.set(botId, safeError(
161
162
  'connection-failed',
162
- 'Slack Socket Mode 连接仍未就绪,请检查两个 Token。',
163
+ t('Slack Socket Mode 连接仍未就绪,请检查两个 Token。'),
163
164
  ));
164
165
  throw error;
165
166
  } finally {
@@ -175,13 +176,13 @@ export class SlackController {
175
176
  return this.#withBotTransition(botId, async () => {
176
177
  const runtime = this.#runtimes.get(botId);
177
178
  if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
178
- const error = new Error('Slack机器人尚未连接');
179
+ const error = new Error(t('Slack机器人尚未连接'));
179
180
  error.code = 'test-target-unavailable';
180
181
  throw error;
181
182
  }
182
183
  await runtime.sendConnectionTest(connectionTestMessage(
183
184
  `${config.name}(${maskSlackBotId(config.platformId)})`,
184
- 'Slack机器人',
185
+ t('Slack机器人'),
185
186
  ));
186
187
  return { sent: true };
187
188
  });
@@ -243,9 +244,9 @@ export class SlackController {
243
244
  },
244
245
  health: {
245
246
  status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
246
- summary: connected ? `Slack${SLACK_DESCRIPTOR.connectionLabel}运行正常`
247
- : state === 'error' ? 'Slack连接未就绪,插件会自动重试'
248
- : 'Slack连接当前离线',
247
+ summary: connected ? t(`Slack${SLACK_DESCRIPTOR.connectionLabel}运行正常`)
248
+ : state === 'error' ? t('Slack连接未就绪,插件会自动重试')
249
+ : t('Slack连接当前离线'),
249
250
  lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
250
251
  lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
251
252
  },
@@ -1,4 +1,5 @@
1
1
  import { splitMessageText } from '../shared/editable-message-stream.mjs';
2
+ import { t } from '../shared/i18n.mjs';
2
3
  import { SlackApi } from './slack-api.mjs';
3
4
  import { createSlackBridgeStatus, SlackHarnessBridge } from './slack-bridge.mjs';
4
5
 
@@ -135,8 +136,18 @@ export function normalizeSlackEvent(payload, botUserId, {
135
136
  };
136
137
  }
137
138
 
138
- function isToolProgress(text) {
139
- return typeof text === 'string' && /^正在使用.+…$/.test(text.trim());
139
+ export function isSlackToolProgress(text) {
140
+ if (typeof text !== 'string') return false;
141
+ const marker = '__DSH_IM_TOOL_NAME__';
142
+ const template = t('正在使用{name}…', { name: marker });
143
+ const markerIndex = template.indexOf(marker);
144
+ if (markerIndex < 0) return false;
145
+ const prefix = template.slice(0, markerIndex);
146
+ const suffix = template.slice(markerIndex + marker.length);
147
+ const source = text.trim();
148
+ return source.startsWith(prefix)
149
+ && source.endsWith(suffix)
150
+ && source.length > prefix.length + suffix.length;
140
151
  }
141
152
 
142
153
  async function appendInChunks(api, target, ts, text, signal) {
@@ -204,7 +215,8 @@ async function createSlackMessageStream({ api, target, signal, logger }) {
204
215
  return [...providerMessageIds];
205
216
  },
206
217
  update(text) {
207
- if (closed || broken || typeof text !== 'string' || !text.trim() || isToolProgress(text)) return;
218
+ if (closed || broken || typeof text !== 'string' || !text.trim()
219
+ || isSlackToolProgress(text)) return;
208
220
  pending = text;
209
221
  schedule();
210
222
  },
@@ -217,7 +229,7 @@ async function createSlackMessageStream({ api, target, signal, logger }) {
217
229
  await inFlight?.catch(() => undefined);
218
230
 
219
231
  const chunks = splitMessageText(text, SLACK_MESSAGE_LIMIT);
220
- const first = chunks[0] ?? '处理完成。';
232
+ const first = chunks[0] ?? t('处理完成。');
221
233
  if (!broken && first.startsWith(appended)) {
222
234
  await appendInChunks(api, target, ts, first.slice(appended.length), signal);
223
235
  await api.stopStream({ channelId: target.channelId, ts, signal });
@@ -1,3 +1,4 @@
1
+ import { t } from '../shared/i18n.mjs';
1
2
  import {
2
3
  deriveTokenBotIdentity,
3
4
  maskPlatformId,
@@ -66,7 +67,7 @@ export function deriveTelegramBotIdentity(platformId) {
66
67
  }
67
68
 
68
69
  export function maskTelegramBotId(platformId) {
69
- return maskPlatformId(platformId, 'Telegram机器人');
70
+ return maskPlatformId(platformId, t('Telegram机器人'));
70
71
  }
71
72
 
72
73
  export class TelegramConfigStore extends TokenBotConfigStore {