@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
@@ -10,6 +10,7 @@ import {
10
10
  connectionTestMessage,
11
11
  connectionTestTargetUnavailable,
12
12
  } from '../shared/connection-test.mjs';
13
+ import { t } from '../shared/i18n.mjs';
13
14
 
14
15
  const ACTIVE_ATTEMPT_STATES = new Set([
15
16
  'starting',
@@ -98,8 +99,8 @@ function publicProvisioningError(error) {
98
99
  if (error instanceof WeixinApiError) return safeAccountError(error.code, error.message);
99
100
  const message = ACTIVATION_ERROR_MESSAGES[error?.code];
100
101
  return message
101
- ? safeAccountError(error.code, message)
102
- : safeAccountError('activation-unknown-failed', '微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。');
102
+ ? safeAccountError(error.code, t(message))
103
+ : safeAccountError('activation-unknown-failed', t('微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。'));
103
104
  }
104
105
 
105
106
  function preserveActivationError(error, fallbackCode) {
@@ -167,7 +168,7 @@ export class WeixinController {
167
168
  if (!token) {
168
169
  this.#errors.set(
169
170
  latest.botId,
170
- safeAccountError('missing-token', '登录凭据缺失,请移除账号后重新扫码。'),
171
+ safeAccountError('missing-token', t('登录凭据缺失,请移除账号后重新扫码。')),
171
172
  );
172
173
  return;
173
174
  }
@@ -176,7 +177,7 @@ export class WeixinController {
176
177
  } catch (error) {
177
178
  this.#errors.set(
178
179
  latest.botId,
179
- safeAccountError('connection-failed', '微信连接未就绪,插件会自动重试。'),
180
+ safeAccountError('connection-failed', t('微信连接未就绪,插件会自动重试。')),
180
181
  );
181
182
  this.#logger.warn?.(`[dsh-weixin] account ${latest.botId} failed to initialize:`, error);
182
183
  } finally {
@@ -227,12 +228,12 @@ export class WeixinController {
227
228
  } catch (error) {
228
229
  if (record.controller.signal.aborted) {
229
230
  record.state = 'cancelled';
230
- record.error = safeAccountError('cancelled', '扫码绑定已取消。');
231
+ record.error = safeAccountError('cancelled', t('扫码绑定已取消。'));
231
232
  } else {
232
233
  record.state = 'failed';
233
234
  record.error = safeAccountError(
234
235
  error instanceof WeixinApiError ? error.code : 'qr-start-failed',
235
- error instanceof WeixinApiError ? error.message : '无法生成微信二维码,请稍后重试。',
236
+ error instanceof WeixinApiError ? error.message : t('无法生成微信二维码,请稍后重试。'),
236
237
  );
237
238
  }
238
239
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
@@ -271,7 +272,7 @@ export class WeixinController {
271
272
  record.verifyResolve = null;
272
273
  await record.task?.catch(() => undefined);
273
274
  if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
274
- record.error ??= safeAccountError('cancelled', '扫码绑定已取消。');
275
+ record.error ??= safeAccountError('cancelled', t('扫码绑定已取消。'));
275
276
  }
276
277
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
277
278
  this.#touch();
@@ -288,7 +289,7 @@ export class WeixinController {
288
289
  await this.#startRuntime(config, token);
289
290
  this.#errors.delete(botId);
290
291
  } catch (error) {
291
- this.#errors.set(botId, safeAccountError('connection-failed', '微信连接仍未就绪,请稍后重试。'));
292
+ this.#errors.set(botId, safeAccountError('connection-failed', t('微信连接仍未就绪,请稍后重试。')));
292
293
  throw error;
293
294
  } finally {
294
295
  this.#touch();
@@ -303,10 +304,10 @@ export class WeixinController {
303
304
  return this.#withBotTransition(botId, async () => {
304
305
  const runtime = this.#runtimes.get(botId);
305
306
  if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
306
- throw connectionTestTargetUnavailable('微信机器人');
307
+ throw connectionTestTargetUnavailable(t('微信机器人'));
307
308
  }
308
309
  return runtime.sendConnectionTest(connectionTestMessage(
309
- `微信机器人(${maskWeixinAccountId(config.accountId)})`,
310
+ t('微信机器人({name})', { name: maskWeixinAccountId(config.accountId) }),
310
311
  ));
311
312
  });
312
313
  }
@@ -352,7 +353,7 @@ export class WeixinController {
352
353
  ? 'error'
353
354
  : 'offline';
354
355
  const error = this.#errors.get(config.botId) ?? (state === 'error'
355
- ? safeAccountError('connection-failed', '微信连接未就绪,插件会自动重试。')
356
+ ? safeAccountError('connection-failed', t('微信连接未就绪,插件会自动重试。'))
356
357
  : null);
357
358
  return {
358
359
  botId: config.botId,
@@ -360,16 +361,16 @@ export class WeixinController {
360
361
  connected,
361
362
  configured: true,
362
363
  bot: {
363
- name: '微信机器人',
364
+ name: t('微信机器人'),
364
365
  accountIdMasked: maskWeixinAccountId(config.accountId),
365
366
  },
366
367
  health: {
367
368
  status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
368
369
  summary: connected
369
- ? '微信消息长轮询运行正常'
370
+ ? t('微信消息长轮询运行正常')
370
371
  : state === 'error'
371
- ? '微信连接未就绪,插件会自动重试'
372
- : '微信连接当前离线',
372
+ ? t('微信连接未就绪,插件会自动重试')
373
+ : t('微信连接当前离线'),
373
374
  lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
374
375
  },
375
376
  stats: {
@@ -438,11 +439,11 @@ export class WeixinController {
438
439
  record.state = 'needs_verification';
439
440
  } else if (response.status === 'verify_code_blocked') {
440
441
  record.state = 'failed';
441
- record.error = safeAccountError('verification-blocked', '配对码多次错误,请重新生成二维码。');
442
+ record.error = safeAccountError('verification-blocked', t('配对码多次错误,请重新生成二维码。'));
442
443
  break;
443
444
  } else if (response.status === 'expired') {
444
445
  record.state = 'expired';
445
- record.error = safeAccountError('expired', '二维码已过期,请重新生成。');
446
+ record.error = safeAccountError('expired', t('二维码已过期,请重新生成。'));
446
447
  break;
447
448
  } else if (response.status === 'scaned_but_redirect') {
448
449
  record.currentBaseUrl = apiBaseFromServer(response.redirect_host, record.currentBaseUrl);
@@ -453,7 +454,7 @@ export class WeixinController {
453
454
  ) ?? this.#configStore.list()[0];
454
455
  if (!existing) {
455
456
  record.state = 'failed';
456
- record.error = safeAccountError('already-bound', '该微信账号已绑定,但本机没有可恢复的凭据。');
457
+ record.error = safeAccountError('already-bound', t('该微信账号已绑定,但本机没有可恢复的凭据。'));
457
458
  } else {
458
459
  record.state = 'connected';
459
460
  record.botId = existing.botId;
@@ -465,7 +466,7 @@ export class WeixinController {
465
466
  const accountId = cleanString(response.ilink_bot_id);
466
467
  const ownerUserId = cleanString(response.ilink_user_id);
467
468
  if (!token || !accountId || !ownerUserId) {
468
- throw new WeixinApiError('incomplete-login', '微信授权成功,但返回的账号凭据不完整。');
469
+ throw new WeixinApiError('incomplete-login', t('微信授权成功,但返回的账号凭据不完整。'));
469
470
  }
470
471
  record.state = 'connecting';
471
472
  this.#touch();
@@ -485,12 +486,12 @@ export class WeixinController {
485
486
  if (!record.controller.signal.aborted && Date.now() >= record.expiresAt
486
487
  && !TERMINAL_ATTEMPT_STATES.has(record.state)) {
487
488
  record.state = 'expired';
488
- record.error = safeAccountError('expired', '二维码已过期,请重新生成。');
489
+ record.error = safeAccountError('expired', t('二维码已过期,请重新生成。'));
489
490
  }
490
491
  } catch (error) {
491
492
  if (record.controller.signal.aborted || error?.name === 'AbortError') {
492
493
  record.state = 'cancelled';
493
- record.error = safeAccountError('cancelled', '扫码绑定已取消。');
494
+ record.error = safeAccountError('cancelled', t('扫码绑定已取消。'));
494
495
  } else {
495
496
  record.state = 'failed';
496
497
  record.error = publicProvisioningError(error);
@@ -4,6 +4,7 @@ import {
4
4
  connectionTestTarget,
5
5
  connectionTestTargetUnavailable,
6
6
  } from '../shared/connection-test.mjs';
7
+ import { t } from '../shared/i18n.mjs';
7
8
 
8
9
  const DEFAULT_START_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000]);
9
10
  const HARNESS_HEALTH_ERROR_CODES = new Set([
@@ -224,7 +225,7 @@ export class WeixinRuntime {
224
225
  const code = response.errcode ?? response.ret;
225
226
  throw new WeixinApiError(
226
227
  code === -14 ? 'stale-token' : 'updates-rejected',
227
- code === -14 ? '微信登录凭据已失效,请移除账号后重新扫码。' : '微信消息同步请求被拒绝。',
228
+ code === -14 ? t('微信登录凭据已失效,请移除账号后重新扫码。') : t('微信消息同步请求被拒绝。'),
228
229
  );
229
230
  }
230
231
  consecutiveFailures = 0;
@@ -293,7 +294,7 @@ export class WeixinRuntime {
293
294
  : typeof this.#config.ownerUserId === 'string' && this.#config.ownerUserId.trim()
294
295
  ? this.#config.ownerUserId.trim()
295
296
  : null;
296
- if (!toUserId) throw connectionTestTargetUnavailable('微信机器人');
297
+ if (!toUserId) throw connectionTestTargetUnavailable(t('微信机器人'));
297
298
  if (!this.#status.ready || !this.#abortController) {
298
299
  throw new Error('Weixin runtime is not connected');
299
300
  }
@@ -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: 2, bots: Object.freeze([]) });
6
8
  const BOT_ID_PATTERN = /^whatsapp_[a-f0-9]{24}$/;
7
9
  const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
@@ -30,7 +32,7 @@ export function deriveWhatsappBotId(accountJid) {
30
32
 
31
33
  export function maskWhatsappAccount(accountJid) {
32
34
  const digits = normalizeWhatsappAccountJid(accountJid)?.split('@')[0] ?? '';
33
- if (!digits) return 'WhatsApp账号';
35
+ if (!digits) return t('WhatsApp账号');
34
36
  if (digits.length <= 7) return `${digits.slice(0, 2)}•••${digits.slice(-2)}`;
35
37
  return `${digits.slice(0, 4)}••••${digits.slice(-4)}`;
36
38
  }
@@ -1,9 +1,11 @@
1
+ import { t } from '../shared/i18n.mjs';
1
2
  import { createTextBridgeStatus, TextHarnessBridge } from '../shared/text-harness-bridge.mjs';
2
3
 
3
4
  export const WHATSAPP_DESCRIPTOR = Object.freeze({
4
5
  key: 'whatsapp',
5
6
  label: 'WhatsApp',
6
- connectionLabel: ' Web 关联设备',
7
+ // Translated lazily: t() must run after setImHostLanguage, not at import time.
8
+ get connectionLabel() { return t(' Web 关联设备'); },
7
9
  });
8
10
 
9
11
  export class WhatsappHarnessBridge extends TextHarnessBridge {
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
3
  import { connectionTestMessage } from '../shared/connection-test.mjs';
4
+ import { t } from '../shared/i18n.mjs';
4
5
  import {
5
6
  deriveWhatsappBotId,
6
7
  maskWhatsappAccount,
@@ -83,8 +84,8 @@ export class WhatsappController {
83
84
  this.#errors.set(config.botId, safeError(
84
85
  error?.code === 'relink-required' ? 'relink-required' : 'connection-failed',
85
86
  error?.code === 'relink-required'
86
- ? 'WhatsApp 关联设备已失效,请移除后重新扫码。'
87
- : 'WhatsApp 连接未就绪,插件会自动重试。',
87
+ ? t('WhatsApp 关联设备已失效,请移除后重新扫码。')
88
+ : t('WhatsApp 连接未就绪,插件会自动重试。'),
88
89
  ));
89
90
  this.#logger.warn?.(`[dsh-im:whatsapp] bot ${config.botId} failed to initialize`);
90
91
  } finally {
@@ -172,7 +173,7 @@ export class WhatsappController {
172
173
  await record.task?.catch(() => undefined);
173
174
  if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
174
175
  record.state = 'cancelled';
175
- record.error = safeError('cancelled', '扫码接入已取消。');
176
+ record.error = safeError('cancelled', t('扫码接入已取消。'));
176
177
  }
177
178
  await this.#deleteAuth(record.authDirectory).catch(() => undefined);
178
179
  }
@@ -192,8 +193,8 @@ export class WhatsappController {
192
193
  this.#errors.set(botId, safeError(
193
194
  error?.code === 'relink-required' ? 'relink-required' : 'connection-failed',
194
195
  error?.code === 'relink-required'
195
- ? 'WhatsApp 关联设备已失效,请移除后重新扫码。'
196
- : 'WhatsApp 连接仍未就绪,请稍后重试。',
196
+ ? t('WhatsApp 关联设备已失效,请移除后重新扫码。')
197
+ : t('WhatsApp 连接仍未就绪,请稍后重试。'),
197
198
  ));
198
199
  throw error;
199
200
  } finally {
@@ -209,14 +210,14 @@ export class WhatsappController {
209
210
  return this.#withBotTransition(botId, async () => {
210
211
  const runtime = this.#runtimes.get(botId);
211
212
  if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
212
- const error = new Error('WhatsApp机器人尚未连接');
213
+ const error = new Error(t('WhatsApp机器人尚未连接'));
213
214
  error.code = 'test-target-unavailable';
214
215
  throw error;
215
216
  }
216
217
  return runtime.sendConnectionTest(
217
218
  connectionTestMessage(
218
219
  `${config.name}(${maskWhatsappAccount(config.accountJid)})`,
219
- 'WhatsApp机器人',
220
+ t('WhatsApp机器人'),
220
221
  ),
221
222
  );
222
223
  });
@@ -275,8 +276,8 @@ export class WhatsappController {
275
276
  bot: { name: config.name, idMasked: maskWhatsappAccount(config.accountJid) },
276
277
  health: {
277
278
  status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
278
- summary: connected ? 'WhatsApp Web 关联设备运行正常'
279
- : state === 'error' ? 'WhatsApp 连接未就绪' : 'WhatsApp 连接当前离线',
279
+ summary: connected ? t('WhatsApp Web 关联设备运行正常')
280
+ : state === 'error' ? t('WhatsApp 连接未就绪') : t('WhatsApp 连接当前离线'),
280
281
  lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
281
282
  lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
282
283
  },
@@ -340,7 +341,7 @@ export class WhatsappController {
340
341
  await this.#startRuntime(config);
341
342
  this.#errors.delete(botId);
342
343
  } catch (error) {
343
- this.#errors.set(botId, safeError('connection-failed', 'WhatsApp 已绑定,消息连接暂未就绪。'));
344
+ this.#errors.set(botId, safeError('connection-failed', t('WhatsApp 已绑定,消息连接暂未就绪。')));
344
345
  this.#logger.warn?.(`[dsh-im:whatsapp] bot ${botId} did not reconnect after QR binding`);
345
346
  }
346
347
  if (previous?.authDirectory && previous.authDirectory !== config.authDirectory) {
@@ -363,11 +364,11 @@ export class WhatsappController {
363
364
  await this.#deleteAuth(record.authDirectory).catch(() => undefined);
364
365
  if (previous) await this.#startRuntime(previous).catch(() => undefined);
365
366
  record.state = 'cancelled';
366
- record.error = safeError('cancelled', '扫码接入已取消。');
367
+ record.error = safeError('cancelled', t('扫码接入已取消。'));
367
368
  } else {
368
369
  await this.#deleteAuth(record.authDirectory).catch(() => undefined);
369
370
  record.state = 'failed';
370
- record.error = safeError('activation-failed', 'WhatsApp 已扫码,但无法保存关联设备。');
371
+ record.error = safeError('activation-failed', t('WhatsApp 已扫码,但无法保存关联设备。'));
371
372
  this.#logger.error?.('[dsh-im:whatsapp] unable to persist linked-device session');
372
373
  }
373
374
  } finally {
@@ -380,10 +381,10 @@ export class WhatsappController {
380
381
  if (TERMINAL_ATTEMPT_STATES.has(record.state)) return;
381
382
  if (record.controller.signal.aborted || error?.name === 'AbortError') {
382
383
  record.state = 'cancelled';
383
- record.error = safeError('cancelled', '扫码接入已取消。');
384
+ record.error = safeError('cancelled', t('扫码接入已取消。'));
384
385
  } else {
385
386
  record.state = 'failed';
386
- record.error = safeError('qr-connect-failed', '无法连接 WhatsApp,请重新生成二维码。');
387
+ record.error = safeError('qr-connect-failed', t('无法连接 WhatsApp,请重新生成二维码。'));
387
388
  }
388
389
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
389
390
  await record.session?.close().catch(() => undefined);
@@ -7,6 +7,7 @@ import {
7
7
  } from '@whiskeysockets/baileys';
8
8
 
9
9
  import { splitMessageText } from '../shared/editable-message-stream.mjs';
10
+ import { t } from '../shared/i18n.mjs';
10
11
  import { ImagePromptError } from '../shared/image-prompt.mjs';
11
12
  import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
12
13
  import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
@@ -107,7 +108,7 @@ async function downloadWhatsappImage(message, download, {
107
108
  throw new ImagePromptError(
108
109
  'image-download-failed',
109
110
  `WhatsApp image download timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
110
- '图片下载失败,请重新发送后再试。',
111
+ t('图片下载失败,请重新发送后再试。'),
111
112
  );
112
113
  }
113
114
  throw error;
@@ -126,7 +127,7 @@ async function downloadWhatsappImage(message, download, {
126
127
  throw new ImagePromptError(
127
128
  'image-too-large',
128
129
  `WhatsApp image exceeded ${maxBytes} bytes`,
129
- '图片超过 5 MB,请压缩后重试。',
130
+ t('图片超过 5 MB,请压缩后重试。'),
130
131
  );
131
132
  }
132
133
  chunks.push(data);
@@ -137,7 +138,7 @@ async function downloadWhatsappImage(message, download, {
137
138
  throw new ImagePromptError(
138
139
  'image-download-failed',
139
140
  `WhatsApp image stream timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
140
- '图片下载失败,请重新发送后再试。',
141
+ t('图片下载失败,请重新发送后再试。'),
141
142
  );
142
143
  }
143
144
  throw error;
@@ -313,12 +314,31 @@ function waitWithSignal(promise, signal) {
313
314
 
314
315
  function uncertainArtifactDelivery(error) {
315
316
  if (error?.code === 'artifact-delivery-uncertain') return error;
316
- const uncertain = new Error('WhatsApp could not confirm result file delivery.');
317
+ const uncertain = new Error('WhatsApp could not confirm artifact delivery.');
317
318
  uncertain.code = 'artifact-delivery-uncertain';
318
319
  uncertain.cause = error;
319
320
  return uncertain;
320
321
  }
321
322
 
323
+ function whatsappArtifactError(error) {
324
+ if (error?.code?.startsWith?.('artifact-')) return error;
325
+ const status = error?.output?.statusCode
326
+ ?? error?.data?.statusCode
327
+ ?? error?.statusCode;
328
+ let code;
329
+ if (status === 401 || status === 403) code = 'artifact-permission-required';
330
+ else if (status === 413) code = 'artifact-too-large';
331
+ else if (status === 429) code = 'artifact-rate-limited';
332
+ else if ([400, 404, 405, 406, 410, 415, 422].includes(status)) {
333
+ code = 'artifact-provider-rejected';
334
+ }
335
+ if (!code) return uncertainArtifactDelivery(error);
336
+ const wrapped = new Error('WhatsApp rejected artifact delivery.');
337
+ wrapped.code = code;
338
+ wrapped.cause = error;
339
+ return wrapped;
340
+ }
341
+
322
342
  export class WhatsappBotClient {
323
343
  #socket;
324
344
  #outboundIds;
@@ -354,14 +374,32 @@ export class WhatsappBotClient {
354
374
  }
355
375
 
356
376
  async sendFile(target, file) {
377
+ return this.#sendArtifact(target, file, {
378
+ document: file.bytes,
379
+ mimetype: file.mediaType ?? 'application/octet-stream',
380
+ fileName: file.fileName,
381
+ }, 'file');
382
+ }
383
+
384
+ async sendImage(target, file) {
385
+ return this.#sendArtifact(target, file, {
386
+ image: file.bytes,
387
+ mimetype: file.mediaType ?? 'image/jpeg',
388
+ }, 'image');
389
+ }
390
+
391
+ async #sendArtifact(target, file, content, presentation) {
357
392
  this.#signal?.throwIfAborted();
358
393
  await this.#stopTyping(target.jid);
359
394
  this.#signal?.throwIfAborted();
360
395
  const deliverySeed = typeof file.deliveryKey === 'string' && file.deliveryKey
361
396
  ? file.deliveryKey
362
397
  : file.artifactId;
363
- const messageId = typeof deliverySeed === 'string' && deliverySeed
364
- ? createHash('sha256').update(deliverySeed).digest('hex').slice(0, 20).toUpperCase()
398
+ const messageIdSeed = presentation === 'image'
399
+ ? `${deliverySeed}:image`
400
+ : deliverySeed;
401
+ const messageId = typeof messageIdSeed === 'string' && messageIdSeed
402
+ ? createHash('sha256').update(messageIdSeed).digest('hex').slice(0, 20).toUpperCase()
365
403
  : undefined;
366
404
  const options = {
367
405
  ...(target.quoted ? { quoted: target.quoted } : {}),
@@ -375,11 +413,7 @@ export class WhatsappBotClient {
375
413
  try {
376
414
  const pending = this.#socket.sendMessage(
377
415
  target.jid,
378
- {
379
- document: file.bytes,
380
- mimetype: file.mediaType ?? 'application/octet-stream',
381
- fileName: file.fileName,
382
- },
416
+ content,
383
417
  options,
384
418
  );
385
419
  trackOutboundArtifactProviderPromise(file, pending);
@@ -390,7 +424,7 @@ export class WhatsappBotClient {
390
424
  result = await waitWithSignal(pending, waitSignal);
391
425
  } catch (error) {
392
426
  if (this.#signal?.aborted) throw abortReason(this.#signal);
393
- throw uncertainArtifactDelivery(error);
427
+ throw whatsappArtifactError(error);
394
428
  }
395
429
  this.#signal?.throwIfAborted();
396
430
  this.#outboundIds.remember(result?.key?.id);
@@ -607,7 +641,7 @@ export class WhatsappRuntime {
607
641
 
608
642
  async sendConnectionTest(text) {
609
643
  if (!this.#status.ready || !this.#client) {
610
- const error = new Error('WhatsApp机器人尚未连接');
644
+ const error = new Error(t('WhatsApp机器人尚未连接'));
611
645
  error.code = 'test-target-unavailable';
612
646
  throw error;
613
647
  }
@@ -7,6 +7,8 @@ import makeWASocket, {
7
7
  useMultiFileAuthState,
8
8
  } from '@whiskeysockets/baileys';
9
9
 
10
+ import { t } from '../shared/i18n.mjs';
11
+
10
12
  const SILENT_LOGGER = Object.freeze({
11
13
  level: 'silent',
12
14
  trace() {},
@@ -57,7 +59,7 @@ function normalizeIdentity(socket, authState) {
57
59
  return {
58
60
  accountJid,
59
61
  name: typeof source?.name === 'string' && source.name.trim()
60
- ? source.name.trim().slice(0, 100) : 'WhatsApp机器人',
62
+ ? source.name.trim().slice(0, 100) : t('WhatsApp机器人'),
61
63
  };
62
64
  }
63
65